Learning 3D Graphics With Three.js | Dynamic Geometry

in utopian-io •  2 years ago  (edited)

What Will I Learn?

  • You will learn what a Geometry is in three.js
  • How they are used within a scene
  • How to update them to make your scene more dynamic
  • How to use Perlin noise in 3D

Requirements

  • Basic familiarity with structure of three.js applications
  • Basic programming knowledge
  • Basic knowledge of application independant 3D rendering (meshes, materials, render calls, etc.)
  • Any machine with a webgl compatible web browser
  • Knowledge on how to run javascript code (either locally or using something like a jsfiddle)

Difficulty

  • Basic

Geometry

Many users of three.js get by just using basic shapes or, more typically, by loading in 3D objects from somewhere else. That works for certain applications but sometimes you want a little more freedom. If you are a beginner to three.js it can be a little daunting to leave the confines of basic geometric shapes, hopefully this tutorial makes things a little easier for you.

The Geometry is one of the fundamental building blocks of a 3D scene in three.js. For the most part you will need to define one in order to draw anything at all.

The Geometry stores important information about the shape and properties of the object being drawn including vertices, uvs, and normals. All of these are combined with a Material inside a Mesh and then sent to the GPU to be drawn to the screen. I don't want to scare you with the details of 3D rendering, so let's just stick with the basics. The vertices describe where a given point is drawn in 3D space, normals describe the orientation of the surface at a given vertex, and uvs create a 2D map over the object ranging from 0-1 so we can access a point on the surface of the object with two dimensional coordinates. Think about uvs like a flat map representation of the earth. We use the vertices and normals to create a globe and we use the uvs to draw the 2D map.

Geometry and BufferGeometry

There are two ways of making and storing geometric information in three.js. They are with Geometry and with BufferGeometry. BufferGeometry is much more restrictive and difficult to work with, but it makes up for that with its increase in speed and efficiency. A general rule of thumb is to use BufferGeometry by default and to only use regular Geometry when you want easy access to the internal properties. For this tutorial we will be using Geometry as it makes everything much more straightforward and the basic concepts will stay the same.

Common Geometries

In order to make things easier for you three.js provides a substantial number of Basic types of Geometry, for each type there is also a corresponding BufferGeometry. For example SphereGeometry and SphereBufferGeometry.

These are typically very easy to instantiate with arguments that make sense. In general there are 1 or 2 arguments for size, and then 1 or 2 arguments for how many segments per side. For example a BoxGeometry is instantiated with 3 values for width, height, and depth and three values for widthSegments, heightSegments, and depthSegments.

Here is an example utilizing many of the basic types:

Included in the above picture are the following basic types:

These are great when you want to draw something using simple shapes. But things get more complicated when you want more complex shapes that aren't provided by three.js or if you want your shape to change and morph over time. In order to address that we are going to learn how to update the geometry while the program is running.

Dynamic Geometry

So, now that you know how to use the basic geometries in three.js, let's look at some advanced usage of geometries. Oftentimes we don't want to just plop an object into a position and let it sit. Luckily the three.js Mesh object has a position property (which is itself a Vector3) so we can move objects like so:

mesh.position.x += 1;

Now this is very straightforward, you can even rotate the mesh in the same way by accessing the rotation property (which is also a Vector3). The more interesting thing to do is update the individual vertices within the model. Say I want to stretch the model itself while the program is running, or say I want a bump to move around the model. I can't just do that by accessing some property of the Mesh what I need to do is update the vertices. Luckily three.js provides us with a way to do this. Through your completed Mesh you can access the Geometry you constructed earlier, and within that Geometry you can access the individual vertices as an array. From this array you can read and modify the individual vertices. In order to illustrate this I will be showing you how to a sphere and morph it into a randomly shaped blob using Perlin noise.

The first step is to set up our object. This is very straightforward. We will be using a SphereGeometry a SphereBufferGeometry can be used as well but the vertices are accessed slightly differently.

var sphere_geometry = new THREE.SphereGeometry(1, 128, 128);
var material = new THREE.MeshNormalMaterial();
var sphere = new THREE.Mesh(sphere_geometry, material);
scene.add(sphere);

We subdivide the sphere with 128 width and height segments. This gives us a lot vertices which translates to very fine detail over the surface of the sphere. Next we set up an update function which we will call everytime we want to update the object.

var update = function() {
    //go through vertices here and reposition them 
}

Make sure to call update() before calling animate() in your code.
Inside, loop over all vertices

for (var i = 0; i < sphere.geometry.vertices.length; i++) {
    var p = sphere.geometry.vertices[i];
}
sphere.geometry.verticesNeedUpdate = true; //must be set or vertices will not update

This gives us all the positions of the vertices which we can manipulate inside the loop. After the loop runs we need to tell three.js that the vertices have been changed so that it can submit the new changes to be drawn. We do this by setting the flag verticesNeedUpdate to true in the sphere's Geometry.

Lets talk about noise, you can download the noise file here. And include it like so in your html file.

<script src="perlin.js"></script>

But what is noise? In one direction noise looks like a cos or sin function in that it is made up of smooth bumps. Except Perlin noise doesn't have a steady amplitude, so the bumps are all different heights. That is nice for us because it looks more organic. In 2D or 3D noise takes a position and returns a value between -1 and 1. The larger the spread of numbers we use in our input the tighter the bumps appear and the smaller the numbers we use as input are the larger the bumps appear, just like with sin and cos. We call the noise function like so:

var value = noise.perlin2(x, y);
var value = noise.perlin3(x, y, z);

What we will do is apply noise to the position of the vertex and push it out from the origin based on the value. In order to keep our vertices more or less in place we first call .normalize() on p, this brings p back into a circle shape with a radius of 1. We then call the function multiplyScalar to multiple p by the noise value. Put together this looks like:

for (var i = 0; i < sphere.geometry.vertices.length; i++) {
    var p = sphere.geometry.vertices[i];
    p.normalize().multiplyScalar(1 + 0.3 * noise.perlin3(p.x, p.y, p.z));
}

You can see that we are multiplying the noise by 0.3 and adding it to 1. This makes sure our noise stays within 0.7-1.3. That way our blob will stay a reasonable size and shape.

Let's make this more interesting looking, let's make the blob a little blobier. With perlin noise we can get finer detail by scaling the input values to make them further apart.

Let's add a variable, k, which changes the scale of the noise.

var k = 3;
for (var i = 0; i < sphere.geometry.vertices.length; i++) {
    var p = sphere.geometry.vertices[i];
    p.normalize().multiplyScalar(1 + 0.3 * noise.perlin3(p.x * k, p.y * k, p.z * k));
}

No we can control how blobby our blob gets!

You may notice the normals look kinda funny. The colors are nice but they dont map onto the blob, let's fix that, add a couple lines at the bottom of the update() function.

sphere.geometry.computeVertexNormals();
sphere.geometry.normalsNeedUpdate = true;

We are now asking three.js to update our normals for each vertex and, just like the vertices, we are asking three.js to resubmit our normal array for drawing. This results in a much crisper looking blob.

Okay so normals look nice and all, but what can we do to make this a little cooler. Well just go back and change that material to another one. Here is the blob with a MeshDepthMaterial.

Try switching it out with different materials and see if you can make more interesting looking blobs!

Animating Over Time

Let's complicate things one more time. It's great that we can add some bumps, but lets make something more dynamic. Lets create a blob that constantly moves and is never the same from frame-to-frame.

First we will move update() to the inside our animate function. Call it directly before calling renderer.render(). Second we add a line to the top of our update() function.

var time = performance.now();

This allows us to track the passage of time in our application. performance.now() is a built in function to javascript that returns time. You can keep track of time anyway that you like, but I prefer this way, it is simple and only takes up one line. One way to animate our blob is just to offset our noise coordinates by time like so:

p.normalize().multiplyScalar(1 + 0.3 * noise.perlin3(p.x * k + time, p.y * k, p.z * k));

Ouch. Too fast let's slow it down

var time = performance.now() * 0.01;
//start loop
    p.normalize().multiplyScalar(1 + 0.3 * noise.perlin3(p.x * k + time, p.y * k, p.z * k));

you should get something that looks like this:

It looks as if the noise is moving across the blob to the left. That is because we are offsetting our noise in the x direction. But what if we want the noise to change all over. We could do this easily by using 4 dimensional noise and using time as a fourth parameter. Unfortunately our library only has 3 dimensional noise. Luckily we have one more trick up our sleeve. Remember our vertex uvs? They are a 2D representation of the surface of our sphere. We can use 2D noise to morph our sphere and then offset the noise in the third dimension by time.

To do this we need to change our function a little bit. We loop over the faces instead and we use the faces to lookup the uv for each vertex. our loop becomes:

for (var i = 0; i < sphere.geometry.faces.length; i++) {
    var uv = sphere.geometry.faceVertexUvs[0][i]; //faceVertexUvs is a huge arrayed stored inside of another array
    var f = sphere.geometry.faces[i];
    var p = sphere.geometry.vertices[f.a];//take the first vertex from each face
    p.normalize().multiplyScalar(1+0.3*noise.perlin3(uv[0].x*k, uv[0].y*k, time));
}

This looks very similar to our previous loop only now we have a few extra things. You'll notice there is an odd discontinuity at the back. This is because the uvs go from 0-1 and wrap around the object. So at the back there the uvs snap from 0 to 1. This could be an issue if you need to look at your blob from all sides. But for now we can forget about it.

Okay, so we have weird organic blogs that change over time. So far we have been picking a vertex position based on the noise value, but what if we allowed the noise value to determine growth rather than position. So each frame the noise would push the vertex in or out. And then it would be in a new position and move at a different speed the next frame. Essentially we will be letting our little blob free to grow as it sees fit. To do so we make a few changes to the inside of our loop, well only to one line

p.add(p.clone().normalize().multiplyScalar(0.1 * noise.perlin3(p.x * k, p.y * k, p.z * k)));

Now our little blob can morph and change without basing itself on time. But what if we combined this with the time based morph from before?

Well, there you have it. A gross, organic looking blob that has a mind of its own.

Summary

That's all there is to it. If you are creative you should be able to figure out a bunch of other operations to run inside our vertex loop and create some really wild effects. If you are musically inclined at all try changing the time modifier so that it matches the beat of your music! With a nice material and correct timing this could make a cool music visualizer.

Hopefully you have learned:

  • How the structure of Geometry is set up in three.js
  • How to leverage three.js to create shapes unconstrained from the basic types

Curriculum

I use a couple different materials in this tutorial under the assumption that you understand what they are. If you don't then follow the tutorial below.



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

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