Basic 3D graphics using Three.js

Learn the basics of 3D graphics and rendering using the WebGL-based Three.js library.

Basic cube

Native WebGL graphics programming, with shaders and the necessary mathematics (matrices, quaternions, and so forth), can be complex. To help mitigate such complexity, a number of simplifying libraries have come into existence, including Three.js. We discuss the basics of this library next.

Like OpenGL, Three.js uses a right-hand coordinate system:

In this figure, the computer screen coincides with the xy-plane and is centered at the origin (0, 0, 0). The positive z-axis points out of the screen towards the observer's eyes.

When a Three.js object, such as a sphere, is added to a scene, the object (by default) is added at the origin of the xyz-coordinate system. So, if you were adding a camera object and a sphere object to a scene, they'd both end up at (0, 0, 0) and you'd be looking at the sphere from the inside out. The solution is to move the camera to a reasonable position, such as down the positive z-axis 50 units: camera.position.z = 50

This code example clarifies the discussion:

Example 1

<!DOCTYPE html>

<html>
<head>
  <meta charset="utf-8" />
  <title>Cube</title>
  <style>
    body {
      text-align: center;
    }

    canvas { 
      width: 100%; 
      height: 100%;
      border: 1px solid black;
    }
  </style>
</head>

<body>
  <h1>Liquid Three.js Cube</h1>
  <p>Change the browser's window size.</p>
  <script src="https://rawgithub.com/mrdoob/three.js/master/build/three.js"></script> <!-- Get the latest version of the Three.js library. -->
  <script>
    var scene = new THREE.Scene(); // Create a Three.js scene object.
    var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Define the perspective camera's attributes.

    var renderer = window.WebGLRenderingContext ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer(); // Fallback to canvas renderer, if necessary.
    renderer.setSize(window.innerWidth, window.innerHeight); // Set the size of the WebGL viewport.
    document.body.appendChild(renderer.domElement); // Append the WebGL viewport to the DOM.

    var geometry = new THREE.CubeGeometry(20, 20, 20); // Create a 20 by 20 by 20 cube.
    var material = new THREE.MeshBasicMaterial({ color: 0x0000FF }); // Skin the cube with 100% blue.
    var cube = new THREE.Mesh(geometry, material); // Create a mesh based on the specified geometry (cube) and material (blue skin).
    scene.add(cube); // Add the cube at (0, 0, 0).

    camera.position.z = 50; // Move the camera away from the origin, down the positive z-axis.

    var render = function () {
      cube.rotation.x += 0.01; // Rotate the sphere by a small amount about the x- and y-axes.
      cube.rotation.y += 0.01;

      renderer.render(scene, camera); // Each time we change the position of the cube object, we must re-render it.
      requestAnimationFrame(render); // Call the render() function up to 60 times per second (i.e., up to 60 animation frames per second).
    };

    render(); // Start the rendering of the animation frames.
  </script>
</body>
</html>

While the code comments do a good job of describing what's going on, let's look more closely at five specific areas:

  1. var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    

    The four parameters of PerspectiveCamera are:

    • 75
    • window.innerWidth / window.innerHeight
    • 0.1
    • 1000

    With these parameters in mind, let's look at this figure.

    • The first parameter (75) defines the camera's vertical field of view, in degrees (from the bottom to the top of the view). It's the extent of the observable world that is seen on the screen at any given moment. The horizontal FOV is calculated using the vertical FOV.
    • The second parameter (window.innerWidth / window.innerHeight) defines the camera's aspect ratio. You'll generally want to use the width of the viewport element divided by its height, otherwise you may end up with an image that appears squished.
    • The third parameter (0.1) defines the near camera frustum plane (find "Near" in the figure). In this case, the near frustum plane nearly coincides with the xy-plane (i.e., screen).
    • The last parameter defines (1000) the far camera frustum plane (find "Far" in the figure). In this case, when an object moves past ±1000 units, it's considered outside of the visible Three.js world and is clipped from view.
  2. var renderer = window.WebGLRenderingContext ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
    

    If the user's browser doesn't support WebGL (Internet Explorer 10 and earlier), a canvas-based renderer is used instead (THREE.CanvasRenderer()). In this case, the codebase still works as-is, but is significancy slower with poorer quality graphics.

  3. document.body.appendChild(renderer.domElement);
    

    You can append the Three.js renderer (viewport) element to any reasonable element, such as a div. In this case, we append the Three.js renderer, which is a canvas element (hence the canvas CSS above), to the body element.

  4. var geometry = new THREE.CubeGeometry(20, 20, 20); // Create a 20 by 20 by 20 cube.
    var material = new THREE.MeshBasicMaterial({ color: 0x0000FF }); // Skin the cube with 100% blue.
    var cube = new THREE.Mesh(geometry, material); // Create a mesh based on the specified geometry (cube) and material (blue skin).
    

    In 3D graphics, you typically create a mesh and then apply a material (such as a bitmap texture) to it. As you can see, a mesh (cube) is created using the specified geometry and material.

  5. camera.position.z = 50;
    

    In Three.js, when an object is added to a scene, it is generally added at the origin (0, 0, 0). In this case, we move the automatically added camera object 50 units down the positive z-axis (towards your eyes) so that the cube and camera don't exist in the same "physical" location.

