Welcome to MSDN Blogs Sign in | Join | Help

Custom Clouds

It is possible (and pretty easy) to add your own weather data (based on your own data sources, or just for fun) to VE3D using WorldEngine.Environment.Weather.  For example, to add some nice fluffy clouds all over:

 Host.WorldEngine.Environment.Weather.Clear();

// specify the type of clouds you want, their density (0 to 1), and their altitude in meters
CloudLayer cloud = new CloudLayer(CloudType.CumulusHumilis, 0.3f, 2000);

// specify the layer you want, its level (strictly speaking the altitude does not have to be in order
// of the layers, but it helps to keep them organized), and a timestamp for when the data is
// relevant -- for automatically generated data like this Now is sufficient.
// Wind and rain are not implemented.
WeatherLayer layer = new WeatherLayer(cloud, CloudLayerLevel.Low, DateTime.Now);

// To add weather evenly everywhere, use LatLon.Empty.  To add a specific location, specify that location.
// The closest points to the camera's position will be used to determine actual weather.
WeatherPoint weather = new WeatherPoint(LatLon.Empty, layer);
Host.WorldEngine.Environment.Weather.AddPoint(weather);

Here's a view of a two-layer cloud system:

Clouds in Seattle

You can find some sample code here.

Posted by NikolaiF | 0 Comments
Filed under: ,

New version of InfoStrat.VE: VE3D in WPF

Goodness for the new year: 

Globe Screenshot

Have you seen the Microsoft Surface Globe application in the Microsoft Touch Pack for Windows 7?  There are plenty of videos of the app in action on YouTube: http://tinyurl.com/YouTubeSurfaceGlobe.

Would you like to build an app like this yourself?  Thanks to InfoStrat.VE, you can! Josh Blake just announced the R2 release of InfoStrat.VE.  Amongst other improvements, it includes support for the same touch interaction on Windows 7 you have available in the Microsoft Surface Globe app.  Full details on Josh’s blog:

http://nui.joshland.org/2010/01/infostratve-release-2-is-now-up.html

 Via Marc

Posted by NikolaiF | 0 Comments

Two good questions

Posting some answers to questions I've gotten recently.

Bug in the samples:

Kind of silly that I hadn't noticed this until now, but there's an error in the sample html for most of the samples.  The name of the functions used for plug-in load and activate conflict with an internal name used in the AJAX code.  Change "On3DPlugInLoaded" and "On3DPlugInActivated" to any other name, for example "PlugInLoaded" and "PlugInActivated".  Remember to change both your function name and the string passed to AttachEvent.

Zoom or Rotate around a point:

There have been several requests about how to allow people to zoom or rotate around a particular point on the screen, rather than the center.  The code here isn't as cleaned up as I normally like it, but it illustrates the ideas.  You can do the rotate by purely calling built-in VE3D functions via the bindings system, but zoom requires a bit more work.  To run the code, replace files in the Multitouch sample with the files in the zip.  You will want to call or adapt the calls to GestureEventSource to be in response to your inputs, rather than the worker thread I created.

Remember you can contact me any time via the contact form, or if you prefer via comments.  Even if it looks like I haven't posted recently :)

Posted by NikolaiF | 0 Comments
Filed under: , ,

Installing VE3D without the setup package

In some scenarios it may be desirable to install the VE3D engine without visiting the www.bing.com/maps site, and/or to avoid the individual downloads that the setup package performs (eg offline or enterprise installations).

The main installation msi files are available at:

http://go.microsoft.com/fwlink/?LinkID=117319 for x86

http://go.microsoft.com/fwlink/?LinkID=117320 for x64 systems

If installed this way, the pre-requisites must already be installed:  the .NET 2.0 Framework and Windows Imaging Components.  Vista and Windows 7 already have both of these by default, for XP the latest versions are available by searching for them on the Microsoft Download Center.

For convenience, here are some links to the prerequisites:

http://go.microsoft.com/fwlink/?LinkID=106132 .NET 2.0 x86

http://go.microsoft.com/fwlink/?LinkID=106134 .NET 2.0 x64

http://go.microsoft.com/fwlink/?LinkID=101060 WIC x86

http://go.microsoft.com/fwlink/?LinkID=101061 WIC x64 

 

Posted by NikolaiF | 0 Comments
Filed under: ,

