Getting Creative with The Built-In Geometries
The three.js core includes a large selection of basic geometric shapes. We’ve already seen two of these:
our trusty BoxBufferGeometry
, and the SphereBufferGeometry
we introduced in the last chapter. There are many other shapes besides these two, from basic cylinders and circles to exotic dodecahedrons. You can use these geometries like an infinite box of stretchy, squishy Lego to build nearly anything your imagination can come up with.
The built-in geometries range from the mundane:
… to the exotic:
… to the specialized:
… and many more besides. Search for “Geometry” in the docs to see all of them.
In this chapter, we’ll use the transformations we learned a few chapters ago ( translate, rotate, and scale) to manipulate these geometries and build a simple toy train model. At the same time, we’ll use this as an opportunity to explore ways of structuring scene components that are more complex than anything we have created so far. We’ll also take a deeper look at using transformations, in particular rotation, which is the trickiest of the three transformations to use. We’ll use just two of the geometries to build the toy train: a box geometry for the cabin, and a cylinder geometries for the wheels, nose, and chimney.
The Material.flatShading
Property
We’ll also introduce a new material property in this chapter.
Material.flatShading
is defined in the base Material
class, which means it’s available for every kind of material. By default, it’s set to false.
As we mentioned in the previous chapter, all geometries are made out of triangles. The only shapes you can draw using WebGL are points, lines, and triangles, and all other shapes are made from these. However, Mesh
objects are made exclusively from triangles, never points or lines. When they are part of a mesh, these triangles are referred to as faces. To create smooth curves, the triangles need to be very tiny. However, to reduce the number of triangles needed faces next to each other are usually blended in lighting calculations. We’ll explain how this works in more detail once we get around to explaining what normals are later in the book.
If you turn on .flatShading
, adjacent faces are no longer blended. You can use this to give an object a carved or faceted look, which can be a nice effect for low-poly objects like our train.
You can create a material with flat shading enabled by passing the parameter into the constructor:
You can also set the material.flatShading
property after creating the material. However, if you have already used the material in a rendered scene (technically, if the material has been compiled), you will also need to set the
material.needsUpdate
flag:
Introducing the CylinderBufferGeometry
This is the first time we’ve used a
CylinderBufferGeometry
, so let’s take a moment to examine it now.
The first three parameters define the shape and size of the cylinder:
radiusTop
: the radius of the top of the cylinder.radiusBottom
: the radius of the bottom of the cylinder.height
: the height of the cylinder.
By making radiusTop
and radiusBottom
different sizes you can create cones instead of cylinders. There is also a
ConeBufferGeometry
, but under the hood, it’s just a CylinderBufferGeometry
with radiusBottom
set to zero.
The next two parameters define how detailed the geometry is:
radialSegments
: how detailed the cylinder is around its curved edge. The default is 8, but you’ll want to increase this in most cases to make the cylinder more smooth.heightSegments
: how detailed the cylinder is along its height. The default value of 1 is usually fine.
The final three parameters define how complete the cylinder is:
openEnded
: whether to draw caps on the top and bottom of the cylinder.thetaStart
: what point around the curvature the cylinder is drawn from.thetaLength
: how far around the curvature to draw.
By setting openEnded
to false, you can create a tube instead of a cylinder. thetaStart
and thetaLength
are easily understood if you play around with them in the live example above, or in your own code. You don’t have to supply all the parameters when creating a CylinderBufferGeometry
. In most cases, the first four are sufficient
By varying the initial parameters, this “cylinder” geometry can be used to create cones, tubes, and various trough-like shapes. Most of the other geometries are similarly flexible, which means the initial set of twenty geometries can be used to create a near-infinite variety of shapes.
Helpers
In the editor, we’ve added a couple of helpers to make it easier for you to build the train. There’s an
AxesHelper
, which is three lines representing the $X$, $Y$, and $Z$ axes, and a
GridHelper
, which is a rectangular grid with thick black lines going through the center of the scene, and smaller gray lines at one-unit intervals.
You’ll often find it useful to add helpers like this when constructing your scenes, especially while you’re getting used to working with the three.js coordinate system. There are many other helpers besides these two to help us visualize all kinds of things our scenes, like boxes, cameras, lights, arrows, planes, and so on.
Here, note the colors of the lines in the axes helper: RGB, representing XYZ: the $X$-axis is red, the $Y$-axis is green, and the $Z$-axis is blue. Next, note that each square of the grid helper is a $1 \times 1$ square, which you can use to help visualize the size of pieces of the train. Our final train will be about nine meters long, perhaps a little big for a toy train (or perhaps not), but we won’t worry about that for now. You can also adjust the size of the squares in the helper, which is useful when building large or small scenes.
Working With Rotations
To build the train, we’ll create several shapes and then transform (translate, rotate, and scale) them into position. Although we covered the technical details of 3D transformations a few chapters ago, putting the theory into practice takes some work. Translating and scaling objects usually works as you expect, as long as you keep the coordinate system firmly in mind. On the other hand, working with rotations can be tricky. Here, we’ll take a few moments to examine the rotation operations we’ll need to build the train.
Look at the world space coordinate system above. The origin, $(0,0,0)$, is at the very center of your scene. Keep this diagram in mind while working with transformations throughout this chapter. Also, note how the colors in the diagram match the colors of the axes helper in the editor: RGB for XYZ.
 