The remaining code in this example (example 1) can be understood by reviewing the associated code comments.

Now let's look at a slightly more complex scenario, a sphere in a light source.

Sphere with light

Using a light source, this code example creates a reflective sphere, textured with a planetary bitmap from NASA:

Example 2

<!DOCTYPE html>

<html>
<head>
  <meta charset="utf-8" />
  <title>Sphere</title>
  <style>
    body {
      text-align: center;
      color: white;
      background-color: black;
    }

    canvas { 
      width: 100%; 
      height: 100%;
    }
  </style>
</head>

<body>
  <h1>Liquid Three.js Sphere</h1>
  <button id="startButton">Start</button>
  <script src="https://rawgithub.com/mrdoob/three.js/master/build/three.js"></script> <!-- Get the latest version of the Three.js library. -->
  <script>
    var bitmap = new Image();
    bitmap.src = 'images/jupiter.jpg'; // Pre-load the bitmap, in conjunction with the Start button, to avoid any potential THREE.ImageUtils.loadTexture async issues.
    bitmap.onerror = function () {
      console.error("Error loading: " + bitmap.src);
    }

    var scene = new THREE.Scene(); // Create a Three.js scene object.
    var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Define the perspective camera's attributes.

    var renderer = window.WebGLRenderingContext ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer(); // Fallback to canvas renderer, if necessary.
    renderer.setSize(window.innerWidth, window.innerHeight); // Set the size of the WebGL viewport.
    document.body.appendChild(renderer.domElement); // Append the WebGL viewport to the DOM.

    // Be aware that a light source is required for MeshPhongMaterial to work:
    var pointLight = new THREE.PointLight(0xFFFFFF); // Set the color of the light source (white).
    pointLight.position.set(100, 100, 250); // Position the light source at (x, y, z).
    scene.add(pointLight); // Add the light source to the scene.

    var texture = THREE.ImageUtils.loadTexture(bitmap.src); // Create texture object based on the given bitmap path.
    var material = new THREE.MeshPhongMaterial({ map: texture }); // Create a material (for the spherical mesh) that reflects light, potentially causing sphere surface shadows.
    var geometry = new THREE.SphereGeometry(50, 64, 64); // Radius size, number of vertical segments, number of horizontal rings.

    var sphere = new THREE.Mesh(geometry, material); // Create a mesh based on the specified geometry (cube) and material (blue skin).
    scene.add(sphere); // Add the sphere at (0, 0, 0).

    camera.position.z = 150; // Move the camera away from the origin, down the positive z-axis.

    var render = function () {
      sphere.rotation.x += 0.002; // Rotate the sphere by a small amount about the x- and y-axes.
      sphere.rotation.y += 0.005;

      renderer.render(scene, camera); // Each time we change the position of the cube object, we must re-render it.
      requestAnimationFrame(render); // Call the render() function up to 60 times per second (i.e., up to 60 animation frames per second).
    };

    document.getElementById('startButton').addEventListener('click', function () {
      render(); // Start the rendering of the animation frames.
    }, false);
  </script>
</body>
</html>

The code comments for example 2 also do a good job of describing what's going, but let's make sure we're clear on these two items:

  1. var bitmap = new Image();
    bitmap.src = 'images/jupiter.jpg';
    

    Just after the page loads, the bitmap image is "pre-loaded" in the time it takes the user to click the Start button. This can help to avoid a potential asynchronous bitmap loading issue with THREE.ImageUtils.loadTexture().

  2. var pointLight = new THREE.PointLight(0xFFFFFF); // Set the color of the light source (white).
    pointLight.position.set(100, 100, 250); // Position the light source at (x, y, z).
    scene.add(pointLight); // Add the light source to the scene.
    

    You need a light source to successfully use THREE.MeshPhongMaterial(). Phong shading gives the planet's bitmap (texture) a reflective/shiny surface:

    var material = new THREE.MeshPhongMaterial({ map: texture });
    

The remaining code in this example (example 2) can be understood by reviewing the associated code comments.

Next, we tackle a WebGL-based Three.js application in The one-body problem.

Three.js Manual

The one-body problem

The two-body problem

The three-body problem

The physics and equations of the two- and three-body problem