Hosting VE3D in native code

Sorry for the glut of posts this week.  I hadn't been able to write much, but there were still interesting topics and questions coming up and I'm trying to address that backlog now.  So, without further ado:

It is possible to host VE3D in a variety of contexts, and so far we've seen WinForms, WPF, and the browser.  It is also possible to host in straight native code.  In this sample, I address the COM interactions needed to host VE3D, and use OpenGL as an example hosting environment.

Download the code here.

VE3D on an OpenGL cube

This sample demonstrates four concepts:

Interaction with native code.
Back buffer retrieval.
Direct camera control.
OpenGL integration.

Native code:

Managed code provides easy and convenient methods for exposing your code to COM, and hence to native callers.
From the managed side, check "Register for COM interop" in your project's Build tab.  Decorate an interface
with ComVisible(true) and provide a guid, and the backing class with the interface type as shown.  Your types
are now visible and callable.

On the native side, you can now instantiate your managed code using typical COM calls, as shown.  It can then
be called similarly to any other object.

To add a new function to the interface, simply add it to the interface file and the backing .cs file, then
recompile.  By designing your interface in appropriately, you can then decide whether to do most of your logic
in native or managed code, depending on where you feel more comfortable.

Backbuffer retrieval:

The Render function provides the most efficient way of pulling VE3D's back buffer into main memory.  In general
it is better to not do this, rather let the hardware render to the screen directly, but some situations demand
using the scene in some other fashion.  Here, we get the memory as a direct pointer.  Note that this approach
assumes that you are handling any format and stride issues yourself.

It is also possible to get a graphics object from systemMemorySurface, an HDC from that, and then use functions
like BitBlt to copy out data and handle some of these issues for you.

Direct camera control:

Most samples thus far have demonstrated use of bindings and actions, or deriving camera controller.  The
CameraController class here shows how to write a controller that can react directly to user input, modifying
camera values directly.  The "best practices" method is to wrap your input device in a EventSource, and use
bindings and actions to communicate the information to your controller.  These structures provide simple
remapping of inputs, if necessary, and handle any threading or marshalling issues. 

However, it is also possible and sometimes appropriate to take the simpler approach used here.

OpenGL integration:

It is possible to integrate VE3D into an existing OpenGL application.  Using the Render method described above,
the pointer produced is suitable for direct consumption by OpenGL.  There are differences in how textures are
handled by the two APIs, but these are fairly simple to overcome, and are described in DrawOpenGLWindow().

Note that there is nothing that limits this sample to OpenGL, anything that can consume the buffer as provided
can host VE3D in the exact same manner.

Note on debugging:

The Visual Studio debugger requires some direction on how to deal with mixed native and managed code.
In the VE3DOpenGL properties page, expand Configuration Properties, click Debugging, then choose Debugger
Type.  You can elect to only attach to native, only to managed, or to both ("mixed").  "Auto" in this case
will be native-only.  If you find your breakpoints are not hitting, it is likely this setting.

Note on glut.h:

For some reason, in the release version only, I managed to get it thinking that it needed glut and I haven't
been able to figure out why.  If you encounter problems while compiling due to glut, just run in debug mode.
This is a problem that is specific to the sample app, not the methodology used.

Enjoy, and as usual please let me know about any questions or problems you may have.

Posted by NikolaiF | 0 Comments

GraphicsProxy RenderState

I've gotten a few questions lately about how to do some alpha effects using models, specifically MeshGraphicsObject.  After you create the GraphicsObject, it has a RenderState object available on it.  The fields on this object will be familiar to those experienced with DirectX, but here's a rundown of a few especially useful ones:

Allow your model to cast shadows on the ground.

            newMesh.RenderState.Lighting.CastShadows = true;

Allow your model to have directional shading (you can adjust the direction of the light using Host.WorldEngine.Environment.SunPosition).

            newMesh.RenderState.Lighting.DirectionalLightEnabled = true;

Allow your model to use transparency, whether from vertex colors, textures, or TextureFactor (see below).

            newMesh.RenderState.Alpha.Enabled = true;
            newMesh.RenderState.Alpha.SourceBlend = Blend.SourceAlpha;
            newMesh.RenderState.Alpha.DestinationBlend = Blend.InvSourceAlpha;

