How to render a cube using Three.js

How to render a cube using Three.js

Before diving into the world of 3D graphics on the web, you need to set up a stage where everything will come to life. This stage is more than just a canvas; it’s a combination of tools, libraries, and the underlying structure that will support all the visual magic you’re about to create.

The most popular library for this purpose is Three.js. It abstracts away many of the complexities of WebGL, allowing even those who aren’t graphics gurus to start creating stunning visuals. To get started, you’ll need to include the Three.js library in your project. Here’s how you can do that:

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

Once you have the library included, you can create a basic scene. A scene is where you’ll place all your objects, lights, and cameras. Here’s a simple example of how to set up a scene:

const scene = new THREE.Scene();

Next, you need a camera to view your scene. The camera determines what part of the scene is visible. For a basic setup, you can use a PerspectiveCamera, which simulates the way the human eye sees. Here’s how to create one:

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

Now that you have your scene and camera, it’s time to set up a renderer. The renderer is what actually draws everything on the screen. You’ll use the WebGLRenderer, which makes use of the GPU for rendering. Here’s a quick way to set it up:

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

Now you have a stage ready for your actors. But wait-there’s more to this setup than just these elements. You need to consider how everything interacts. For instance, you’ll probably want to respond to window resizing so that your canvas adjusts accordingly. Here’s how to handle that:

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

With your stage set, you can now think about what you’ll put on it. But before we get to that, let’s take a moment to appreciate the fundamental relationship between the scene, camera, and renderer. Each plays a vital role in the rendering pipeline, and understanding this is crucial for creating more complex scenes.

The holy trinity of scene, camera, and renderer

Think of this trinity as the absolute minimum required to make a movie. The scene is your film set. It’s the entire universe where your story takes place, containing all the actors (meshes), props (more meshes), and lighting. You can add things to it and remove things from it, but it’s fundamentally just a big, dumb container. It doesn’t do anything on its own. It just holds stuff in a data structure called a scene graph, which is basically just a tree of objects.

The camera is, well, the camera. It’s your viewpoint into this universe. You point it at the part of the set you want to film. The parameters you passed to the PerspectiveCamera are critical, so let’s break them down, because if you get these wrong, you’ll either see nothing at all or a horribly distorted mess that looks like it came from a funhouse mirror.

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

The first argument, 75, is the vertical field of view (FOV) in degrees. Think of it as the lens on your camera. A small number is like a telephoto lens, zooming in on a small area. A large number is like a wide-angle lens, capturing more of the scene but introducing distortion at the edges. A value between 60 and 90 is usually a good starting point for a natural look.

The second argument, window.innerWidth / window.innerHeight, is the aspect ratio. This is non-negotiable. If the aspect ratio of your camera doesn’t match the aspect ratio of your output canvas, your perfect spheres will be rendered as ugly ellipsoids. Everything gets stretched or squashed. This is precisely why we have that resize event listener: to keep the camera’s aspect ratio in sync with the canvas’s dimensions.

The last two arguments, 0.1 and 1000, define the near and far clipping planes. This is a performance optimization disguised as a camera feature. The renderer is smart enough to know it doesn’t need to draw things that are infinitely far away or things that are literally inside the camera lens. So you give it a range. Anything closer to the camera than 0.1 units won’t be rendered. Anything farther than 1000 units will also be ignored. You have to set these values carefully. If your near plane is too large, you might see through nearby objects as they get close. If your far plane is too small, your distant mountains might just pop out of existence as you move away from them.

Finally, the renderer. This is the film crew, the director, and the post-production studio all rolled into one. It takes the scene (the set) and the camera (the instructions on what to film) and produces the final image. The line renderer.setSize(window.innerWidth, window.innerHeight) tells the renderer how big the final movie screen (your element) should be. And document.body.appendChild(renderer.domElement) is the equivalent of putting that screen in the movie theater (your web page).

The renderer is the workhorse. It looks at every object in the scene, figures out where it should be in 2D space based on the camera’s position and orientation, calculates how it should be lit, and then painstakingly draws the millions of pixels that form the final image. It’s doing an incredible amount of matrix multiplication and other linear algebra, and thankfully, Three.js and the underlying WebGL API handle most of it for you. Your job is to feed it a well-constructed scene and a properly configured camera. If you do that, the renderer will take care of the rest. To actually make it do that work, you have to explicitly call its render method.

renderer.render(scene, camera);

This single line is where the magic happens. It takes a snapshot of the scene from the camera’s point of view at that exact moment in time. But a single snapshot is just a photograph, not a movie. To create animation, you need to call this method over and over again. Before we get to that, however, our scene is still depressingly empty. It’s like a film set with no actors.

Now for our star performer, the humble cube

So we have a stage, a camera, and a film crew, but the stage is empty. An empty black screen is not exactly going to win any awards. We need an actor. In the world of 3D graphics, our actors are called Mesh objects. A Mesh is the combination of two distinct things: a Geometry, which defines its shape, and a Material, which defines its appearance (its color, how shiny it is, whether it’s transparent, etc.). You can’t have one without the other. A shape without an appearance is invisible, and an appearance without a shape is… well, it’s just an abstract concept.

