Home  >  

Creating an Interactive 3D Globe

Author photo
February 4, 2009 | | Comments (21)
AddThis Social Bookmark Button
Here's a fun post that I've wanted to do for a while, but just haven't had the time until recently. I'm very interested in working with geographic data, 3D, and data visualization. This post is the first of what I hope to be several, where I will combine these interests and create an interactive 3D globe that you can recreate for your own purposes.

First, Let's take a look at what is being built. It is a fully interactive globe, where you can click and drag, zoom in, or zoom out.

(Give it time to load, i have a big texture embedded. The entire file size is 2 megs.)



I know some browsers have odd mouse-scroll behaviors with Flash, so zooming with the mouse wheel may not work well if you're on a mac. If that's the case for you, either use the +/- keys or Launch globe in a new window.

Now, let's examine how it all works.

The full code is located further below, but first we'll examine it one piece at a time. The first step in this was building the 3D sphere and applying a texture, without any interactivity. I used Papervision3D to create a sphere instance. I rotated the sphere on the Y axis, so that north points up, and then added it to a DisplayObject3D container. All of the manipulations that occur in this example occur within the container. I did this for 2 reasons: 1) So that when the rotation in the x, y, and z directions is zero, north faces up. 2) In the future, if we add other 3D objects with respect to the earth sphere (inside of the container), rotating the container will rotate everything together.

var bmp:Bitmap = new textureImage() as Bitmap;
var bmpMaterial:BitmapMaterial = new BitmapMaterial( bmp.bitmapData );

sphere = new Sphere(bmpMaterial, 600, 64, 64);

sphere.alpha = .5
sphere.rotationX = 0;
sphere.rotationY = 180;
sphere.rotationZ = 0;

container = new DisplayObject3D();
container.addChild( sphere, "sphere" );

scene.addChild(container);
singleRender();
The texture that I used as the surface of the sphere is a Blue Marble image, which is a topographical view of the Earth, provided by NASA’s Earth Observatory. It is a Mercator projection of a topographical view of the Earth. I don't know the licensing details, but be sure to scour NASA's site to make sure you are legally allowed to use it.

The next thing I wanted to do was add interactivity and be able to click and drag to rotate the sphere. It's not nearly as simple as rotating around the x and y axes as you drag. If you only rotate around the x and y axes, eventually you will get to a point where your click actions drag in the inverse direction or a perpindiuclar direction based on what angle of the container/sphere you are currently viewing. It gets really complex and confusing very quickly. After a lot of scouring the internet reading about three dimensional rotations, I came upon some great resources from http://blog.zupko.info/, which introduced the concept of Quaternions. I then used as a search term to find Free Rotation of a Sphere Using Quaternions on the Papervision blog (of all locations).

Initially, I was calling the startRendering() function in the constructor. This starts a rendering cycle which essentially renders the scene on every frame event. This can take up a large number of CPU cycles and system resources. Since this 3D scene is only changing when interacting with it, I changed this so that the render cycle only executes while dragging. the scene.
private function onMouseDown( event : Event ) : void
    {
       Tweener.removeAllTweens();
       this.startRendering();
       previousMousePoint = new Point(viewport.containerSprite.mouseX, viewport.containerSprite.mouseY);
       this.stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
       this.stage.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
     }
    
    private function onMouseUp( event : Event ) : void
    {
       this.stopRendering();
       this.stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
       this.stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp );
     }
    
    private function onMouseMove( event : Event ) : void
    {  
       var currentMousePoint:Point = new Point(viewport.containerSprite.mouseX, viewport.containerSprite.mouseY);
  
       var difference:Point = currentMousePoint.subtract(previousMousePoint);
        var vector:Number3D = new Number3D(difference.x, difference.y, 0);
  
       var rotationAxis:Number3D = Number3D.cross(vector, FORWARD);
       rotationAxis.normalize();
  
       var distance:Number = Point.distance(currentMousePoint, previousMousePoint);
       var rotationMatrix:Matrix3D = Matrix3D.rotationMatrix(rotationAxis.x, -rotationAxis.y, rotationAxis.z, distance/(600*Math.pow(container.scale, 5)));
  
       container.transform.calculateMultiply3x3(rotationMatrix, container.transform);
       
       //this line used to apply transform to actual rotation values, so that if you change scale, the changes are persisted
       container.copyTransform(container);
       
       previousMousePoint = currentMousePoint
       
       trace( container.rotationX, container.rotationY, container.rotationZ, container.scale ); 
     }