Allow use of TextureFactor to control the overall color of the model.  For example, you can create a model that has a texture with a white patch, and then create two models, one red and one blue, both using the same texture but differing on the value of TextureFactor.

            newMesh.RenderState.Stages[0].Blending.ColorArgument1 = TextureArgument.TextureColor;
            newMesh.RenderState.Stages[0].Blending.ColorArgument2 = TextureArgument.TFactor;
            newMesh.RenderState.Stages[0].Blending.ColorOperation = TextureOperation.Modulate;

Allow use of TextureFactor to control the overall opacity of the model.  This is great for fading effects, as changing the TextureFactor per-frame is cheap.

            newMesh.RenderState.Stages[0].Blending.AlphaArgument1 = TextureArgument.TextureColor;
            newMesh.RenderState.Stages[0].Blending.AlphaArgument2 = TextureArgument.TFactor;
            newMesh.RenderState.Stages[0].Blending.AlphaOperation = TextureOperation.Modulate;

Change the filtering used for textures.  For example, in the XFile sample, if you zoom in close to the model the texture gets blocky.  Dropping this code in improves the situation.

            newMesh.RenderState.Stages[0].Sampler.MagnificationFilter = TextureFilter.Linear;
            newMesh.RenderState.Stages[0].Sampler.MinificationFilter = TextureFilter.Linear;

The TextureFactor value mentioned above.  This value would make the model ghostly transparent and red-tinged.  This (and other values in RenderState) can be cheaply changed every frame.

            newMesh.RenderState.TextureFactor.Value = Color.FromArgb(128, Color.Red);

One additional note:  to use alpha, you must also call AddAlphaRenderable in your actor's Render function instead of just AddRenderable.  The distance is a sorting function that lets the renderer know what order to draw the alpha in (affects blending).  A constant value works in some situations, but if you find things look wrong, especially when one of your models occludes another, try using a value based on the distance between the model and the camera.

Here's a few shots to demonstrate the effects of these switches:

XFile Model

Everyone's favorite XFile sample model.

XFile Model, lit

The same model, with shadows and directional lighting (the sun is above and a bit behind, if you move it the shading and shadows move appropriately).

XFile Model, alternate texture

The same again, with an alternate texture that contains transparency and the various alpha switches on.  Note one detail:  the shadow casting code does not account for transparency.

XFile model, all switches

Same again, with all switches described above turned on.  The texture filtering is improved (look for blockiness in the center of the body in the previous shot), the color is shifted red, and the whole model is somewhat transparent, even parts where the texture is opaque.

Posted by NikolaiF | 2 Comments
Filed under: , ,

X File bulk display

I was asked the other day about how to load multiple x file -based models into VE3D without having to place each one manually.  I've written up a sample here.  The idea is more or less a fusion of the existing ActorDataSource and XFile samples, using the actor from the XFile sample rather than the bunnies.  I've also mocked up a very simple data file that contains placement information, rather than generating data on the fly as for the bunnies.

The way I've done this, the parsing code for the x files is done in the actor.  In this way, swapping out file formats should be straightforward so long as you have parsing code for it (sorry, only x files are directly implemented in the engine, but the results of any parse should still be easily convertible into VE3D graphics objects).

For the sake of simplicitly I did do one thing wrong.  The flow of the code is like this:  multiple background threads all execute QueryPrimitivesInternal at the same time, and when they are finished, they move through a lock into ActorBuilder.  They are still on background threads, but they now execute serially.  So, really the data retrieval (in this case, loading from resources, but probably a network request) and the parsing, as appropriate, should happen in QueryPrimitivesInternal, and the action in ActorBuilder should be kept to a minimum.  I violated this rule in the sample code for the sake of illustration, but you should be aware of it.

Finally, an important consideration is level of detail.  Dense and numerous models can slow down loading and rendering.  DataSources take advantage of the ActorBounds and scale concepts, such that it is possible to use simplified models at certain LODs, and switch to more detailed as the user zooms in.  If your needs are less extreme, then you can simply use scale to cause models to stop rendering when not in view, and at a certain distance, which is what I have done in the sample.

Have fun!

Posted by NikolaiF | 0 Comments

Constraining camera movement

Just got a good question:  what if I want to prevent the camera from moving outside certain bounds, for example to restrict altitude?

