Learning 3D Graphics With Three.js | Dynamic Geometry
utopian-io·@clayjohn·
0.000 HBDLearning 3D Graphics With Three.js | Dynamic Geometry
#### 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](https://en.wikipedia.org/wiki/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](https://jsfiddle.net)) #### 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```](https://threejs.org/docs/index.html#api/core/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```](https://threejs.org/docs/index.html#api/core/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```](https://threejs.org/docs/index.html#api/core/Geometry) and with [```BufferGeometry```](https://threejs.org/docs/index.html#api/core/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: ![][Basic Geometry Scene] Included in the above picture are the following basic types: - [```PlaneGeometry```](https://threejs.org/docs/index.html#api/geometries/PlaneGeometry) - [```BoxGeometry```](https://threejs.org/docs/index.html#api/geometries/BoxGeometry) - [```ConeGeometry```](https://threejs.org/docs/index.html#api/geometries/ConeGeometry) - [```CylinderGeometry```](https://threejs.org/docs/index.html#api/geometries/CylinderGeometry) - [```SphereGeometry```](https://threejs.org/docs/index.html#api/geometries/SphereGeometry) - [```IcosahedronGeometry```](https://threejs.org/docs/index.html#api/geometries/IcosahedronGeometry) - [```TorusGeometry```](https://threejs.org/docs/index.html#api/geometries/TorusGeometry) - [```LatheGeometry```](https://threejs.org/docs/index.html#api/geometries/LatheGeometry) - [```ExtrudeGeometry```](https://threejs.org/docs/index.html#api/geometries/ExtrudeGeometry) 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: ```javascript 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](https://en.wikipedia.org/wiki/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. ```javascript 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); ``` ![][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. ```javascript 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 ```javascript 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](https://raw.githubusercontent.com/josephg/noisejs/master/perlin.js). And include it like so in your html file. ```html <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: ```javascript 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: ```javascript 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. ![][Soft Blob] 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. ```javascript 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)); } ``` ![][Blob No Normal] 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. ```javascript 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. ![][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```. ![][Depth Blob] 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. ```javascript 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: ```javascript 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 ```javascript 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: ![][Animated Blob] 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: ```javascript 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. ![][Animated Blob Uv] 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 ```javascript p.add(p.clone().normalize().multiplyScalar(0.1 * noise.perlin3(p.x * k, p.y * k, p.z * k))); ``` ![][Animated Blob Growth] 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? ![][Animated Blob Combined] 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. - [Using Materials in three.js](https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-advanced-materials-and-custom-shaders) [Basic Geometry Scene]: https://i.imgur.com/PvOHEcT.png [Sphere]: https://i.imgur.com/Pu7Q6Df.png [Blob No Normal]: https://i.imgur.com/tn0Ho2T.png [Blob]: https://i.imgur.com/0rhOrHC.png [Soft Blob]: https://i.imgur.com/2a2Kbgi.png [Depth Blob]: https://i.imgur.com/2UXXeen.png [Animated Blob]: https://i.imgur.com/6BezBlh.gif [Animated Blob UV]: https://i.imgur.com/MRNjbU9.gif [Animated Blob Growth]: https://i.imgur.com/1g65BBH.gif [Animated Blob Combined]: https://i.imgur.com/tAVvCtd.gif <br /><hr/><em>Posted on <a href="https://utopian.io/utopian-io/@clayjohn/learning-3d-graphics-with-three-js-or-dynamic-geometry">Utopian.io - Rewarding Open Source Contributors</a></em><hr/>