Length: longish
Three.js is a fun JavaScript library that can create 3D shapes. Moving shapes around the 3D world is even more fun!
In this blog post, you will learn how to make a cube containing moving balls that bounce by adding the velocity of each ball to its position.
Adding physics to objects in three.js can be done using physics engines like Ammo.js, Cannon.js, and Enable3d. But for basic physics, we can use simple math involving vectors.
What are position and velocity vectors?
Each 3dObject in three.js has a world position on the x-axis, y-axis, and z-axis. These coordinates are stored as a Vector3. e.g. Vector3(1, 2, 3) is the position x=1, y=2, z=3.
Velocity is the change in position of an object during a period of time. e.g. Moving 10 miles per hour.
We can also give the object a velocity as a Vector3. For example, if the object moves left two units each frame, the velocity vector is Vector3(-2, 0, 0). Two units are subtracted from the object's x-position each time frame. The object moves left on the x-axis by 2, and stays in the same position on the y-axis and z-axis.
To update the new position of the object, we can add the position vector and velocity vector. The x values, y values, and z values will be totaled to make a new Vector3 showing the new position.
- Position Vector: Vector3( 1, 2, 3)
- Velocity Vector: Vector3(-2, 0, 0)
- New Position Vector:
- add x values: (1 + -2) = -1
- add y values: (2 + 0) = 2
- add z values: (3 + 0) = 3
- Vector3(-1, 2, 3)
In this example, all code, except for CSS, is in the index.html document.
1. Start by Importing Three.js Modules.
- Orbit controls allows the user to move the camera with the mouse.
- GUI allows the user to change settings using slider controls.
- Stats visualizes performance rendering the program.
All three.js modules are kept in my modules folder.
You can get these files from:
2. Declare Global Variables
The properties and values of the slider controls are in the config object. The user will be able to control the number of balls in the cube, the ball radius, the velocity of the balls, the light intensity of the scene, the size of the cube container, the cube container opacity and color. That is a lot of c's.
3. Program Setup
The program is setup by a number of functions. Each function performs a specific task in the program.
4a Create the Three.js Scene: initScene();
This function creates the scene, camera, and renderer in three.js - standard boilerplate code.
4b Create a Directional Light: initLights();
Create a directional light by passing in the color and intensity. The intensity is controlled by the GUI slider config object and lightIntensity property declared in the global variables in step 2.
4c Create a Skybox: skybox();
The background will be a giant cube with the user, camera, light, and shapes inside the giant cube. An image texture will be applied to each side of the cube to make it look like a large 3d continuous background.
My skybox images are from OpenGameArt.org here :
https://opengameart.org/content/space-skyboxes-0
All images are in the skybox folder in my code editor directory. The setPath method identifies this folder.
Images are loaded in an array. The order is important! Start with the x-axis (p = positive, n = negative), then the y-axis, and z-axis.
4d Create Orbit Controls: controller();
Enable damping of orbit controls to give the user a more realistic feel of moving the camera.
The maximum distance (maxDistance) the camera can dolly out is 1000 meters. This helps minimize clipping of the skybox.
4e Add Stats Display: statDisplay();
The stats display is a JavaScript performance monitor that shows:
- FPS - number of frames rendered in last second
- MS - milliseconds needed to render a frame
- MB - megabytes of allocated memory
Clicking on the display will change the units (FPS, MS, or MB).
You can get stats.js on Github here: https://github.com/mrdoob/stats.js/.
4f: Slider Controls: datGUI();
Slider controls allow the user to change settings of objects in the 3d world, creating a more immersive experience for the user.
By default, the slider controls show at the top right corner of the screen. To change this, set autoPlace to false. I created a <span> element with an id="datgui" before the <script> tag with all the JavaScript code.
We can connect the slider controls to the span element by getting the span element by the id of "datgui" in the object guiSpan and append the gui controls (gui) to the object guiSpan.
Now we can position the slider controls in CSS using the id of datgui (#datgui).
Returning to our datGUI function, let's add a slider control for every variable we want the user to control in our scene.
For each control, we add the object and property of the value we are changing for that slider. For example, the first slider control changes the numBalls property of the config object. The value is the number of balls.
After the object and property of the value we are changing for that slider, we pass in:
- the lowest number possible for that slider,
- highest number possible for that slider
- incremental value (smallest amount of change possible)
- name that will show on the slider control
- function what will be called when the slider value changes.
The last slider control is a color palette or color picker. When adding a color palette (gui.addColor), colors are RGB values of 0 - 255 in an array. The property 'cubeColor' stores the initial color of [150, 150, 150].
In three.js, object colors are stored as values from 0 - 1. This will lead to converting values shortly.
4g Slider Controls Callback Functions
Let's look at how the slider controls control the property values of the object in the scene. Each time the user adjusts a slider control, we need to update the value of the slider control to that object property to see it in the scene.
(1) changeColor(); - changing the color of the container cube
The color palette uses red, green and blue values from 0 - 255 in and array (config.cubeColor). Three.js uses red, green and blue values from 0 - 1.
To convert the color pallete values to three.js values we are going to normalize the color pallete values by dividing them by the largest possible value (255).
- largest possible value: 255 -> 255/255 = 1
- smallest possible value is 0 -> 0/255 = 0
We can pass color values from 0-1 into the color property of the container cube as a new THREE.Color(R, G, B).
(2) changeSize(); - change the size of the container cube
The changeSize function changes the scale of the container cube on the x, y, z axes. The scale is multiplied by the length, width, and height to give the size of the container cube.
We can use the set method to change the container cube scale values using the slider control value (config.cubeSize).
(5) initBalls(); - clears screen of 3d objects, creates container cube, creates balls
This function is called when:
- the user changes the number of balls in the container
- the user changes the size (radius) of the balls in the container
- the user changes the velocity of the balls in the container
- when the program first runs during initial setup
- clear scene of 3d objects (disposeMeshes function)
- create container cube (createContainer function)
- create balls
The disposeMeshes function creates an empty array and traverses or examines each object in the scene and determines if that object is a mesh. If so, the mesh object is pushed into the meshes array.
Then, the material and geometry of each mesh in the meshes array is removed from the scene using the dispose method, and the mesh object is removed from the scene using the remove method.Now the scene just contains a camera, light, and skybox.
Let's create the container cube.
In the createContainer function, the slider control values for the container cube opacity (config.cubeOpacity), scale of the container cube (config.cubeSize) and color values (config.cubeColor) are used to make the container cube. In this way, the opacity, cube size and color is not reset to the original values, but to the present slider control values.
Now to create the balls! It is here the remaining slider controls change the number of balls, ball size (radius), and velocity of the balls.
We create our ball geometry (THREE.SphereGeometry) only once, as it is the same ball geometry for all of the balls. We can pass in out slider control radius value (config.radius) to build the SphereGeometry.
Let us also pass in the slider control value for the number of balls (config.numBalls) to control the number of loops in the for loop. Each time the for loop runs, the ball geometry, color, and x, y, z position is passed into the createBall function.
The createBall function creates the each ball and gives it an initial velocity as a Vector3 by using the randomDirection method. For example, Vector3(1, -1, -1) means the ball is moving 1 unit to the right, 1 unit down, and 1 unit back each frame.
The user can control the velocity of the balls by changing the slider control for the scalar (config.velScalar).
A scalar is number used to change the size or magnitude of a vector.
For example, if we multiplied Vector3(1, -1, -1) by a scalar of 2:
Vector3(1 , -1 , -1 ) * 2
Vector3( 1 * 2 , -1 * 2 , -1 * 2 )
Vector3( 2, -2 , -2 )
, the result is Vector3(2, -2, -2) as each amount in the vector is multplied by the scalar.
The ball velocity is the velocity vector multiplied by the slider control scalar value.
The velocity vector is stored in the userData object of the 3d object. The userData object is a handy place to store information about the 3d object.
So each ball has a userData object and the value of the vel property is the velocity of that ball.
5 Animate Loop: animate();
The animate loop updates the orbit controls and stats output.
As well, it updates each ball position in the render function and checks to see if it is at the container cube boundary in the checkEdges function.
Let's look at the render function first.
6 Update Ball Position with Velocity: render();
The render function updates each ball position by adding the current position vector3(x, y, z) and the velocity vector3(x, y, z) to make a new position vector3(x, y, z).
The forEach method iterates through each ball (b) in the balls array to add the position and velocity vectors. I really like the forEach method.
7 Checking if a ball is at the edge of the container: checkEdges();
Now that the ball position is updated, let's check if each ball is colliding with the container cube.
We will have to check both ends of the positive and negative ends of all three axes.
So, where are the container cube sides located?
The origin of the container cube is the origin (x=0, y=0, z=0).
The sides are 100 m. Split it in half (1/2 for the positive side, 1/2 for the negative side), the sides of the container cube are at -50 and +50 on the x, y, z axes.
We can't forget the side length is multiplied by the container cube scale on the slider controls.
we also can't forget the ball has a radius. Right now, we are checking if the middle of the ball is on the edge. If we add the radius on the negative side and subtract the radius on the positive side of the axes, the ball is less likely to go past the container wall.
The start of the checkEdges function defines the edges of the container cube:
- nedge: negative side of x, y, z axes: -50 * scale + ball radius
- edge: positive side of x, y, z axes: +50 * scale - ball radius
Still in the checkEdges function, we need to compare every ball position to all six container wall positions.
If the ball is over the edge:
- the ball is moved back into the container cube by the offset
- velocity of the ball is reversed
- change sign of velocity (+ to -) or (- to +)
8 Respond to Window Resize Events: resize();
Lastly, add an event lister that calls a function in response to window resize events.
There we go! We accomplished quite a bit:
- imported modules
- declared global variables, including slider control properties for dat.gui
- created a skybox
- initialized the scene, camera, and renderer
- added a directional light
- added and set orbit controls
- added stats display for rendering performance on the device
- added dat.gui slider controls for various object properties
- added callback functions to change the object property when a slider control changed
- added a method to dispose of 3d objects: geometry, material, and mesh
- created a container cube
- created moving balls by adding velocity and position vectors
- made the balls rebound off the cube container sides
- added window resize capability