Technically it may be best to implement your own CameraController, so that you can make sure the restrictions are done smoothly for all cases.  But for many cases a simple mechanism will do.  Here it is:

this.Host.CameraControllers.CameraChanged += new CameraControllerManager.CameraChangedEventHandler(CameraControllers_CameraChanged);

 

void CameraControllers_CameraChanged(Microsoft.MapPoint.Rendering3D.Cameras.PredictiveCamera camera)

{

   if (camera.Viewpoint.Position.Altitude > 10000)

   {

      camera.Viewpoint.Position.Altitude = 10000;

   }

}

Posted by NikolaiF | 0 Comments
Filed under:

Actor data source questions

I just fielded a few good questions via email about actor data sources, and thought it would make a good post.

Actor data sources are a way of getting little bits of your code (actors) into the world, handing spatial indexing and cache management for you.  When each actor is in view, they are given the opportunity to execute code in the Render and Update functions.  When the cache is full and an actor is evicted, they again may execute code in the OnRemove function.

You create them by implementing a DataSource with usage Actor, and an ActorBuilder which translates the Primitives returned into Actor objects, along with information like what areas of the Earth the actors should be used in, as an ActorBounds object.  All of this can be seen in the ActorDataSource sample.

In the sample, however, only one Primitive is returned per request, and only one Actor.  What if you want more?  The important thing to know is that there has to be a 1-1 correspondence between Primitives and Actors *.  You must create one Actor for every Primitive used.  If you create more than one Actor for a single Primitive, only one will appear.

The reason for this is the Actor update story.  It is possible to update an Actor already in the system by two methods.  When an Actor is updated, the PrimitiveId from the passed-in primitive is used to find and update the actor in question.  If you add multiple actors per primitive, an ambiguity arises as to what you actually want to do.

It is possible to resolve this ambiguity by creating "nested" actors.  This is simple.  Just create the actors you want, and then a "container"

*:  actually you can create a single Actor with multiple Primitives, but I don't want to confuse the issue for now.

Updating an Actor:

Expiration.  On the Entity attached to your primitive, add a property "ExpiresInSeconds":

e.EntityTypes["bunny"].Properties.Create("ExpiresInSeconds", typeof(double));

...

entitySpec.Properties.SetValue("ExpiresInSeconds", 10.0);

Your QueryPrimitivesInternal function will be called again for every area the affected Actors live, and OnRemove will be called for the expiring Actors.

Manual.  You can specify a specific PrimitiveId to update using OnDataChanged in your source DataSource.

PrimitiveIdWriteableCollection wc = new PrimitiveIdWriteableCollection();
wc.Add(new BunnyPrimitiveId(this, tile));
OnDataChanged(new SpatialExtent(null, null, wc.GetReadOnlyCopy()));

You must also implement the QueryPrimitivesByIdsInternal function.  A collection of ids is provided, and you must return an array of Primitives that matches the input (so, the first item in the id collection should result in a Primitive at array index 0, etc).  If an element of the array is left null, that means to delete the Actor corresponding to that PrimitiveId.  Your ActorBuilder is then called to allow you to create the new replacement Actors.

You can also specify a region of the Earth to update in the SpatialExtent, but update by entity is not supported.  Updating very large areas can be expensive, so use spatial regions with care.  Update by PrimitiveId is the most efficient.

Posted by NikolaiF | 0 Comments

Bing Maps 3D

As you may be aware, the website changed names to Bing Maps:  http://www.bing.com/maps/.  The 3D view on the site is now called "Bing Maps 3D".

As a developer it doesn't change much for you, and in terms of the engine/SDK it's still valid to refer to it as "Virtual Earth 3D".  The only technical issue I can think of is to repeat that it's a good idea to use fwlinks for the urls for our base layer data, as explained at the end of the post here.

Thanks!

Posted by NikolaiF | 0 Comments

Data Format update and reminder

You may recall that I posted about a data format change for the new version.  The final switch-over is going to happen July 9th.  If you are still using an old version, models will stop displaying.  So upgrade now, the new version is better anyway.

For those already on the new version (assembly version 4), you do not need to change anything at all.

The plus side is that all of you developers have, for reasons of compatibility, been getting the old data for some time now, even if you have upgraded (right now, only Bing Maps gets the new data, everyone else gets the old).  On that date you will automatically start getting the new data, and yes you will notice.

