Friday, December 6, 2013

Bing 3D Maps using WebGL and BabylonJS


In this post, we will look at how we can use BabylonJS and WebGL  to create a 3D height map and route path using data from Bing Maps REST Services.

TRY THE DEMO [IE11, Chrome, or FF required] |  
DOWNLOAD THE SOURCE




NOTE: Before you read any further, please note that at the time of this writing, there is rumored to be a native Bing Maps 3D WebGL control, and the release is likely imminent! Still, this sample will be very useful if you are interested in terrain mapping and other aspects of using BabylonJS!

Overview

Now that IE11 supports WebGL along with Chrome, Firefox, and a host of other browsers, I am betting that we will see a resurgence of interest in this 3D browser tech. BabylonJS is an easy to use 3D library written in JavaScript that leverages the power of WebGL to create cross-platform 3D experiences. If we couple that with the Bing Maps API, which allows querying of satellite imagery, driving directions, and elevation data, we can fairly easily build a realistic 3D scene of any area on earth!

Bing Maps Tile System

When you visit Bing Maps (or google maps for that matter), the view you get is actually composed of a series of square "tiles" - each being 256x256 pixels wide. As you pan the map around, new tiles are loaded into the view so that it is filled.

The details on this tile system are available on this MSDN page, but in short, each 256x256 tile has a "Quad Key" which is a numeric representation based on Zoom Level and Location.



At the bottom of the msdn article mentioned, pay special notice to the sample code. These functions let us get tiles for any point (latitude, longitude) on the map. To get a tile image's Quad Key for a particular Latitude and Longitude, we first convert the Lat/Long to a Pixel Offset in the world map, then convert the Pixel Offset to a Tile Offset and then finally convert the Tile Offset to a Quad Key.

NOTE: All of the code in the msdn article is given in C#, but the download project has a JavaScript conversion of these utility functions.

Querying Elevations using Bing
Back in January, Bing Maps added an Elevations API which allows for querying of elevation data for a bounding box on the map. This msdn article gives the details on the REST API for this data.
For our purposes, we will query a set of equally spaced positions on each tile to get the elevation for those points. We can then later set the e levation (Y-axis) values of those points to create a 3D effect.
Bounding box grid showing order of elevations
Note that, to query Elevations and Directions from the Bing Maps API, you will need a developer key, which you can get here. Note that Bing has pretty generous licensing for mobile apps and some websites.
Once you get your key, you will need to modify ProxyController.cs in the sample code to use your key, on this line:

const
string _bingMapsKey = " *** ENTER YOUR BING MAPS API KEY HERE *** ";

Displaying the Tiles and Elevation in 3D

Now that we have the Image Tiles and the Elevation data from Bing Maps, we can display these in Babylo nJS. In 3D Space, we have X,Y, and Z axes to display our 2D tiles plus elevation data on. Below is how we'll do it:




As you can see in the image above, the 2D tiles will stretch in the X (latitude) and Z (longitude) axes, and the Y-axis will be used to show the elevation.

The key to creating the tile's elevation (height map) is the following function. It takes a list of elevations and applies them to the Y-axis (height) value of a tile.

BINGWEBGL.Engine.prototype.createGroundFromElevationData = function (name, elevations, width, height, subdivisions, minHeight, maxHeight, scene, updatable) {
        var ground;

        ground = this._scene.getMeshByName(name);
        if (ground == null)
            ground = new BABYLON.Mesh(name, this._scene);

     &nbsp ;  ground._isReady = false;

        var indices = [];
        var positions = [];
        var normals = [];
        var uvs = [];
        var row, col;

        // Getting height map data
        var heightMapWidth, heightMapHeight;
        if (elevations != null) {
            heightMapWidth = elevations.length / 2
       &nbsp ;    heightMapHeight = elevations.length / 2;
        }

        // Vertices
        var elevationIndex = 0;
        for (row = 0; row <= subdivisions; row++) {
            for (col = 0; col <= subdivisions; col++) {
                var position = new BABYLON.Vector3((col * width) / subdivisions - (width / 2.0), 0, ((subdivisions - row) * height& lt;/SPAN>) / subdivisions - (height / 2.0));

                // Compute height
                if (elevations != null) {
                    var heightMapX = (((position.x + width / 2) / width) * (heightMapWidth - 1)) | 0;
           &am p;nbsp;        var heightMapY = ((1.0 - (position.z + height / 2) / height) * (heightMapHeight - 1)) | 0;

                    position.y = (elevations[elevationIndex] - this._meanElevation);  // Math.random() * 20;
                    elevationIndex++;
                }

                // Add  vertex
                positions.push(position.x, position.y, position.z);
                normals.push(0, 0, 0);
                uvs.push(col / subdivisions, 1.0 - row / subdivisions);
            }
        }

        // Indices
        for (row = 0;&l t;/SPAN> row < subdivisions; row++) {
            for (col = 0; col < subdivisions; col++) {
                indices&l t;SPAN class=p>.
push(col + 1 + (row + 1) * (subdivisions + 1));
                indices.push(col + 1 + row * (subdivisions +1
));
                indices.push(col + row * (subdivisions + 1));

                indices.push(< /SPAN>col + (row + 1) * (subdivisions + 1));
                indices.push(col + 1 + (row + 1) * (subdivisions + 1));
                indices.push(col + row * (subdivisions + 1));
            }
        }

        // Normals
        BABYLON.Mesh.ComputeNormal(positions, normals, indices);

        // Transfer
        ground.setVerticesData(positions, BABYLON.VertexBuffer.PositionKind, updatable);
        ground.setVerticesData(normals, BABYLON.VertexBuffer.NormalKind, updatable);
        ground.setVerticesData(uvs, BABYLON.VertexBuffer.UVKind, updatable);
        ground.setIndices(indices);

        ground._updateBoundingInfo();

        ground._isReady = true;

        return ground;</ FONT>
    };

}
Summary
This is just a short post to give an overview of the concepts used for the Babylon/Bing WebGL demo. If you are interested in more details, I suggest you check out the source code and post any questions in the comments area below!