Learning 3D Graphics With Three.js | Manual Matrices

View this thread on: d.buzz | hive.blog | peakd.com | ecency.com
·@clayjohn·
0.000 HBD
Learning 3D Graphics With Three.js | Manual Matrices
#### What Will I Learn?

- You will learn how a matrix is used under the hood in three.js
- You will learn how to create and manipulate matrices in three.js

#### Requirements

- Basic familiarity with structure of three.js applications
- Basic programming knowledge
- Basic knowledge of 3D geometry
- Basic knowledge of 3D operations (translate, rotate, scale)
- Familiarity with Matrices outside of three.js
- Any machine with a webgl compatible web browser
- Knowledge on how to run javascript code (either locally or using something like a [jsfiddle](https://jsfiddle.net))

#### Difficulty

- Intermediate

#### Why Learn How to Handle Matrices Manually
In three.js 3D operations are split into three components: position, rotation, and scale. You can access these as ```Vector3``` properties of any ```Object3D``` derived object for example, a ```Mesh```. Every frame three.js will recalculate a local and world matrix from these three properties. 

We have two reasons for wanting to take control of our own matrices. The first is speed. While three.js has made 3D operations easy and fast enough for most use cases, sometimes it is just faster to do things on our own. And in such a case it may be worthwhile to gain the speedup and only update our matrices when we need to. The second reason is that when you have a lot of operations that rely on the position of other objects in the scene it can be easier and more intuitive to just update the matrices manually rather than relying on three.js's built in matrix hierarchy. 

This second point deserves a little more elaboration. Currently three.js stores a world matrix and local matrix for each object in the scene. When one object is added to another the child object inherits the matrix of its parent. It multiplies its own local matrix by its parents matrix to create a world matrix. Its parent does the same with its parent and so on. This gives us a robust and intuitive way to store our objects in a hierarchy, but, as I will show below, it has some shortcomings. 

#### 3D Operations in three.js
three.js allows you to make changes to your mesh very easily using the properties ```position```, ```rotation```, and ```scale```. These are the basic object operations in 3D graphics and in three.js. These operations allow you to manipulate and orient single objects with ease. 

It is much easier to discuss 3D operations with visual aids so lets start with a basic scene comprised of a ```BoxGeometry```, a ```SphereGeometry```, and a ```CylinderGeometry```:

```javascript
var box_geometry = new THREE.BoxGeometry(); //Default width, length, height of 1
var sphere_geometry = new THREE.SphereGeometry(0.5, 32, 32); //radius of 0.5, with 32 horizontal and vertical segments
var cylinder_geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.5); //0.1 radius at top and bottom with a height of 0.5
			
var material = new THREE.MeshLambertMaterial({color: new THREE.Color(0.9, 0.55, 0.4)});
```

Then we create three meshes and place them into the scene

```javascript
var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
sphere.position.y += 1;
var cylinder = new THREE.Mesh(cylinder_geometry, material);
cylinder.position.y += 1.75;

scene.add(box);
scene.add(sphere);
scene.add(cylinder);
```

In three.js positions refer to the center of the object. So the ```box``` is centered at ```(0, 0, 0)``` and its top is at 0.5. So our ```sphere``` needs to move up by 1 unit so that it sits on top of the ```box```. Then, since the ```cylinder``` is only 0.5 units tall it only needs to move up 1.75 to clear the top of the ```sphere```. This code produces the scene below:

![][Simple Scene]

Not the most beautiful scene, but it has enough elements to highlight the problem. Now say we want our little pile of objects to be half the size. Easy! We just scale each object by 0.5 like so:

```javascript
box.scale.multiplyScalar(0.5);
sphere.scale.multiplyScalar(0.5);
cylinder.scale.multiplyScalar(0.5);
```

Since ```scale``` is just a regular ```Vector3``` we take advantage of the ```Vector3``` builtin function ```multiplyScalar``` to set all three axis of the ```Vector3``` to 0.5. And this results in...

![][Scaled Scene]

...something completely wrong. What has happened here? Well when we scaled each object it got scaled relative to the position it was given so it shrunk in place. In order to make the pile work again we would need to recalculate where each object should be based on the other objects' scaling. Now, this isn't such a tough problem to fix. three.js has an elegant way to handle cases like this. We define an empty ```Object3D``` and we place our three objects inside it and then we apply the scale to the parent object.

```javascript
var pile = new THREE.Object3D();
pile.scale.multiplyScalar(0.5);

pile.add(box);
pile.add(sphere);
pile.add(cylinder);
scene.add(pile);
```

*remember to no longer scale the objects individually, or add them to the scene individually*

Okay, so now we have something that works a bit better.

![][Child Objected Scaled]

Lets try adding rotations into the mix. Lets try to rotate that ```cylinder``` around the surface of the ```sphere``` a bit as if it is sliding off.

```javascript
cylinder.rotation.z -= Math.PI * 0.25;
```

![][Naive Rotation]

Again, not quite what we wanted. We have two options here, we can calculate, using our math skills, what position the ```cylinder``` should be at relative to the ```sphere``` to be in the correct place. Or we can create another ```Object3D``` between the ```pile``` and the ```cylinder``` that is positioned at the ```sphere```'s location but rotated in the direction we want and then we can just translate the ```cylinder``` to the correct position. That sounds awfully complicated. It would be much easier to calculate the matrices ourselves. So let's try that.

#### Handling 3D Operations On Your Own

A note before starting. We need to tell three.js not to update the matrix based on the ```position```, ```rotation```, and ```scale``` properties by setting the property ```matrixAutoUpdate``` to ```false```.

```javascript
box.matrixAutoUpdate = false;
sphere.matrixAutoUpdate = false;
cylinder.matrixAutoUpdate = false;
```

Because we have done this we can no longer take advantage of the easy access to ```position```, ```rotation```, and ```scale```. We now only deal with a single method which is ```applyMatrix```. Other than that everything will be handled outside of the object using methods from ```Matrix4```. We will create a ```Matrix4``` for each object and then we will multiply matrices with that matrix to apply subsequent operations. To begin let's start with that simple translation scene again.

```javascript
//do nothing for box because it does not move

var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0);
sphere.applyMatrix(sphere_matrix);

var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
```

There is a lot to unpack here. First we leave the ```box``` alone because it won't move. Then we create a new translation matrix which we apply to the ```sphere```. It translates one unit in the ```y``` direction. But for the ```cylinder``` we copy over the matrix from the ```sphere``` (make sure to clone it, if you don't you will end up applying the translation to the ```sphere``` as well) and we apply a further translation from there. What this does is ensure that the ```cylinder``` inherits all the operations from the sphere. Alternatively we could have made it a fresh matrix and translated it 1.75 units again.

![][Matrix Scene]

Looks identical to the first scene. That is good, it means everything is working as it should. Now let's see what we can do to solve our rotation problem. We can actually solve it by inserting a single line. Before we translate our ```cylinder``` we rotate it like so:

```javascript
cylinder_matrix.multiply(new THREE.Matrix4().makeRotationZ(-Math.PI * 0.25));
```

That's it. And we end up with.

![][Matrix Rotation]

It looks perfect. Then if we want to scale the whole scene we will just add a scale operation to the ```box``` ```Mesh``` and then copy that matrix down the line. Put together we end up with.

```javascript
var box_matrix = new THREE.Matrix4().makeScale(0.5, 0.5, 0.5);
box.applyMatrix(box_matrix);

var sphere_matrix = box_matrix.clone();
sphere_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0));
sphere.applyMatrix(sphere_matrix);

var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
```

![][Matrix Rotation Scaled]

And there we have it! Everything scales neatly and it requires next to no additional effort. The astute reader will realize that all we have done is created an object hierarchy with the box at the top and the cylinder at the end. If we really wanted to achieve this effect we could have added the cylinder to an empty object with a rotation and added that to the sphere and then added the sphere to the box. Under the hood three.js would be doing the same thing that we just did in 8 lines of code. But wasn't that a lot more fun?

#### Summary
Uses custom matrix math is very powerful. Don't worry if that is not immediately clear from this tutorial. One day while creating cool webgl experiments you will find yourself needing to tie together different objects without parenting them to each other. Hopefully you have learned:

- The value of the ```Matrix``` in three.js
- How to handle ```Matrix``` operations yourself in three.js

#### Curriculum
To learn some more basic aspects of three.js please follow the below tutorials. Although they are not required to understand this one.

- [Materials in three.js](https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-advanced-materials-and-custom-shaders)
- [Dynamic Geometry in three.js](https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-dynamic-geometry)
- [Procedural Geometry in three.js](https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-procedural-geometry)
 


[Simple Scene]: https://i.imgur.com/nIc9wHz.png
[Scaled Scene]: https://i.imgur.com/UmAqS3K.png
[Child Objected Scaled]: https://i.imgur.com/K1M2lx9.png
[Naive Rotation]: https://i.imgur.com/jtzSS9p.png

[Matrix Scene]: https://i.imgur.com/sf4pCu2.png
[Matrix Rotation]: https://i.imgur.com/EynvFZS.png
[Matrix Rotation Scaled]: https://i.imgur.com/EbQ8YKt.png

<br /><hr/><em>Posted on <a href="https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-manual-matrices">Utopian.io -  Rewarding Open Source Contributors</a></em><hr/>
👍 , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,