Posted by NikolaiF | 0 Comments
Filed under:

WMS Data

I was having a discussion with Kurt Guenther from Infusion yesterday on the topic of WMS servers and VE3D.  There is a large amount of very interesting spatial data out there served by Web Map Services, or WMS servers.  VE3D is able to process this data using ConnectionParameter-based DataSources.  Setting it up is pretty easy:

ConnectionParameters cp = new ConnectionParameters(http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Portland/Portland_ESRI_Neighborhoods_AGO/MapServer/export?bbox={16},{17},{18},{19}&bboxSR=4326&layers=&layersDefs=&size=256,256&imageSR=102113&format=png&transparent=true&dpi=&f=image);

            string connectionParameters = cp.ToString(); 

            DataSourceLayerData layerData = new DataSourceLayerData(LayerName, Name, connectionParameters, DataSourceUsage.TextureMap);

            Host.DataSources.Add(layerData);

Drop this in to your Activate (plug-in) or Initialized handler (winforms etc) app and fly to Portland, OR to see the data (thanks to ArcGIS for the sample data and Kurt for the pointer).

I won't go into all the details of the WMS spec here, but I will call out a few important parts.  First, in order for this to work you must specify the bbox to be in Geographic coordinates (4326).  This is usually the default.  Second, you must pay attention to the returned projection.  In the above case, we have instructed the server to return 102113, which is Web Mercator, VE3D's native image projection.  This data can be consumed directly with very little overhead.  However, different servers support only certain formats and projections, and you must be careful for any particular server (it is possible to query this using WMS's GetCapabilities).  Nearly all support returning images in Geographic coordinates, 4326.  It is possible to instruct VE3D to reproject this to Web Mercator (this is the only reprojection VE3D will do for you, fortunately it will work for the majority of data).

Now, to look at ConnectionParameters and what it can do for you.  First, there's the big url.  Notice that the bbox has string.Format replacement variables in it:  {16}, {17}, etc.  Requests to the server are made on a per-tile basis (the T key is useful here).  For each request, these variables are replaced with data specific to the tile in question.  Here is a list of available parameters:

0 Reserved

1 Map Style

2 Round-robin integer

3 Reserved

4 Quadkey

5 Extension

6 Generation

7 Stripe (0-3)

8 Tile Host

9 App Host

10 Language code

11 Region code

12 Tile LOD

13 Tile X

14 Tile Y

15 Low order stripe (0-1)

16 MinLong

17 MinLat

18 MaxLong

19 MaxLat

20 RequestToken

21 Culture name

The most interesting ones are 4 (the main quad-key, very useful for already-tiled data), 12, 13, 14, and and 16-19.  To see some of this in action, check out the TerrainImages sample (also available in the sample code of course).  Programatically, you can investigate these values using the TileId class, using GetRequestCode, GetPosition, and GetLatLonBoundingBox.

As for other values on ConnectionParameters, you can control reprojection (if your data is in Geographic, specify CoordinateReferenceSystem = WGS84CoordinateReferenceSystem.Instance, as noted before this is the only reprojection natively supported, so otherwise leave this field uninitialized), the bounds of where the data is valid is terms of Lat/Long and LOD (useful to reducing unnecessary network requests), and caching behavior.

Caching behavior deserves special discussion, because WMS servers are often quite slow.  By default, data loaded using ConnectionParameters is not cached locally.  Once it moves out of memory, it must be re-queried.  If you set CacheRetention however, the data will be kept locally for the period of time specified (unless it is evicted due to space restrictions in the interim).  If you set CacheRetention = new TimeSpan(24, 0, 0), for example, for 24 hours from the time of query a given tile will not be re-queried.  Note that some servers are dynamic, such as weather data, so it's important to use an appropriate value here.

And just to give Kurt a poke in the ribs, please remember to only add DataSources after/during the Initialized event!

 Update:

John Fletcher from Latitude Geographics pointed out that the example I give above isn't actually WMS compliant in its query parameters.  He suggests an alternate sample, of the British Columbia, CA area:

ConnectionParameters p = new ConnectionParameters("http://openmaps.gov.bc.ca/mapserver/libcwms2?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=DBM_7H_MIL_BATHYMETRIC_POLY,BC_LABEL,DBM_7H_MIL_DRAINAGE_LINE,DBM_7H_MIL_DRAINAGE_POLY,DBM_7H_MIL_ROADS_LINE,DBM_7H_MIL_POLITICAL_POLY_PS,DBM_7H_MIL_POPULATION_POINT&STYLES=,,,,,,&SRS=EPSG:4326&BBOX={16},{17},{18},{19}&WIDTH=256&HEIGHT=256&FORMAT=image/png&TRANSPARENT=TRUE&EXCEPTIONS=application/vnd.ogc.se_inimage&");

p.CoordinateReferenceSystem = Microsoft.MapPoint.CoordinateSystems.Wgs84CoordinateReferenceSystem.Instance;

Host.DataSources.Add(new DataSourceLayerData("foo", "bar", p.ToString(), DataSourceUsage.TextureMap));

John also provided a link to the WMS spec repository (http://www.opengeospatial.org/standards/wms).  Note that the sample above is for WMS 1.1.1.  The most current version is 1.3.0, but many servers use the older, or support both.  No matter which you use (or even for something a little off spec like my original example) the interface with VE3D is the same.

Posted by NikolaiF | 0 Comments
Filed under: , , , ,

Modifying cache location

My, it has been a long time since I posted.  I promise I will try to be better.

Recently I was asked if it is possible to change the location of the cache file.  It is, with certain minor restrictions.  You can change the location by adding this to user.config:

      <setting name="PersistentCacheDirectory" serializeAs="String">
        <value>path here</value>
      </setting>

 

Or, when creating the GlobeControl directly, pass in a GlobeControlInitializationOptions object with PersistentCachePath set.

 

In either case, you can either specify a full path to anywhere, or a relative path from the current location (\AppData\LocalLow\Microsoft\Virtual Earth 3D\).

Putting the file outside LocalLow will cause problems when running in IE7/8 protected mode (Vista/Win7 with UAC on).  For a WinForms or WPF-based solution, it’s fine.  In such a case, the GlobeControlInitializationOptions setting is generally preferred as it will prevent problems when running Bing Maps on the same machine.  On the other hand, this will also cause two caches to be written, one in the default location and containing data from runs in Bing Maps, and one in the specified location.  The best approach depends on your exact usage.

 

Posted by NikolaiF | 0 Comments
Filed under: ,

Manual Rendering

Folks have really reacted to my mention of manual rendering, so I've made a sample for it.  Good thing, too, because to be frank there are a few warts.  On the upside, however, it is possible to work around all of them and the code required is not too scary.

Get the code!

In this sample, I host a GlobeControl on a form like normal, but instead of using the built-in render thread I render on the UI thread using the manual render API.  If you are content to let WinForms present to the screen for you, you're done at this point.

But it is common in these situations to want to get the rendered surface to make a movie, present in some other context like WPF, etc.  I include an example of the most basic way to do this, which is the only official way to currently do it (WorldEngine.CaptureScreenShot).  If this isn't good enough, and you're a bit more daring, it is possible to get our rendering surface and do more clever things with it.  That's what the good folks at Infostrat did for their WPF code, and for the time being I actually recommend using their wrapper if you want to use WPF.

I've labelled all the warts and their workarounds with the tag "WORKAROUND" in the code so you can look more closely if you like, but you should also just be able to copy and paste the sample and get your code to work as it should.  Also, this is not actually an an official part of the sample pack, but something similar to it will be.

Anisotropic Filtering

Anisotropic has been a bit of a frustration for us because while it is very easy to do (it's really just a flag to DirectX), the compatibility issue I mentioned before made it a pain to actually get it in to the control.  It's a shame we couldn't turn it on by default, but at least it's there.  The effect of it is really quite dramatic, as you can see below.  I took these images at this location, near the 405 and 90 interchange in Bellevue.

With anisotropic filtering enabled:

Anisotropic filtering

Without:

Trilinear Filtering

To turn it on in the website, click "options" in the upper right corner, then "3D settings", and finally check "Use anisotropic filtering".  To turn it on in code:

plugIn.Host.RenderEngine.Graphics.Settings.UseAnisotropicFiltering = true;

These settings are remembered in the user.config file for future runs of the control.  The perf penalty for this on cards that are even remotely modern is almost zero, so go turn it on now!

Posted by NikolaiF | 3 Comments
Filed under: ,
More Posts Next page »
 
Page view tracker