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:
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).
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.