Learning 3D Graphics With Three.js | Manual Matrices

in #utopian-io7 years ago (edited)

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)

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:

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

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:

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:

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...

...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.

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.

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.

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

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.

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.

//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.

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:

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

That's it. And we end up with.

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.

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);

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.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @clayjohn I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x