The next logical step was to add zoom capabilities based on the + and - keys, and on the mouse wheel. When zooming, I was originally just changing the scale of the container object, without any animation or easing. This was very abrupt, and did not lend to a good experience, so I used the Tweener library to add easing and animation to the zoom action. I added getter/setter methods to be used in the Tweener easing. The setter function (shown below) changes the target scale, and calls the singleRender() function, which redraws the 3D scene. Similar to the technique used to only render when dragging, this technique only redraws the scene when the scale actually changes, which minimizes CPU utilization.

The large file size of the end result (approximately 2 megs) is because the texture used is a very large image. It's a jpg, which is 5400x2700 pixles. I'm using the full size image so that there is little distortion as you zoom into the image. However, there is still pixellation when you zoom close. I'm hoping to find some time to fix this, which will require even higher resolution source images, and progressively loaded textures (perhaps a tile map) which would greatly enhance the experience, but that won't make it in for this blog post.
    
    private function onKeyDown( event : KeyboardEvent ) : void
    {
       trace( event.keyCode );
       switch ( event.keyCode ) 
       {
          case 187: // +
          case 107: // +
            zoom( 3 );
            break;    
          case 189: // -
          case 109: // -
            zoom( -3 );
            break;
          case 82: // r (reset)  
            resetView();
            break;  
        }
     }
    
    private function onMouseWheel( event : MouseEvent ) : void
    {
       zoom( event.delta );
     }
    
    private function zoom( delta : Number ) : void
    {
       targetScale = targetScale + (delta * .01);
       targetScale = Math.max( targetScale, .5  );
       targetScale = Math.min( targetScale, 1.6 );
       Tweener.addTween( this, {sceneScale:targetScale, time:1, transition:"easeOutQuart"} )
     }
    
    public function set sceneScale( value : Number ) : void
    {
       container.scale = value;
       singleRender();
     }
    
    public function get sceneScale() : Number
    {
       return container.scale;
     }
   
I finally added the resetView() function to revert the 3D scene to its original state if you so choose. It reuses the technique used to Tween the zoom to animate the Globe back to a defulat view where the US is centered.

Put it all together, and you get the interactive globe shown above. The full source is below. You can create one for yourself by copying this code, finding a suitable surface image, and downloading the Papervision and Tweener swc files to include in the Actionscript project.