Let’s start with the simplest possible actor: a cube. Three.js provides a whole set of pre-built geometries for common shapes, saving you from the mind-numbing task of defining every single vertex and face yourself. For a cube, we use BoxGeometry.

const geometry = new THREE.BoxGeometry(1, 1, 1);

This creates a cube that is 1 unit wide, 1 unit high, and 1 unit deep. A “unit” in Three.js is an abstract concept; you can decide if it means one meter, one centimeter, or one parsec. The important thing is to be consistent. For now, just think of it as a 1x1x1 cube.

Next, we need to give our cube a skin. We need a Material. There are many kinds of materials, but the simplest one is the MeshBasicMaterial. This material is not affected by lights in the scene. It’s like painting the object with glow-in-the-dark paint. Whatever color you give it, that’s the color you’ll see, regardless of lighting conditions. This is great for debugging or when you just want to get something on the screen quickly.

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

Here, we’re creating a basic material and giving it a color using a hex value. 0x00ff00 is a bright, unapologetic green. Now that we have the shape (geometry) and the appearance (material), we can finally create our actor, the Mesh, and add it to our world.

const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

We instantiate a Mesh with our geometry and material, and then we call scene.add() to place it in our scene. It’s now officially part of our scene graph. So if we call renderer.render(scene, camera) now, we’ll see our glorious green cube, right? Wrong. You’ll still see a black screen. Why? Because you just committed a classic blunder. By default, both the camera and the cube we just created are at the exact same position: the origin of our 3D world, coordinates (0, 0, 0). You’ve essentially placed the camera *inside* the cube. You can’t film a movie if the camera is stuck inside the actor’s head. We need to move the camera back so it can see the cube.

camera.position.z = 5;

Every object in Three.js, including the camera, has a position property with x, y, and z components. The Z-axis points out of the screen towards you. By setting camera.position.z to 5, we’ve moved the camera 5 units back along the Z-axis. Now it’s looking at the origin, where our cube is sitting. With this one-line change, calling renderer.render(scene, camera) will finally reward you with the image of a green square on a black background. It’s a square, not a cube, because you’re looking at it head-on, but trust me, it’s a cube. We’ve successfully rendered our first object. But it’s static. It just sits there. To bring it to life, we need to introduce motion, and that means we need to talk about the render loop.

The magic of telling the browser to draw, over and over again

A single call to renderer.render(scene, camera) is like taking one photograph. It captures a single, static moment in time. To create the illusion of motion-an animation-you need to be a filmmaker, not a photographer. You need to create a whole sequence of these photographs, each one slightly different from the last, and then display them in rapid succession. This process is handled by what’s known as the “render loop” or “animation loop.” It’s the beating heart of any real-time graphics application.

The goal is to create a function that does two things: first, it updates the state of the objects in your scene (for example, moving or rotating them), and second, it calls renderer.render() to draw the newly updated scene. Then you need to have the browser call this function over and over again, ideally at the same frequency as your monitor’s refresh rate, which is usually 60 times per second.

You might be tempted to use something like setInterval(myAnimationFunction, 1000 / 60) to achieve this. Don’t. That’s the old, brute-force way of doing things. The browser provides a much smarter, more efficient mechanism specifically for this purpose: requestAnimationFrame. When you use requestAnimationFrame, you’re essentially telling the browser, “I want to do an animation. Please call my function right before you do your next repaint.” The browser then schedules your function to run at the most optimal time. This has huge advantages: it’s synchronized with the display’s refresh rate for smoother animations, and it’s smart enough to pause the animation when the user switches to another tab, saving precious CPU cycles and battery life.

Here is what a basic render loop using requestAnimationFrame looks like:

function animate() {
	requestAnimationFrame(animate);
	renderer.render(scene, camera);
}
animate();

Let’s break this down. We define a function called animate. The very first thing this function does is call requestAnimationFrame and pass itself-the animate function-as the callback. This is how we create the loop. Each time animate runs, it ensures it will be called again on the next available frame. After that, it calls renderer.render() to draw the current state of the scene. Finally, we make a single call to animate() outside the function definition to kick the whole process off. Right now, this will render our static cube 60 times a second, which is pointless. To see the power of the loop, we need to change something *inside* it.

Let’s make our cube rotate. Every object you add to the scene has properties like position, scale, and rotation. We can modify these properties inside our animate function, and because the function runs on every frame, the changes will be rendered, creating animation. Let’s add two lines to our loop to rotate the cube on its X and Y axes.

function animate() {
	requestAnimationFrame(animate);

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render(scene, camera);
}
animate();

Now we’re talking! Inside the loop, before we render, we’re incrementing the cube’s rotation. The rotation values are in radians, so we’re adding a small amount (about half a degree) to the X and Y rotation on every single frame. When the browser runs this code 60 times a second, your eyes perceive these tiny, discrete rotational steps as a single, smooth spinning motion. You’ve now taken your static scene and breathed life into it. This simple loop-update state, then render-is the fundamental pattern you will use for every animation you ever create in Three.js.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *