To finish off our look at developing an HTML5-based 3D viewer for our Apollonian web-service, today’s post integrates the trackball capability of the Three.js library. Many thanks to Jeff Geer for once again pointing me in the right direction on this. :-)
The trackball capability allows you not to worry about manual implementation of 3D navigation inside your viewer: you simply set up some basic parameters to indicate the size of your model and the speed with which you want navigation to occur – as well as the keys for rotate, zoom and pan – and then you just let it fly.
Here’s the updated file for you to try in either Chrome or Firefox (or elsewhere with WebGL enabled), with the A, S and D keys mapped to rotate, zoom and pan, respectively.
Here’s a video of the trackball capability in action:
And here’s the updated source 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, trackball;
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, scene and trackball
renderer =
animateWithWebGL ?
new THREE.WebGLRenderer() :
new THREE.CanvasRenderer();
camera =
new THREE.PerspectiveCamera(
VIEW_ANGLE,
ASPECT,
NEAR,
FAR
);
scene = new THREE.Scene();
trackball =
new THREE.TrackballControls(camera, container);
trackball.rotateSpeed = 1.4;
trackball.zoomSpeed = 2.0;
trackball.panSpeed = 0.5;
trackball.noZoom = false;
trackball.noPan = false;
trackball.staticMoving = true;
trackball.dynamicDampingFactor = 0.3;
trackball.minDistance = 1;
trackball.maxDistance = 100;
trackball.keys = [65, 83, 68]; // [a:rotate, s:zoom, d:pan]
trackball.addEventListener('change', render);
// 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 '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);
trackball.update();
}
function render()
{
renderer.render(scene, camera);
}
</script>
</body>
</html>
I was able to strip out all the code enabling zoom/spin, although I left the piece in that lets you change levels with the number keys. Once again Three.js and WebGL have really impressed me: this is very interesting technology, and really feels comparable with a native, GPU-enabled 3D graphics experience.
Over the next few posts, we’re going to wrap up this series by implementing a Metro-style 3D viewer using WinRT and DirectX. Fun, fun, fun! :-)