September 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        








« Creating a 3D viewer for our Apollonian service using HTML5 – Part 1 | Main | Manufacturing DevBlog »

May 18, 2012

Creating a 3D viewer for our Apollonian service using HTML5 – Part 2

In the last post, we created another, basic 3D viewer for the data from our Apollonian web-service – this time using HTML5 via Three.js. In this post, we’ll extend the code to listen for keyboard events and manipulate the model according to user-input, as well as enabling feature detection of WebGL (allowing the same implementation to call into the canvas rendering code when WebGL isn’t present).

We’re introducing keyboard-based commands to enable zoom, rotate (meaning spin, although not continuously) and the change of levels:

  • +
    • Zoom in
  • -
    • Zoom out
  • , or %
    • Rotate left
  • . or /
    • Rotate right
  • & or a
    • Rotate up
  • ( or z
    • Rotate down
  • 09
    • Change level (0 == 10)

You’ll see some strange choices for some of the rotate keys, above: on browsers other than Chrome, the keypress event gets called with symbols representing the arrow keys (left: ‘%’ right: ‘/’ up: ‘&’ down: ‘(‘). But because of an issue with Chrome that means its doesn’t receive the keypress event for arrow keys, we’re also checking for other characters, too (left: ‘,’ right ‘.’ up: ‘a’ down: ‘z”).

For changing levels: we might also have confirmed the change of level with the user via a message-box, but in the end I felt it wasn’t too disruptive to just do it.

Before we see the code, here’s a quick demo video and a link to the page:

[Errata: I mistakenly said “level 5” in the below video instead of “level 10”, and I also should have reinforced that the view was really using WebGL rather than basic HTML5 Canvas.]

Unable to display content. Adobe Flash is required.

Here’s the updated HTML code:

<!doctype html>

<html>

  <head>

    <title>Apollonian Viewer</title>

  </head>

  <body>

    <script

      type="text/javascript"

      src="http://code.jquery.com/jquery-1.7.1.js">

    </script>

    <script type="text/javascript" src="js/xdr.js"></script>

    <script type="text/javascript" src="js/Three.js"></script>

    <script type="text/javascript">

      var animateWithWebGL;

      var container, root = null;

      var camera, scene, renderer;

      var zoomScale, xRotation, yRotation;

      var changingLevel = false;

 

      init();

      animate();

 

      // Feature test for WebGL

 

      function hasWebGL()

      {

        try

        {

          var canvas = document.createElement('canvas');

          var ret =

            !!(window.WebGLRenderingContext &&

                (canvas.getContext('webgl') ||

                 canvas.getContext('experimental-webgl'))

              );

          return ret;

        }

        catch(e)

        {

          return false;

        };

      }

 

      function init()

      {

        zoomScale = 1;

        xRotation = 0;

        yRotation = 0;

        var rotInc = 0.05;

 

        animateWithWebGL = hasWebGL();

 

        container = document.createElement('div');

        container.style.background = "#000000";

        document.body.appendChild(container);

 

        // Set the scene size (slightly smaller than the

        // inner screen size, to avoid scrollbars)

 

        var WIDTH = window.innerWidth - 25,

            HEIGHT = window.innerHeight - 25;

 

        // Set some camera attributes

 

        var VIEW_ANGLE = 45,

            ASPECT = WIDTH / HEIGHT,

            NEAR = 0.1,

            FAR = 200;

 

        // Create the renderer, camera and scene

 

        renderer =

          animateWithWebGL ?

            new THREE.WebGLRenderer() :

            new THREE.CanvasRenderer();

        camera =

          new THREE.PerspectiveCamera(

            VIEW_ANGLE,

            ASPECT,

            NEAR,

            FAR

          );

        scene = new THREE.Scene();

 

        // The camera starts at 0,0,0 so pull it back

 

        camera.position.z = 4;

 

        // Create a point light

 

        var pointLight = new THREE.PointLight(0xFFFFFF);

 

        // Set its position

 

        pointLight.position.x = 2;

        pointLight.position.y = 10;

        pointLight.position.z = 26;

 

        // Add to the scene

 

        scene.add(pointLight);

 

        // And the camera

 

        scene.add(camera);

 

        // Start the renderer

 

        renderer.setSize(WIDTH, HEIGHT);

 

        // Attach the renderer-supplied DOM element

 

        container.appendChild(renderer.domElement);

 

        $(document).keypress(

          function (event)

          {

            // On Firefox we need event.which rather than keyCode

 

            var code = event.keyCode ? event.keyCode : event.which;

            switch (String.fromCharCode(code))

            {

              case '-': // Zoom out

                zoomScale -= 0.1;

                break;

              case '+': // Zoom in

                zoomScale += 0.1;

                break;

              case '=': // Reset view

                zoomScale = 1;

                root.rotation.x = 0;

                root.rotation.y = 0;

                break;

              case '\'': // Rotate right

              case '.':

                yRotation = rotInc;

                break;

              case '%': // Rotate left

              case ',':

                yRotation = -rotInc;

                break;

              case '&': // Rotate up

              case 'a':

                xRotation = -rotInc;

                break;

              case '(': // Rotate down

              case 'z':

                xRotation = rotInc;

                break;

              case '0':

              case '1':

              case '2':

              case '3':

              case '4':

              case '5':

              case '6':

              case '7':

              case '8':

              case '9':

                var level = code - '0'.charCodeAt();

                populateWithLevel(level == 0 ? 10 : level);

                break;

            }

          }

        );

        populateWithLevel(10);       

      }

 

      function populateWithLevel(level)

      {

        // Make sure we're not already changing levels

 

        if (changingLevel)

          return;

 

        changingLevel = true;

 

        // If we already have a level loaded, remove the

        // root from the scene and delete it

 

        if (root != null)

        {

          scene.remove(root);

          delete root;

        }

 

        // Make sure CORS is enabled

 

        jQuery.support.cors = true;

 

        // Call our web-service with the appropriate level

 

        $.ajax(

          {

            url:

              'http://apollonian.cloudapp.net/api/spheres/1/' +

              level,

            crossDomain: true,

            data: {},

            dataType: "json",

            error: function(err)

            {

              alert(err.statusText);

            },

            success: function(data)

            {

              // Create the spheres' materials

 

              var materials =

                [

                  new THREE.MeshLambertMaterial({ color: 0x000000 }),

                  new THREE.MeshLambertMaterial({ color: 0xFF0000 }),

                  new THREE.MeshLambertMaterial({ color: 0xFFFF00 }),

                  new THREE.MeshLambertMaterial({ color: 0x00FF00 }),

                  new THREE.MeshLambertMaterial({ color: 0x00FFFF }),

                  new THREE.MeshLambertMaterial({ color: 0x0000FF }),

                  new THREE.MeshLambertMaterial({ color: 0xFF00FF }),

                  new THREE.MeshLambertMaterial({ color: 0xA9A9A9 }),

                  new THREE.MeshLambertMaterial({ color: 0x808080 }),

                  new THREE.MeshLambertMaterial({ color: 0xD3D3D3 }),

                  new THREE.MeshLambertMaterial({ color: 0xFFFFFF }),

                  new THREE.MeshLambertMaterial({ color: 0xFFFFFF })

                ];

 

              // Set up the sphere vars

 

              var rootRad = 0.01, segs = 9, rings = 9;

 

              // Create our root object

 

              var sphereGeom =

                new THREE.SphereGeometry(rootRad, segs, rings);

 

              // Create the mesh from the geometry

 

              root =

                 new THREE.Mesh(sphereGeom, materials[0]);

 

              scene.add(root);

 

              // Process each sphere, adding it to the scene

 

              $.each(

                data,

                function (i, item)

                {

                  // Get shortcuts to our JSON data

 

                  var x = item.X, y = item.Y, z = item.Z,

                      rad = item.R, level = item.L;

 

                  var length = Math.sqrt(x * x + y * y + z * z);

 

                  // Only add spheres near the edge of the outer one

                  // (and only the front half if not animating)

 

                  if (

                    length + rad > 0.99 &&

                      (animateWithWebGL || z > 0)

                  )

                  {

                    // Create the mesh from the geometry

 

                    var sphere =

                      new THREE.Mesh(sphereGeom, materials[level]);

 

                    sphere.position.x = x;

                    sphere.position.y = y;

                    sphere.position.z = z;

                    var scaledRad = rad / rootRad;

                    sphere.scale.x = scaledRad;

                    sphere.scale.y = scaledRad;

                    sphere.scale.z = scaledRad;

 

                    root.add(sphere);

                  }

                }

              );

 

              // Draw!

 

              renderer.render(scene, camera);

 

              changingLevel = false;

            }

          }

        );

      }

 

      function animate()

      {

        requestAnimationFrame(animate);

 

        render();

      }

 

      function render()

      {

        if (root != null)

        {

          // Apply rotations around X and/or Y

 

          if (xRotation != 0)

          {

            root.rotation.x += xRotation;

            xRotation = 0;

          }

          if (yRotation != 0)

          {

            root.rotation.y += yRotation;

            yRotation = 0;

          }

        }

 

        // Apply the zoom

 

        camera.position.z = 4 / zoomScale;

 

        camera.lookAt(scene.position);

 

        renderer.render(scene, camera);

      }     

    </script>

  </body>

</html>

A few things to note:

  • The hasWebGL() function returns true if the browser supports the use of WebGL. We use the result of this function to determine whether to create and use a WebGL or a Canvas renderer.
  • We use jQuery to assign a handler to the document’s keypress() event.
    • We need to check event.keyCode in most browsers but event.which on Firefox.
  • When changing levels we set a flag: if we kick off multiple level changes – very easy to do with the longer-loading levels – then we run the risk of stomping on our geometry list… safest to protect it with a rudimentary critical section.

That’s about all there is to it. The application itself works surprisingly well on WebGL-enabled browsers. If I use it for long enough, I can usually crash the page in Chrome (as you’ll see from the demo video), but in Firefox the page is rock solid (which leads me to be believe it’s a browser issue rather than a problem with the code).

Since publishing the last post (and this one was already pretty well baked, too), Jeff Geer has pointed me at the trackball capability in Three.js, which allows you to more simply create a user-interface to control 3D orientation of your model. In the next post we’ll take a look at integrating that – probably alongside some of the existing keypress-enabled level changing – to see what that provides.

blog comments powered by Disqus

Feed/Share

10 Random Posts