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        








« AEC DevBlog & a month to go until DevCamps | Main | Creating a 3D viewer for our Apollonian service using HTML5 – Part 2 »

May 16, 2012

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

After looking at how to bring data from our Apollonian web-service into Unity3D, Android and iOS over the last few weeks, it seemed natural to extend this series to cover HTML. A big thanks to Jeff Geer for suggesting Three.js, which is the HTML5-based framework I ended up adopting for this project.

I like Three.js for a number of reasons: just as jQuery (another library I’ve used in this project, to good effect) attempts to abstract away the messiness inherent in supporting multiple browsers, Three.js does the same for the world of browser-resident 3D.

The other main reason I like Three.js is that it’s open source. I know, I know – I work for a proprietary software vendor… :-) But unless there’s a trusted, proprietary vendor providing platform technology, I really do prefer to have the peace of mind of having access to the framework’s source. And for my adjacent, experimental projects, I tend not to want to spend money on fully-fledged toolkits/frameworks, either, which means I gravitate to open source tools, or at least those that have versions available for free.

In terms of getting started with Three.js, the online tutorial deals with creating a simple scene with a sphere in it, so that was just perfect. :-)

The coding was reasonably straightforward: the main issue – on all fronts – was to enable cross-domain scripting of our web-service. On the server side, we had done so using a crossdomain.xml file when we first needed to call it from Unity, but we needed to go one step further, this time: we had to modify the Web.config file of our ASP.NET project and redeploy the project to Windows Azure. Not a big deal, but it needed doing.

That addressed the issue of calling our web-service from most browsers, but then some additional work was needed for IE9. While it worked well enough from a local HTML file, we had to implement a special ajaxTransport to make it work (another one was suggested, in this bug report against jQuery, but even resolving this obscure issue, I couldn’t get that one to work.. so I took the previously suggested .js file, minified it, and posted it as xdr.js to the blog’s supporting folder).

Here’s the HTML code for our basic viewer application:

<!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 = false;

      var container, root = null;

      var camera, scene, renderer;

 

      init();

      animate();

 

      function init()

      {

        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);

 

        jQuery.support.cors = true;

 

        // We'll just load level 8 - good detail, but also quick

 

        $.ajax(

          {

            url: 'http://apollonian.cloudapp.net/api/spheres/1/8',

            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);

            }

          }

        );

      }

 

      function animate()

      {

        // Animation really only looks good with WebGL rendering

 

        if (animateWithWebGL)

        {

          requestAnimationFrame(animate);

 

          render();

        }

      }

 

      function render()

      {

        var timer = Date.now() * 0.0005;

 

        camera.position.x = Math.cos(timer) * 4;

        camera.position.z = Math.sin(timer) * 4;

        camera.lookAt(scene.position);

 

        renderer.render(scene, camera);

      }     

    </script>

  </body>

</html>

If you want to give it a try, here’s the “canvas” version of the viewer, which I’ve chosen not to have animated. If you’re using Google Chrome or Mozilla Firefox, you may want to try the WebGL version of the viewer, which has some hard-coded rotation. Apparently this can work in other browsers, too, but I haven’t managed to see that, myself.

The only difference between the two HTML files is the value of the animateWithWebGL flag, which obviously causes a slightly different code-path to be followed in a few places.

To see them both in action on my system, here’s a quick video:

Unable to display content. Adobe Flash is required.

To compare how the canvas-based viewer looks on different OS/browser combinations, here are those I’ve tested. The results are mostly pretty decent, although Safari on OS X had something very strange going on (whereas it worked fine in the iOS emulator on the same system).

Apollonian Viewer web-page in Chrome on MacApollonian Viewer web-page in Firefox on MacApollonian Viewer web-page in Safari on Mac

Apollonian Viewer web-page in IE9 on Windows 7Apollonian Viewer web-page in Firefox on Windows 7Apollonian Viewer web-page in Safari on iPad

In the next post, we’re going to make a few enhancements to this HTML5-based viewer: we’ll add support for keyboard events – to enable zoom, rotate and changing levels, in particular – as well as implementing feature detection to use WebGL automatically when available or to default to the use of the canvas renderer, otherwise.

blog comments powered by Disqus

Feed/Share

10 Random Posts