Next, take a look at the train. The cabin is made from a box geometry, and everything else is made from cylinders. Even the chimney is a cylinder with a smaller radius at the bottom than at the top. The red nose is oriented along the $X$-axis, while the black wheels are orientated along the $Z$-axis. Finally, the chimney is oriented upwards along the $Y$-axis. When we say a cylinder is oriented along an axis, we mean the axis is parallel to a line drawn through the center of the cylinder.
Before we proceed to move the pieces into position, remember, the direction of a positive rotation in three.js is anti-clockwise. This is probably the opposite to what your intuition expects, and it’s also the opposite of CSS rotations, so make a special note of this:
Positive Rotation = Anti-Clockwise!
When we create a CylinderBufferGeometry
, it starts out pointing upwards like a tree trunk, oriented along the $Y$-axis. How do we work out the rotations required to move this into position, to create the wheel, chimney, and nose? Of course, we could use trial and error. However, we’d prefer to use a more sophisticated approach.
 
Let’s consider the large red nose first. We want the nose to lie along the $X$-axis. This means we need to rotate it by $90^{\circ}$, or $\frac{\pi}{2}$ radians, anti-clockwise around the $Z$-axis.
 
That accounts for the nose. What about the wheels? Once again, the cylinder we’ll create for the wheels begins its life pointing upwards along the $Y$-axis.
 
We want the wheels to lie parallel to the $Z$-axis, so this time, we’ll rotate around the $X$-axis. Again, it’s a $90^{\circ}$ anti-clockwise (positive) rotation.
 
