Building our first Three.js scene
We're going to step through our first, incredibly boring, Three.js scene. It's just going to be a box. That's it. Nothing fancy. I'm intentionally keeping it dry so that there isn't anything extra that can confuse or distract a new user.
Don't worry, we'll add some flair to this in the next article. For now, let's focus on understanding how things actually work.
Your Hello Cube
We'll be doing a few discrete things here.
First, you'll need a place to render your scene. We'll be drawing directly onto the #webgl tag, so your index.html file will look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three.js is Rad</title>
</head>
<body>
<canvas id="webgl"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Let's update our style.css to match:
* {
margin: 0;
padding: 0;
}
body, html {
overflow: hidden; /* Prevent scrolling */
}
#webgl {
width: 100vw;
height: 100vh;
display: block;
}Great, now we're ready to render our first scene! We'll be working in the /src/main.ts file that we imported into our index.html.
Since this is the first time we are encountering a lot of this stuff, I'll be explicit in describing what it does and where you can find out more.
Piece by piece
You'll start by setting up your scene. You have a lot of options here, like what color it is, fog, etc. We are just going to use the most basic defaults for now.
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xBBBBBB);Next, we are going to define a cube. It is just using the THREE.Mesh object, with the simplest of defaults. The object is instantiated and then we just set the normal material. Then, we add the mesh to the scene.
// Object
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(mesh);We've got some sizes that we want to set, because we always want to be aware of the size of the parent container. In this case, the parent is the entire window as we've set the canvas object to take up all of the screen real estate.
// Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};Next, we have to have a camera. It's the digital view that we use to gaze into our new scene. Without it, Three.js has no way to peer inside.
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
scene.add(camera);Now we get to the Renderer. It's a weird concept, but basically, it's the thing that paints the view onto the camera that you've set up. It is the tool we will use in a moment to combine the scene and the camera. It's the glue.
// Renderer
const canvas = document.querySelector('#webgl') as HTMLCanvasElement;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));Without a way to watch the screen size and update our view when something changes, we are stuck with whatever Three.js got when it first painted everything on to the screen. That's not great. So, we set up a basic listener that checks if anything changes, and updates the camera and rendered if it does.
// Resize Listener
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});Lastly, we build our animation loop. There is a concept here, called the tick, that is worth talking about for a second. In the world of 3D development, the tick function is the heartbeat of your application. Without it, you aren't creating a movie. Instead, you're just taking a single polaroid. That's no good. WebGL renders a single frame and then stops. To create the illusion of motion, we need to redraw the scene dozens of times per second.
Inside our tick, we are telling it, for each tick that goes by, rotate the cube just a little bit.
// Animation Loop
const tick = () => {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();All together this comes out to:
import * as THREE from 'three';
import './style.css';
// Scene
const scene = new THREE.Scene();
// Object
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(mesh);
// Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
scene.add(camera);
// Renderer
const canvas = document.querySelector('#webgl') as HTMLCanvasElement;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Resize Listener
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// Animation Loop
const tick = () => {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();