Make a Rotatable 3D Product Boxshot with Three.js

Learn how to use the Three.js JavaScript library to create a 3D rotatable product boxshot in the page. No Flash required!

Make a Rotatable 3D Product Boxshot with Three.js

Works best with Chrome, Firefox, or any other WebGL-enabled browser

To say that JavaScript has come a long way in recent years is a bit of an understatement. At the turn of the century we were using it mainly for validating forms and opening annoying pop-up windows. These days, JavaScript can play HTML5 video and audio; generate richly-detailed graphics on the fly; query a mobile device to display its location; and even make a web server.

One area where JavaScript has improved in leaps and bounds is 3D graphics. Ten years ago, the idea of smooth, real-time, realistic 3D graphics running inside a web browser seemed out of this world. Nowadays it's becoming commonplace, thanks to modern browsers supporting both the canvas element and WebGL, a library that adds hardware-accelerated, OpenGL-like 3D graphics capabilities to the browser.

Building on top of these layers, some enterprising folks have started creating JavaScript libraries that make it easy for the average coder — that is, someone without a degree in computer graphics — to create realistic, animated 3D scenes, right in the browser. This is opening up a world of possibilities for online games, interactive videos, web apps, and much more.

Once such library that is really starting to gain traction is Mr. doob's Three.js. This library gives you all you need to create complex 3D scenes in the browser, using nothing but JavaScript.

While Three.js is relatively easy to use, it can still be daunting if you're not familiar with the concepts involved in computer-generated imagery. In this tutorial, you'll get a gentle introduction to Three.js's basic features, and learn how to put them to practical use by creating a rotatable 3D boxshot scene.

The boxshot I've used is for my new jQuery Mobile book (shameless plug), but you can of course use the same code to showcase any other product — as long as it's, well, box-shaped.

What Exactly Is Three.js?

Three.js is a JavaScript library that lets you create and render objects in a 3D scene. It's designed to be lightweight and easy to use.

Three.js can render its 3D scenes using the WebGL library built into many modern browsers, such as Chrome and Firefox. This is the recommended approach, since it uses the computer's GPU (graphics chip) to do the work. This makes rendering much faster, takes the load off the CPU, and also gives you more 3D features.

For browsers that don't yet support WebGL, Three.js can also render scenes directly onto an HTML canvas element, or render using SVG. The canvas approach is generally pretty slow, while the SVG approach is somewhat limited (no bitmap textures, for example).

Three.js can do some pretty awesome things — check out some of the demos that come with the library. It is also a major component of The Wilderness Downtown, an interactive video for the band Arcade Fire, and 3 Dreams of Black (below), an interactive film promoting the album Rome.

3 Dreams of Black screenshot
3 Dreams of Black uses Three.js to create a gorgeous immersive experience right in the browser.

Not only can you create 3D objects from scratch using Three.js and JavaScript, you can also import ready-made objects created using 3D apps like Blender straight into Three.js. This opens up a world of possibilities for interactive 3D scenes and games, all running within the browser.

For this tutorial, though, we'll keep things nice and easy, and create a simple box with some bitmap textures wrapped around it. We'll use WebGL if it's available, and fall back to the canvas renderer for browsers that don't support WebGL but do support canvas: this includes IE9+, Opera, and Safari (although you can optionally enable WebGL support in the latest Safari version).

Ready? Let's start building!

Step 1: Install Three.js

The easiest way to install Three.js is to click the Downloads button over at the Three.js GitHub page. When the popup appears, download the latest package (I used version r42 for this tutorial) and unzip it.

Within the package folder, you'll see a build folder containing the file Three.js. Create a folder called, for example, boxshot somewhere in your local website, and copy the Three.js file to the boxshot folder.

Step 2: Create requestAnimFrame.js

The other library file you need is requestAnimFrame.js. This is a short snippet of JavaScript written by Paul Irish that emulates the window.requestAnimationFrame() method for browsers that don't yet support it.

requestAnimationFrame() is a modern replacement for the setInterval() method, and it's designed specifically for animating stuff. It's better than setInterval() because the browser can optimize the animation to make it smoother, and it can also reduce the animation's frame rate if it's running in a background tab, thereby conserving battery life on laptops and mobile devices. Cool!

So, save the following code as requestAnimFrame.js in your boxshot folder:

/**
 * Provides requestAnimationFrame in a cross browser way.
 * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
 */

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       || 
          window.webkitRequestAnimationFrame || 
          window.mozRequestAnimationFrame    || 
          window.oRequestAnimationFrame      || 
          window.msRequestAnimationFrame     || 
          function(/* function */ callback, /* DOMElement */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

Step 3: Set Things Up

OK, you're now ready to create your basic page for the boxshot, and initialize things. Save the following file as book.html in your boxshot folder:

<!doctype html>
<html>
<head>
<title>A Rotatable 3D Product Boxshot with Three.js</title>
<script type="text/javascript" src="Three.js"></script>
<script type="text/javascript" src="requestAnimFrame.js"></script>
<style>
body { margin: 0; padding: 0; }
</style>

<script type="text/javascript">

window.onload = function() {

  // Set up some variables and add a mousemove handler to the page
  var mouseX = 0;                                               // Mouse X pos relative to window centre
  var mouseY = 0;                                               // Mouse Y pos relative to window centre
  var windowCentreX = window.innerWidth / 2;                    // Window centre (X pos)
  var windowCentreY = window.innerHeight / 2;                   // Window centre (Y pos)
  var WebGLSupported = isWebGLSupported();                      // Check for WebGL support

  document.addEventListener( 'mousemove', function( event ) {

    // Update mouseX and mouseY based on the new mouse X and Y positions
    mouseX = ( event.clientX - windowCentreX );
    mouseY = ( event.clientY - windowCentreY );
  }, false );

}

</script>
</head>
<body></body>
</html>

Here you've created a simple HTML5 page with an empty body element. In the head, you've included the Three.js and requestAnimFrame.js libraries, and started to build a window.onload event handler function that will set up and run the 3D scene. The function starts by setting up some variables that track the mouse position relative to the window centre, and store the (x,y) position of the window centre.

The function also calls isWebGLSupported() to determine if the browser supports WebGL or not, and stores the result in the variable WebGLSupported. You'll write this function later in the tutorial.

Finally, the function binds a handler function to the document's mousemove event. This function simply updates the mouseX and mouseY variables with the new mouse coordinates relative to the window centre.

We'll add more code to this function in the next few steps to build our boxshot scene.

Step 4: Create the Renderer

The next step is to create a Three.js renderer object. The renderer is in charge of painting the entire scene onto a canvas element (in the case of the WebGL and canvas renderers), or an svg element (in the case of the SVG renderer).

Here's the code to create the renderer — add it to the end of the window.onload handler function you created in Step 3, before the function's closing brace:

  // Create the renderer and add it to the page's body element
  var renderer = WebGLSupported ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
  renderer.setSize( window.innerWidth, 600 );
  document.body.appendChild( renderer.domElement );

This code starts by creating either a THREE.WebGLRenderer object if the browser supports WebGL, or a fallback THREE.CanvasRenderer object if it doesn't. It sets the renderer's size to the width of the browser window, and makes it 600 pixels tall. Finally it adds the renderer's canvas element — stored in the domElement property — to the document body so that it appears in the page.

Step 5: Create the Scene and Camera

You're now ready to set up the scene and camera. The scene object holds all the objects and lights that make up the 3D scene, while the camera object specifies how the scene looks through the virtual camera that points "into" the page and views the scene.

Here's the code to create the scene and camera — as before, add it to the end of your window.onload handler:

  // Create the scene to hold the object
  var scene = new THREE.Scene();

  // Create the camera
  var camera = new THREE.Camera(
      35,                       // Field of view
      window.innerWidth / 600,  // Aspect ratio
      .1,                       // Near plane distance
      10000                     // Far plane distance
  );

  // Position the camera
  camera.position.set( -15, 10, 15 );

First the code creates a new THREE.Scene object and stores it in the variable scene. Next it creates a THREE.Camera object and stores it in camera. THREE.Camera's constructor takes a number of parameters that define how the scene looks through the camera:

  • Field of view defines how much of the scene can be viewed through the camera. It's measured in degrees. Large numbers make the scene look farther away (180 degrees makes it infinitely far away). Small numbers make it look closer. 35 degrees is a good compromise.
  • Aspect ratio defines the camera's aspect ratio — the ratio of the width of the camera window to its height. Usually you want this to be the same as the renderer's aspect ratio.
  • The Near and far plane distances define the distance from the camera to the near and far planes. Imagine a pyramid, with the pointy end at the camera and the base off in the distance. This pyramid it then intersected by the two planes, which are parallel to the page. The visible scene is the space inside the pyramid and between the two planes. Any part of the scene that falls outside this space is clipped (not shown). Usually you want to use a very small value for the near plane, and a very large value for the far plane.

    Technically this space is known as a viewing frustum. Now there's something to impress your friends with!

Once we've created our camera, we position it in the scene by calling the set() method of the camera's position property. We place it 15 units to the left of the origin, 10 units above the origin, and 15 units in front of the origin.

Step 6: Add the Lights

Lights allow you to add realism to a scene by creating areas of light and shadow. If you don't add lights then the scene looks rather flat.

Let's add a couple of lights to the scene: a point light — which simulates a single point of light such as a light bulb — and an ambient light, which provides a base level of light to illuminate the whole scene and soften the shadows.

Here's the code — add it to the window.onload handler, after the code you added in Step 5:

  // Add the lights

  var light = new THREE.PointLight( 0xffffff, .4 );
  light.position.set( 10, 10, 10 );
  scene.addLight( light );

  ambientLight = new THREE.AmbientLight( 0xbbbbbb );
  scene.addLight( ambientLight );

First we create our point light as a THREE.PointLight object. We give the light a white colour and an intensity of 0.4, which is fairly low (the default is 1). This stops the light from adding too much glare to the book. We then position the light by calling the set() method of the light's position property. We position it 10 units to the right of the origin, 10 units above the origin, and 10 units in front of the origin. Finally, we add it to the scene by calling scene.addLight(), passing in the light object.

Our ambient light is simpler to set up. We give it a light grey colour so that it doesn't wash out the scene, then we add it to the scene using scene.addLight().

Step 7: Create the Materials

Now comes the part where we make the materials that govern the appearance of the book. In the next step, we'll wrap these materials around a cuboid shape to create the book object.

Here's the code to create the materials — again, add it to the end of the window.onload handler:

  // Create the materials

  var materialClass = WebGLSupported ? THREE.MeshLambertMaterial : THREE.MeshBasicMaterial;
  var darkGrey =  new materialClass( { color: 0x333333 } );
  var bookCover = new materialClass( { color: 0xffffff, map: THREE.ImageUtils.loadTexture( 'master-mobile-web-apps-with-jqm.png' ) } );
  var bookSpine = new materialClass( { color: 0xffffff, map: THREE.ImageUtils.loadTexture( 'master-mobile-web-apps-with-jqm-spine.png' ) } );
  var bookPages = new materialClass( { color: 0xffffff, map: THREE.ImageUtils.loadTexture( 'master-mobile-web-apps-with-jqm-pages.png' ) } );
  var bookPagesTopBottom = new materialClass( { color: 0xffffff, map: THREE.ImageUtils.loadTexture( 'master-mobile-web-apps-with-jqm-pages-topbottom.png' ) } );

  var materials = [
    bookSpine,          // Left side
    bookPages,          // Right side
    bookPagesTopBottom, // Top side
    bookPagesTopBottom, // Bottom side
    bookCover,          // Front side
    darkGrey            // Back side
  ];

First, the code chooses whether to create THREE.MeshLambertMaterial objects or THREE.MeshBasicMaterial objects, depending on whether the browser supports WebGL or not. THREE.MeshLambertMaterial produces a much nicer effect, shading the objects in the scene according to their positions relative to the lights, but it only works with WebGL. Therefore if we're not working with WebGL, we use THREE.MeshBasicMaterial instead. This doesn't respond to the lighting in the scene, making the book look a bit unrealistic, but it's a decent fallback.

Next we create our first material, darkGrey, for the back of the book. This is simply a dark grey colour, represented by the hex value 0x333333. We pass this value as the color argument to the material's constructor.

The next material is the book cover image, bookCover. To create this, we first pass a fallback colour of 0xffffff (white) as the color argument to the constructor, followed by a map argument, which is the texture map to use for the material. We create this texture map by calling THREE.ImageUtils.loadTexture(), passing in the filename of the image we want to use for the map (master-mobile-web-apps-with-jqm.png).

Make sure you put all the texture image files in your boxshot folder so that loadTexture() can find them.

The remaining materials — bookSpine, bookPages, and bookPagesTopBottom — work in much the same way. In each case, we pass in the filename of the image to use for the material's texture map.

Once we've created all our materials, we group them into an array called materials so that we can apply them to our book object in the next step.

Here's how the various texture files look — you'll find all of the image files in the code download if you want to play with them:

The texture images
The textures that we'll wrap around the book object. Clockwise from top left: bookCover, bookSpine, bookPages, and bookPagesTopBottom.

Step 8: Create the Book

Now that we've set up the scene and created the materials, adding the book itself is relatively easy. Here's the code — add it to the end of your window.onload handler as usual:

  // Create the book and add it to the scene
  var book =  new THREE.Mesh( new THREE.CubeGeometry( 7, 10, 1.2, 4, 4, 1, materials ), new THREE.MeshFaceMaterial() );
  scene.addChild( book );

First we create the cuboid shape for our book by creating a THREE.CubeGeometry object. We pass it 7 arguments, as follows:

  • The width, height, and depth of the book (7, 10, and 1.2 units respectively).
  • The number of segments to use across the width, height and depth of the cuboid (4, 4, and 1 respectively). This defines how many smaller shapes make up the overall cuboid. It's mainly important when using the canvas renderer. Too few segments, and the book's texture can look warped. Too many, and the poor computer grinds to a halt!
  • The materials array we created in Step 7. Three.js maps the materials to the six faces of the cuboid in the following order: left, right, top, bottom, front, and back. It also stretches each material so it fills the entire face.

Once we have our THREE.CubeGeometry object, we use it to create a new THREE.Mesh object, book. This is the object that is actually rendered in the scene. We also create a new THREE.MeshFaceMaterial object and use this for the mesh's material. As I understand it, this is essentially a "pass-through" material that lets the underlying materials wrapped around the cuboid show through.

Finally, we add our book mesh to the scene by calling scene.addChild().

Step 9: Start Animating

We've now built our whole scene. All that's left to do is render it! Here's the code to do just that — again, add all these lines of code inside your window.onload handler, at the end:

  // Begin the animation
  animate();


  /*
    Animate a frame
  */

  function animate() {

    // Rotate the book based on the current mouse position
    book.rotation.y = mouseX * 0.005;
    book.rotation.x = mouseY * 0.005;

    // Render the frame
    renderer.render( scene, camera );

    // Keep the animation going
    requestAnimFrame( animate );
  }

Since we want the user to be able to rotate the book, we need to render it not once, but continuously. To that end, we first call a function, animate(), to kick-start the animation process. Within animate(), we rotate the book around the y and x axes based on the current mouse position relative to the window centre. The rotation values are specified in radians ( 2 x π radians = a full turn), so we scale down the values of mouseX and mouseY in order to make the rotation more controllable.

Once we've rotated the book, we call the renderer object's render() method to render the scene, passing in the scene to render and the camera to use (we created these in Step 5). Finally, the function calls requestAnimFrame(), passing in its own function name, so that the function is called again when a new frame is available, and the animation repeats indefinitely.

Step 10: Checking for WebGL Support

Our script is pretty much done, but we still need to write that isWebGLSupported() function to check if the browser supports WebGL. Add this function after your window.onload handler function, before the closing </script> tag:

/*
  Check if the browser supports WebGL
  Adapted from http://doesmybrowsersupportwebgl.com/
*/

function isWebGLSupported() {

  var cvs = document.createElement('canvas');
  var contextNames = ["webgl","experimental-webgl","moz-webgl","webkit-3d"];
  var ctx;

  if ( navigator.userAgent.indexOf("MSIE") >= 0 ) {
    try {
      ctx = WebGLHelper.CreateGLContext(cvs, 'canvas');
    } catch(e) {}
  } else {
    for ( var i = 0; i < contextNames.length; i++ ) {
      try {
        ctx = cvs.getContext(contextNames[i]);
        if ( ctx ) break;
      } catch(e){}
    }
  }

  if ( ctx ) return true;
  return false;
}

Rather than reinventing the wheel, I took the code from Does My Browser Support WebGL? and simplified it somewhat. Essentially, the function creates a canvas element and tries to create a WebGL context in it. If the browser is Internet Explorer then the function checks to see if the browser can create a WebGL context using WebGLHelper.CreateGLContext — this refers to the IEWebGL plugin that people can use to enable WebGL support in IE. For non-IE browsers, the function tries creating various WebGL-related contexts. If a context was created successfully, the function returns true.

The End Result

The texture images

Here's the boxshot again. It's fast (on WebGL browsers at least), it looks great, and — thanks to the joys of Three.js — it was easy to create, too!

As I mentioned at the start of the article, for best results you'll want to use a WebGL-enabled browser, such as Chrome, Firefox, or Safari with WebGL turned on. If you're using a non-WebGL browser then the animation will be a lot jerkier, and the book won't be lit by the lights in the scene. You'll also notice that the mesh lines are overlaid on the book texture, which isn't ideal — I've no idea if it's possible to hide them! Still, it's better than nothing. :)