The final mesh we need to consider is the chimney. Once again, we’ll create a geometry (this time, cone-shaped) that starts out pointing up along the $Y$-axis. The chimney also points upwards, so we won’t need to rotate this mesh after creating it.
When working with rotations, often, we’ll use the
three.js helper function .degToRad
to
convert from degrees to radians. However, many degree values are easy to write as radians since $180^{\circ} = \pi$ radians, so simple division will give us a range of other radian values, in particular, $90^{\circ} = \frac{\pi}{2}$ and $45^{\circ} = \frac{\pi}{4}$.
A Simple Toy Train Model
With all that talk of rotations under our belts, hopefully, it will be easy to build the train, so let’s get started. We’ll also use this simple model as an opportunity to build a template for future, more complex scene components. To that end, we’ll create separate modules for geometries, materials, and meshes, and then create a Train
class to coordinate the other modules and provide a minimal interface for use within World
.
If this sounds familiar to you, it’s because this is a microcosm of how we set up the World app. There are two reasons for this:
- Familiarity: The more similar individual sections of our code are, the less we have to think when switching focus.
- Reusability: Just as we want to be able to hand the World/ folder over to another developer with a single paragraph of instructions on how to use it, we want to be able to copy the Train/ component between our apps with zero effort.
On the other hand, this structure won’t be the best for every possible component you create. Always make sure the structure of your code supports what you are trying to build, rather than making you fight against it.
In the editor, we have deleted the meshGroup.js module from the previous chapter and replaced it with a new components/Train/ folder. If you’re working on your own machine, go ahead and do that now. Inside this folder, there are four modules:
- components/Train/geometries.js
- components/Train/materials.js
- components/Train/meshes.js
- components/Train/Train.js
Initial Structure of geometries.js, materials.js, and meshes.js
The first two modules follow a similar format to all the other components and systems we’ve created so far.
import { BoxBufferGeometry, CylinderBufferGeometry } from 'three';
function createGeometries() {}
export { createGeometries }
import { MeshStandardMaterial } from 'three';
function createMaterials() {}
export { createMaterials }
Finally, the meshes module. This is similar to the other two, however, the meshes will require the geometries and materials created by the other two modules, so import them at the top of the module, after we import Mesh
from the three.js core (vendor imports will always go before our local imports). Finally, call each function and store the results in the geometries
and materials
variables.
import { Mesh } from 'three';
import { createGeometries } from './geometries.js';
import { createMaterials } from './materials.js';
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
}
export { createMeshes }
The Train
Class Extends Group
Next, the Train
class. Here, we’ll do something new and
extend the Group
class:
import { Group } from 'three';
class Train extends Group {
constructor() {
super();
}
}
export { Train }
Note the use of super()
. This means the Train
class now has all the normal functionality of a Group
. In particular, we can add objects to it, and we can add it directly to our scene:
We can also add objects to the train from within the class itself, using this.add
:
Import the Meshes
Using this knowledge, we can finish setting up the Train
class. First, import the createMeshes
function, then call it and store the result in a member variable, this.meshes
. At the very end of this chapter, we’ll add some animation to the wheels, which means we need to access the meshes from outside the constructor, hence the use of a member variable here.
import { Group } from "three";
import { createGeometries } from "./geometries.js";
import { createMaterials } from "./materials.js";
import { createMeshes } from "./meshes.js";
class Train extends Group {
constructor() {
super();
this.meshes = createMeshes();
}
}
export { Train };
World.js Setup
Over in World, import the Train
class. If you’re working with code from the last chapter, remove any references to meshGroup
from the file.
import { createCamera } from './components/camera.js';
import {
createAxesHelper,
createGridHelper,
} from './components/helpers.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { Train } from './components/Train/Train.js';
import { createControls } from './systems/controls.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
Next, create an instance of the train and add it to the scene.
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const controls = createControls(camera, renderer.domElement);
const { ambientLight, mainLight } = createLights();
const train = new Train();
scene.add(ambientLight, mainLight, train);
const resizer = new Resizer(container, camera, renderer);
scene.add(createAxesHelper(), createGridHelper());
}
Other Changes
Note that we have also made some minor adjustments to the camera position in camera.js, slightly moved the
controls.target
in controls.js to better frame the train, as well as reducing the intensity of both lights in lights.js.
Create the Materials
At this point, we have finished creating the structure of our new scene component. All that remains is to set up the materials, geometries, and meshes. These don’t have to take the form of a train. You can use this structure as a template to create any shape you can dream of.
We’ll create two materials for the train: a dark gray material for the chimney and wheels, and a reddish material for the body. We’ll use
MeshStandardMaterial
with
.flatShading
enabled for both. Other than .flatShading
, there’s nothing new here. Here’s the complete materials module:
import { MeshStandardMaterial } from 'three';
function createMaterials() {
const body = new MeshStandardMaterial({
color: 'firebrick',
flatShading: true,
});
const detail = new MeshStandardMaterial({
color: 'darkslategray',
flatShading: true,
});
return { body, detail };
}
export { createMaterials };
We’ve chosen firebrick
red for the body and darkslategray
for the wheels and chimney, but you can take a look through the
list of CSS colors and pick out two that you like. At the end of the module, we return an object containing both materials for use within meshes.js.
Create the Geometries
We’ll use just two types of geometry for every part of the train: a box geometry for the cabin, and cylinder geometries with various parameters for everything else.
The Cabin Geometry
First up, the box-shaped cabin. A single BoxBufferGeometry
will suffice here. Create one with the following parameters:
Length | Width | Height |
---|---|---|
$2$ | $2.25$ | $1.5$ |
function createGeometries() {
const cabin = new BoxBufferGeometry(2, 2.25, 1.5);
}
Different values for the length, width, and height will give us a rectangular box, unlike the cube we have used in previous chapters.
The Nose Geometry
Next, create the first CylinderBufferGeometry
for the nose, using these parameters:
Top radius | Bottom radius | Height | Radial segments |
---|---|---|---|
$0.75$ | $0.75$ | $3$ | $12$ |
radiusTop
and radiusBottom
are equal, so we’ll get a cylinder. A value of $12$ for the radialSegments
, when combined with Material.flatShading
, will make the cylinder look like it has been roughly carved.
function createGeometries() {
const cabin = new BoxBufferGeometry(2, 2.25, 1.5);
const nose = new CylinderBufferGeometry(0.75, 0.75, 3, 12);
}
The Wheels Geometry
We can reuse a single CylinderBufferGeometry
for all four wheels, even the large rear wheel. You can reuse a geometry in any number of meshes, and then change the .position
,.rotation
and .scale
for each mesh. This is more efficient than creating a new geometry for every mesh, and you should do this whenever possible. Create a cylinder geometry with these parameters:
Top radius | Bottom radius | Height | Radial segments |
---|---|---|---|
$0.4$ | $0.4$ | $1.75$ | $16$ |
The higher value of 16 for radialSegments
will make the wheels look more rounded. We’re creating the geometry at the correct size for the three smaller wheels, so, later, we’ll have to increase the scale of the larger rear wheel.
function createGeometries() {
const cabin = new BoxBufferGeometry(2, 2.25, 1.5);
const nose = new CylinderBufferGeometry(0.75, 0.75, 3, 12);
// we can reuse a single cylinder geometry for all 4 wheels
const wheel = new CylinderBufferGeometry(0.4, 0.4, 1.75, 16);
}
The Chimney Geometry
Finally, the chimney. It’s a cone, not a cylinder, but as discussed above, if we create a cylinder geometry with different values for radiusTop
and radiusBottom
, the result will be a cone shape. This time, leave radialSegments
at the default value of 8.
Top radius | Bottom radius | Height | Radial segments |
---|---|---|---|
$0.3$ | $0.1$ | $0.5$ | default value |
function createGeometries() {
const cabin = new BoxBufferGeometry(2, 2.25, 1.5);
const nose = new CylinderBufferGeometry(0.75, 0.75, 3, 12);
// we can reuse a single cylinder geometry for all 4 wheels
const wheel = new CylinderBufferGeometry(0.4, 0.4, 1.75, 16);
// different values for the top and bottom radius creates a cone shape
const chimney = new CylinderBufferGeometry(0.3, 0.1, 0.5);
}
Final Geometries Module
Finally, return all of the geometries as an object at the end of the function. Putting all that together, here’s the final geometries module:
import { BoxBufferGeometry, CylinderBufferGeometry } from 'three';
function createGeometries() {
const cabin = new BoxBufferGeometry(2, 2.25, 1.5);
const nose = new CylinderBufferGeometry(0.75, 0.75, 3, 12);
// we can reuse a single cylinder geometry for all 4 wheels
const wheel = new CylinderBufferGeometry(0.4, 0.4, 1.75, 16);
// different values for the top and bottom radius creates a cone shape
const chimney = new CylinderBufferGeometry(0.3, 0.1, 0.5);
return {
cabin,
nose,
wheel,
chimney,
};
}
export { createGeometries };
Create the Meshes
All that remains is to create the meshes. First, we’ll create the cabin, nose, and chimney individually, then
we’ll create one wheel and .clone
it to create the other three.
The Cabin and Chimney Meshes
Create the cabin and chimney meshes as usual, using the body material for the cabin and the detail material for the chimney, then move each mesh into position.
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
}
The values entered for the positions are the result of some trial and error. However, with practice, you’ll find that positioning objects becomes more intuitive and faster. As we mentioned above, there’s no need to rotate the chimney, as it’s already oriented correctly when we create it.
The Nose Mesh
Next up is the big red nose. Create the mesh as normal, using geometries.nose
and materials.body
. This time
we need to rotate as well as position the mesh:
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
const nose = new Mesh(geometries.nose, materials.body);
nose.position.set(-1, 1, 0);
nose.rotation.z = Math.PI / 2;
}
This completes the red body of the train, along with the chimney.
Create the Prototype Wheel
Now, the wheels. We’ll create the smallWheelRear
first and then clone it to create the rest, just as we did with our
protoSphere
from the previous chapter. Create the smallWheelRear
mesh, and then translate it down half a unit on the $Y$-axis to position it under the train. Then,
rotate it to lie along the $X$-axis.
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
const nose = new Mesh(geometries.nose, materials.body);
nose.position.set(-1, 1, 0);
nose.rotation.z = Math.PI / 2;
const smallWheelRear = new Mesh(geometries.wheel, materials.detail);
smallWheelRear.position.y = 0.5;
smallWheelRear.rotation.x = Math.PI / 2;
}
When we clone this wheel to create the rest of the wheels, the cloned meshes will inherit the transformations from the prototype. This means the cloned wheels will start correctly rotated and positioned at the bottom of the train, and we just need to space them out along the $X$-axis.
Create the Other Small Wheels
Clone the proto-wheel to create the other two small wheels, then move each into position along the $X$-axis:
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
const nose = new Mesh(geometries.nose, materials.body);
nose.position.set(-1, 1, 0);
nose.rotation.z = Math.PI / 2;
const smallWheelRear = new Mesh(geometries.wheel, materials.detail);
smallWheelRear.position.y = 0.5;
smallWheelRear.rotation.x = Math.PI / 2;
const smallWheelCenter = smallWheelRear.clone();
smallWheelCenter.position.x = -1;
const smallWheelFront = smallWheelRear.clone();
smallWheelFront.position.x = -2;
}
Create The Large Rear Wheel
The final piece of our train is the large rear wheel. Once again, clone the small wheel, then move it into position at the back of the train. This time, we also need to scale it to make it larger:
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
const nose = new Mesh(geometries.nose, materials.body);
nose.position.set(-1, 1, 0);
nose.rotation.z = Math.PI / 2;
const smallWheelRear = new Mesh(geometries.wheel, materials.detail);
smallWheelRear.position.y = 0.5;
smallWheelRear.rotation.x = Math.PI / 2;
const smallWheelCenter = smallWheelRear.clone();
smallWheelCenter.position.x = -1;
const smallWheelFront = smallWheelRear.clone();
smallWheelFront.position.x = -2;
const bigWheel = smallWheelRear.clone();
bigWheel.position.set(1.5, 0.9, 0);
bigWheel.scale.set(2, 1.25, 2);
}
By scaling, we have doubled the diameter of the big wheel and increased its length by 1.25. But how did we work out which axes to scale on?
Look at the initial position of a newly created CylinderBufferGeometry
once again. Scaling happens independently of rotation, so even though we rotated the mesh, we must decide how to scale based on the original, unrotated geometry. By examining this diagram, we can see that to increase the height, we need to scale on the $Y$-axis, and to increase the diameter, we need to scale by an equal amount on the $X$-axis and $Z$-axis. This gives us the final .scale
value of $(2, 1.25, 2)$.
Final Meshes Module
Putting that all together, here’s the final meshes module. Once again, we have returned an object containing all the meshes for use in the Train module.
import { Mesh } from 'three';
import { createGeometries } from './geometries.js';
import { createMaterials } from './materials.js';
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
const cabin = new Mesh(geometries.cabin, materials.body);
cabin.position.set(1.5, 1.4, 0);
const chimney = new Mesh(geometries.chimney, materials.detail);
chimney.position.set(-2, 1.9, 0);
const nose = new Mesh(geometries.nose, materials.body);
nose.position.set(-1, 1, 0);
nose.rotation.z = Math.PI / 2;
const smallWheelRear = new Mesh(geometries.wheel, materials.detail);
smallWheelRear.position.y = 0.5;
smallWheelRear.rotation.x = Math.PI / 2;
const smallWheelCenter = smallWheelRear.clone();
smallWheelCenter.position.x = -1;
const smallWheelFront = smallWheelRear.clone();
smallWheelFront.position.x = -2;
const bigWheel = smallWheelRear.clone();
bigWheel.position.set(1.5, 0.9, 0);
bigWheel.scale.set(2, 1.25, 2);
return {
nose,
cabin,
chimney,
smallWheelRear,
smallWheelCenter,
smallWheelFront,
bigWheel,
};
}
export { createMeshes };
Add the Meshes to the Train
Next, we’ll add the meshes to the Train. We’ll do this in the train’s constructor.
class Train extends Group {
constructor() {
super();
this.meshes = createMeshes();
this.add(
this.meshes.nose,
this.meshes.cabin,
this.meshes.chimney,
this.meshes.smallWheelRear,
this.meshes.smallWheelCenter,
this.meshes.smallWheelFront,
this.meshes.bigWheel
);
}
}
With that, the train should appear in your scene.
Spin the Wheels!
As a final touch, let’s set the wheels spinning. Give the train a .tick
method, following the same
pattern we use for all animated objects.
class Train extends Group {
constructor() {
// ... lines skipped for clarity
}
tick(delta) {}
}
Next, over in World, add the train to the updatables array.
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const controls = createControls(camera, renderer.domElement);
const { ambientLight, mainLight } = createLights();
const train = new Train();
loop.updatables.push(controls, train);
scene.add(ambientLight, mainLight, train);
const resizer = new Resizer(container, camera, renderer);
scene.add(createAxesHelper(), createGridHelper());
}
Now, we need to figure out what axis to spin the wheels on. Refer once again to the diagram of the initial cylinder geometry orientation. We want it to spin around the axis going through its center, which is the $Y$-axis. The fact that we have rotated the wheels to lie along the $Z$-axis doesn’t change this.
Next, we need to figure out how fast to spin the wheels. We’ll spin at a rate of $24^{\circ}$ per second to give us one complete rotation every fifteen seconds. As usual, we must convert this to radians using the degToRad
helper function.
import { Group, MathUtils } from 'three';
import { createMeshes } from './meshes.js';
const wheelSpeed = MathUtils.degToRad(24);
class Train extends Group {
Finally, update the tick method to rotate each of the four wheels. We must scale the per-second speed by delta here, as usual. Refer back to the Animation Loop chapter for an explanation of why we do this.
tick(delta) {
this.meshes.bigWheel.rotation.y += wheelSpeed * delta;
this.meshes.smallWheelRear.rotation.y += wheelSpeed * delta;
this.meshes.smallWheelCenter.rotation.y += wheelSpeed * delta;
this.meshes.smallWheelFront.rotation.y += wheelSpeed * delta;
}
Once you make this change, the wheel should start to rotate, and with that, our toy train is complete!
Beyond Simple Shapes
The last two chapters have shown us both the strengths and the limitations of the built-in three.js geometries. It’s easy to create one hundred or one thousand clones of a mesh in a loop, and it was relatively easy to create a simple model of a toy train. However, creating a real-world object like a cat or a human would soon overwhelm us. Even for a model as basic as this one, the trial and error required to move the pieces of the train into position took some time.
To create truly amazing models, we need to use an external program designed for that purpose and then load the model into three.js. In the next chapter, we’ll see how to do just that.