Full Source Code:
package 
{
   import caurina.transitions.Tweener;
   
   import flash.display.Bitmap;
   import flash.events.Event;
   import flash.events.KeyboardEvent;
   import flash.events.MouseEvent;
   import flash.filters.DropShadowFilter;
   import flash.geom.Point;
   import flash.text.TextField;
   
   import org.papervision3d.core.math.Matrix3D;
   import org.papervision3d.core.math.Number3D;
   import org.papervision3d.materials.BitmapMaterial;
   import org.papervision3d.objects.DisplayObject3D;
   import org.papervision3d.objects.primitives.Sphere;
   import org.papervision3d.view.BasicView;
   
   [SWF(backgroundColor = "0xFFFFFF", frameRate="30")]
   public class StaticPapervisionGlobe extends BasicView
   {
      [Embed(source="assets/blueMarble.jpg")]
      private var textureImage:Class;
      private var sphere:Sphere;
      private var container : DisplayObject3D;
      
      private var previousMousePoint : Point = new Point();
      private var targetScale : Number = 1;
  
       private static const FORWARD:Number3D = new Number3D(0, 0, 1);
  
      public function StaticPapervisionGlobe()
      {
         super();
   
         var bmp:Bitmap = new textureImage() as Bitmap;
         var bmpMaterial:BitmapMaterial = new BitmapMaterial( bmp.bitmapData );
         
         sphere = new Sphere(bmpMaterial, 600, 64, 64);
         
         sphere.alpha = .5
         sphere.rotationX = 0;
         sphere.rotationY = 180;
         sphere.rotationZ = 0;
         
         container = new DisplayObject3D();
         container.addChild( sphere, "sphere" );
         
         scene.addChild(container);
         singleRender();
         
         this.stage.doubleClickEnabled = true;
         this.stage.addEventListener( MouseEvent.DOUBLE_CLICK, onDoubleClick );
         this.stage.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
         this.stage.addEventListener( MouseEvent.MOUSE_WHEEL, onMouseWheel );
         this.stage.addEventListener( KeyboardEvent.KEY_DOWN, onKeyDown );
         
         var text : TextField = new TextField();
         text.width = 200;
         text.text = "Use the mouse to drag/pan\nUse the mouse wheel to zoom\nR - reset to default view\n+/- to zoom in/out";
         this.stage.addChild( text );
         
         resetView();
       }
      
      private function onKeyDown( event : KeyboardEvent ) : void
      {
         trace( event.keyCode );
         switch ( event.keyCode ) 
         {
            case 187: // +
            case 107: // +
              zoom( 3 );
              break;    
            case 189: // -
            case 109: // -
              zoom( -3 );
              break;
            case 82: // r (reset)  
              resetView();
              break;  
          }
       }
      
      private function onMouseWheel( event : MouseEvent ) : void
      {
         zoom( event.delta );
       }
      
      private function onDoubleClick( event : MouseEvent ) : void
      {
         zoom( 10 );
       }
      
      private function onMouseDown( event : Event ) : void
      {
         Tweener.removeAllTweens();
         this.startRendering();
         previousMousePoint = new Point(viewport.containerSprite.mouseX, viewport.containerSprite.mouseY);
         this.stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
         this.stage.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
       }
      
      private function onMouseUp( event : Event ) : void
      {
         this.stopRendering();
         this.stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
         this.stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp );
       }
      
      private function onMouseMove( event : Event ) : void
      {  
         var currentMousePoint:Point = new Point(viewport.containerSprite.mouseX, viewport.containerSprite.mouseY);
    
         var difference:Point = currentMousePoint.subtract(previousMousePoint);
          var vector:Number3D = new Number3D(difference.x, difference.y, 0);
    
         var rotationAxis:Number3D = Number3D.cross(vector, FORWARD);
         rotationAxis.normalize();
    
         var distance:Number = Point.distance(currentMousePoint, previousMousePoint);
         var rotationMatrix:Matrix3D = Matrix3D.rotationMatrix(rotationAxis.x, -rotationAxis.y, rotationAxis.z, distance/(600*Math.pow(container.scale, 5)));
    
         container.transform.calculateMultiply3x3(rotationMatrix, container.transform);
         
         //this line used to apply transform to actual rotation values, so that if you change scale, the changes are persisted
         container.copyTransform(container);
         
         previousMousePoint = currentMousePoint
         
         trace( container.rotationX, container.rotationY, container.rotationZ, container.scale ); 
       }
      
      private function zoom( delta : Number ) : void
      {
         targetScale = targetScale + (delta * .01);
         targetScale = Math.max( targetScale, .5  );
         targetScale = Math.min( targetScale, 1.6 );
         Tweener.addTween( this, {sceneScale:targetScale, time:1, transition:"easeOutQuart"} )
       }
      
      public function resetView() : void
      {
         Tweener.addTween( container, {time:3, rotationX:-45, rotationY:0, rotationZ:0, transition:"easeOutQuart"} )
         Tweener.addTween( this, {sceneScale:1, time:3, transition:"easeOutQuart"} )
       }
      
      public function set sceneScale( value : Number ) : void
      {
         container.scale = value;
         singleRender();
       }
      
      public function get sceneScale() : Number
      {
         return container.scale;
       }
      
    }
}


Related Links:
Launch globe in a new window
NASA Blue Marble
Free Rotation of a Sphere Using Quaternions
Papervision 3d
Tweener

___________________________________
Andrew Trice
Principal Architect
Cynergy Systems
http://www.cynergysystems.com


Read more from Andrew Trice. Andrew Trice's Atom feed

Comments

21 Comments

marc said:

Zooming not working here with keyboard! (MacBook Pro, FireFox3). I'm clicking the movie to make it get the focus, but no way...

Scroll wheel not working of course... (never will understand this issue for Flash and Mac).

Great article a part from this :-)


Marc Baiges

Andrew Trice said:

Thanks Marc, I figured that the keys would work to get around the scroll issues. I don't have a mac, so I haven't tested it on one. BTW, which +/- keys did you use? The ones next to the backspace key, or the ones on the right side of a full-size keyboard? On mine, the ones next to backspace/delete seem to work fine, but not the ones next to the number pad. I'll have to update the code to fix this.

Andrew Trice said:

OK, i fixed it to support both sets of +/- keys, and updated the inline source code accordingly. Anyone on a mac, please let me know if those keys work now. Thanks!

(be sure that the flash application has focus, or else keyboard events will definitely be ignored)

Douglas Knudsen said:

nice post! And Quaternions are mentioned! For info on when Hamilton came up with the idea of Quaternions, check out his wiki page: http://en.wikipedia.org/wiki/William_Rowan_Hamilton One of those cool math geek stories that sticks in my mind.

Mapquest has a great example of this globe too that has a bit of acceleration in it.

DK

Matt Giger said:

Nice effort! Technically the map is in the Plate Carrée projection, not Mercator. If I may suggest, you should alter the camera focal length and positioning so you get less of a fisheye effect.

Andrew Trice said:

Thanks for the feedback Doug & Matt. I'm not a cartography expert, so thanks for the correction on the map projection. If anyone wants to know more about Plate Carrée, check it out here: http://en.wikipedia.org/wiki/Equirectangular_projection

Andrew Trice said:

If anyone's interested in the mapquest globe that Douglas mentioned, its online at: http://globe.mapquest.com/

Brent said:

Very nice, thanks for the post. I am working on a project right now where one of the requirements is to use a 3D Globe as the initial navigation control for the site.

This is exactly what I am after, except that I also need the user to be able to click on individual continents. I haven't downloaded the code yet, but am doing so now, and will attempt it. Do you have any pointers on how to get started with adding that type of functionality to it?

Matt Giger said:

Shameless plug: I've been working on this exact problem for about 2 years now and will soon to release a Javascript and Flex API for my Flash/AIR based virtual globe at http://www.earthbrowser.com

It reads KML and Shapefiles and can be used to create customized interfaces for almost any kind of geospatial data.

Idan said:

Hey Andrew,

Great post! Thanks for sharing! I am getting this error compiling it tho:

TypeError: Error #1007: Instantiation attempted on a non-constructor.
at StaticPapervisionGlobe()

How do I fix this? Thanks in advanced.

Andrew Trice said:

Make sure that the file you are trying to compile is named StaticPapervisionGlobe.as


Alec Eriksson said:

Great example and thanks for all the effort putting this together!

I've been tinkering with it lately but I'm having one problem... When I zoom (scroll or keyboard) it resets the rotation of the sphere, seemingly at random. Any thoughts? Your code is intact save for a few tweaks here and there (probably just enough to break it!) but I can't get it to work quite right.

Thanks again!

Alec Eriksson said:

Update: It seems as those through process of elimination, there's something inconsistent about the way this line is being handled...

container.copyTransform(container);

When I comment it out, obviously a zoom results in the user-defined rotation going away, but at least it always goes away the same. When I turn it back on I get the flaky results where zooming results in it quickly screwing up the transform and then finishing the tweened zoom.

Andrew Trice said:

Alec,
That line is intentional so that the rotation transformation gets applied to the sphere's public rotation properties. Without it, the rotation and scale do not work properly. If you are changing other properties, then there's likely a conflict somewhere else. What other properties are you changing?

-Andy

Alec Eriksson said:

Andy,

Thanks for the response! I've adapted your script to one of my own that pulls the model from a DAE file. When I tried to wholesale copy/paste your code (just to get things running from a fresh confirmed-working point) I couldn't get it to run in the current Papervision. I'm new so I commensed with hacking.

If you are feeling brave you can look at my script at the link below. You'll notice that weirdness happens when you rotate the moon and then try to zoom.

www.mphasemedia.com/extranet/alec/moonglobe.zip

Thanks again!

Alec

Alec Eriksson said:

I gave up today and decided to paste my specific DAE related lines of code into your code, Andy. However I'm getting the same Instantiation error Idan, and my actionscript file is named StaticPapervisionGlobe.as properly.

Help! Sorry for being high-maintenance!

Andrew Trice said:

Make sure that the file is in the source root, has the appropriate package name, is named "StaticPapervisionGlobe.as", and also make sure the constructor is:

public funciton StaticPapervisionGlobe() {

Also make sure you have the Tweener and Papervision libraries. I used the swc for both of them. Everything should compile.

Papervision 3D version: 2.0.869 (swc)
Tweener version: 1.31.74 for AS3 (swc)

Alec Eriksson said:

Andy,

Just found out that the EMBED in your code doesn't work with Flash CS3 because it lacks the Flex compiler. When I changed that line of code (I created an empty bitmap just to test it) it started working, albeit without a texture.

Andrew Trice said:

Thanks! Good to know.

lokesh said:

HI sir.
can u please tell that
which software you used for creating this (flash or flex)

brandon said:

zoom (+/-) works in mac

Leave a comment


Tag Cloud

iPad

What's your take on the iPad? (Putting aside the Flash/iPad flame war)

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.