Tuesday, January 6, 2015

Displaying WebGL on Bing Maps (using Pixi.js)

Something that has been on my backlog for some time is trying to mix Bing Maps and WebGL, similarly to what I've done for an "old" Google Maps experiment.

That previous demo was done on top of a Google maps sample, hence just requiring some small tweaks and improvements. Also, was very low-level and not really practical to adapt to more "real-world" usage, as it required programming the shaders, computing the transformation matrixes, etc.
Thus, I was trying to find a alternative WebGL JS lib that was:
  • Fast
  • Easy to use, albeit still providing some low-level control, namely on primitives drawing
After some research I ended up with two candidates:
IvanK Lib is pretty good (and fast) but Pixi.js takes the cake with tons of functionality and a large community using it.

I'm going to enumerate the various experiments I did, showing a sample page for each.


Recommendation: use Chrome as otherwise it might be painfully slow/non-working.

1.  Create a pixi stage on top of Bing Maps.
http://psousa.net/demos/bingmaps/webgl/pixi/pixi1.html
var mapDiv = map.getRootElement();
stage = new PIXI.Stage();

// create a renderer instance mapping (pun intended) the size of the map.
renderer = PIXI.autoDetectRenderer(
    map.getWidth(), 
    map.getHeight(), 
    {transparent: true});

// add the renderer view element to the DOM, making it sit on top of the map
mapDiv.parentNode.lastChild.appendChild(renderer.view);

renderer.view.style.position = "absolute";
renderer.view.style.top = "0px";
renderer.view.style.left = "0px";

renderer.render(stage);


Yep, nothing visible. Regardless, if you open a DOM inspector you can see a canvas element that was generated on top of the map.


2. Add a sprite to the map.
http://psousa.net/demos/bingmaps/webgl/pixi/pixi2.html
var texture = PIXI.Texture.fromImage("img/bunny.png");
var bunny = new PIXI.Sprite(texture);

// center the sprite anchor point
bunny.anchor.x = 0.5;
bunny.anchor.y = 0.5;

bunny.lat = 40.0;
bunny.lon = -8.5;

var pixelCoordinate = map.tryLocationToPixel(
    new MM.Location(bunny.lat, bunny.lon), 
    MM.PixelReference.control);

bunny.position.x = pixelCoordinate.x;
bunny.position.y = pixelCoordinate.y;

stage.addChild(bunny);
Although the bunny is properly added on top of the map it's not georeferenced. Thus, if the map is moved the bunny stays on the same screen position.


3. Listen to the viewchange event and update the sprite position
http://psousa.net/demos/bingmaps/webgl/pixi/pixi3.html
MM.Events.addHandler(map, 'viewchange', updatePosition);
(...)

function updatePosition(e) {
    var pixelCoordinate = map.tryLocationToPixel(
        new MM.Location(bunny.lat, bunny.lon),
        MM.PixelReference.control);

    bunny.position.x = pixelCoordinate.x;
    bunny.position.y = pixelCoordinate.y;
    renderer.render(stage);
}
4. Do the same thing for 1000 sprites
http://psousa.net/demos/bingmaps/webgl/pixi/pixi4.html
Depending on your machine (and graphics card) should still behave nicely. Regardless, when displaying lots of similar sprites pixi.js supports the concept of SpriteBatch:
The SpriteBatch class is a really fast version of the DisplayObjectContainer built solely for speed, so use when you need a lot of sprites or particles
5. Use SpriteBatch
http://psousa.net/demos/bingmaps/webgl/pixi/pixi5.html
container = new PIXI.SpriteBatch();
stage.addChild(container);

for (var i = 0; i < 1000; i++) {
    var bunny = new PIXI.Sprite(texture);

    // center the sprites anchor point
    bunny.anchor.x = 0.5;
    bunny.anchor.y = 0.5;

    // move the sprite t the center of the screen

    bunny.lat = 40.0 + Math.random() * 20;
    bunny.lon = -8.5 + Math.random() * 50;

    var pixelCoordinate = map.tryLocationToPixel(
        new MM.Location(bunny.lat, bunny.lon), 
        MM.PixelReference.control);

    bunny.position.x = pixelCoordinate.x;
    bunny.position.y = pixelCoordinate.y;

    container.addChild(bunny);

    bunnies.push(bunny);
}
It's really simple to use. Instead of adding the sprites to the stage add them to a SpriteBatch. Now, the problem is that this code is still updating the position of each individual sprite when moving/zooming the map.

6. Scale the SpriteBatch instead of reposition individual sprites
http://psousa.net/demos/bingmaps/webgl/pixi/pixi6.html
function updatePosition(e) {
    if(!e.linear) //zooming animation
    {
        var currentWidth = getCurrentWidth();
        var diff = startWidth / currentWidth;

        container.scale.x = diff;
        container.scale.y = diff;

        var divTopLeft = map.tryLocationToPixel(startPosition, MM.PixelReference.control);

        var x = divTopLeft.x;
        var y = divTopLeft.y;

        container.position.x = x;
        container.position.y = y;

        renderer.render(stage);
    }
}
This sample doesn't update the individual sprites and scales the SpriteBatch as a whole. This provides a good performance impact, although the sprites will look pixelated on higher zoom levels.
An improved solution would be to use this mechanism on panning/zooming, and having different LOD (Level-of-Detail) Sprites, which would be redrawn when the zoom animation finished.

Now, instead of drawing sprites I'm going to show how to draw primitives (in this case rectangles).

7. Draw primitives
http://psousa.net/demos/bingmaps/webgl/pixi/pixi7.html

graphics = new PIXI.Graphics();
var referencePixel = map.tryLocationToPixel({ latitude: 44, longitude: -9.5}, MM.PixelReference.control);

graphics.beginFill(0xFFFFFF);
for(var i=0; i < 20000; i++) {
    graphics.drawRect(referencePixel.x + Math.random()* 1200.0, referencePixel.y + Math.random()*900.0, 2, 2);
}
graphics.endFill();
I'm basically creating 20000 small rectangles using pixi.js. On higher zoom levels precision isn't lost as this is vector data (as opposed to the raster data from the previous examples).

All of this is obviously non-production code with various bugs. Regardless, future looks promising :)

4 comments:

  1. Cool post. My only issue is the browser support for WebGL. Wish older versions of IE supported it. I've been experimenting with d3js and topoJSON recently and have had a lot of success with showing a lot of data on Bing Maps.

    ReplyDelete
  2. Hi Ricky. Well, Pixi does fallback to Canvas (although losing some performance on the way) so should support a couple more versions of IE :)

    I'm curious about those topoJSON experiments. Will you post something about that on your blog?

    ReplyDelete
  3. Thanks for sharing this! Have you tried drawing > 20,000 rects? There seems to be an upper limit.

    ReplyDelete
  4. Chris, apparently so (although I haven't seen that documented anywhere). Regardless, you can always add multiple containers to the main stage, each with less than 20.000 rects.

    ReplyDelete