To see the boxshot on your own computer, you'll need to view book.html via your local web server — for example, http://localhost/boxshot/book.html. If you try to open the book.html file directly in your browser, it won't work. This is because browsers, by default, block JavaScript code from loading other local files for security reasons. This means that the texture images won't load, and you'll see a JavaScript error to that effect.

Summary

In this article you've explored the basics of the awesome Three.js JavaScript 3D engine. You learned how to set up Three.js, and how to create renderers, scenes, cameras, lights, materials, textures, cuboids and meshes. You also saw how to animate 3D scenes using render() and requestAnimationFrame(), and learned how to detect WebGL support in a browser.

There's an awful lot more to Three.js than we've seen here — the best way to learn is to explore the examples that come with the library — but hopefully this tutorial has given you a useful introduction to Three.js and the world of JavaScript 3D rendering.

Have fun!

Follow Elated

Related articles

Responses to this article

6 responses (oldest first):

21-Aug-11 00:15
quite interesting, love the easy interface…

though I have a small question, I saw the box rotates when I move the mouse, can this be limited to just the a section of the screen or are we talking about the whole page.
23-Aug-11 22:50
@Crazyhunk: Sure, you could grab the canvas element that the renderer uses with renderer.domElement, then attach the mousemove handler to the canvas element instead of the document. Then the box should only rotate when the mouse is moved inside the canvas element.
13-Mar-12 12:11
Nice work there, but i found out the script is consuming too much gpu resources, you can check this easly with gpu-z.
To work around this issue, change one couple lines
// animate(); and inside animate function //requestAnimFrame( animate );
to call the script just put setInterval( animate, 1000 / 60 ); before mouse event, gpu resource will drop from 60% to 0% and when you move the object he goes max 10% -15%.

keep going, nice tutorial.
23-Mar-12 01:18
@fred: Interesting - thanks for the suggestion!
17-Jul-13 07:37
Great tutorial. It helped me do this: http://www.miguelrivero.net/mainWeb/images/portafolio/exp/BowieNextDay/heroesCoverCube.html

2 things:
1) Why does the cube shows those wires?
2) Can you extend on how to change the mousemove to a mouseover (in this case the cover div)?

Thanks!
12-Aug-13 23:57
@miguelrivero:

1) Try overdraw: true:

https://github.com/mrdoob/three.js/issues/1044

2) Why would you want to use a mouseover event instead of mousemove? That would only trigger when the user moved their mouse into the div.

Post a response

Want to add a comment, or ask a question about this article? Post a response.

To post responses you need to be a member. Not a member yet? Signing up is free, easy and only takes a minute. Sign up now.

Top of Page