123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- Title: Three.js Custom Geometry
- Description: How to make your own geometry.
- TOC: Custom Geometry
- <div class="warning">
- <strong>NOTE!</strong> This article is deprecated. Three.js r125
- removed support for <code>Geometry</code>. Please refer to
- the article on <a href="threejs-custom-buffergeometry.html">custom BufferGeometry</a>.
- </div>
- A [previous article](threejs-primitives.html) gave a tour of
- the various built in primitives included in THREE.js. In this
- article we'll cover making our own geometry.
- Just to be clear, if you are serious about making 3D content,
- the most common way is to use a 3D modeling package like
- [Blender](https://blender.org),
- [Maya](https://www.autodesk.com/products/maya/overview),
- [3D Studio Max](https://www.autodesk.com/products/3ds-max/overview),
- [Cinema4D](https://www.maxon.net/en-us/), etc...
- You'd build a model and then export to [gLTF](threejs-load-gltf.html)
- or [.obj](threejs-load-obj.html) and load them up.
- Whichever one you choose, expect to spend 2 or 3 weeks going through
- their respective tutorials as all of them have a learning curve
- to be useful.
- Still, there are times when we might want to generate our own
- 3D geometry in code instead of using a modeling package.
- First let's just make a cube. Even though three.js already
- provides us with `BoxGeometry` and `BoxGeometry` a
- cube is easy to understand so let's start there.
- There are 2 ways to make custom geometry in THREE.js. One
- is with the `Geometry` class, the other is `BufferGeometry`.
- Each has their advantages. `Geometry` is arguably easier to
- use but slower and uses more memory. For few 1000s triangles
- it's a great choice but for 10s of thousands of triangles
- it might be better to use `BufferGeometry`.
- `BufferGeometry` is arguably harder to use but uses less
- memory and is faster. If quick rule of thumb might be
- if you're going to generate more than 10000 triangles
- consider using `BufferGeometry`.
- Note when I say `Geometry` is slower I mean it is slower to
- start and slower to modify but it is not slower to draw so
- if you're not planning on modifying your geometry then
- as long as it's not too large there will only be slightly more
- delay for your program to start using `Geometry` vs using
- `BufferGeometry`. We'll go over both eventually. For now
- though let's use geometry as it's easier to understand IMO.
- First let's make a cube with `Geometry`. We'll start
- with an example from [the article on responsiveness](threejs-responsive.html).
- Let's remove the part that uses `BoxGeometry` and replace it with
- a `Geometry`.
- ```js
- -const boxWidth = 1;
- -const boxHeight = 1;
- -const boxDepth = 1;
- -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
- +const geometry = new THREE.Geometry();
- ```
- Now let's add the 8 corners of a cube. Here are the 8 corners.
- <div class="threejs_center"><img src="resources/cube-vertex-positions.svg" style="width: 500px"></div>
- Centered around the origin we can add the vertex positions like this
- ```js
- const geometry = new THREE.Geometry();
- +geometry.vertices.push(
- + new THREE.Vector3(-1, -1, 1), // 0
- + new THREE.Vector3( 1, -1, 1), // 1
- + new THREE.Vector3(-1, 1, 1), // 2
- + new THREE.Vector3( 1, 1, 1), // 3
- + new THREE.Vector3(-1, -1, -1), // 4
- + new THREE.Vector3( 1, -1, -1), // 5
- + new THREE.Vector3(-1, 1, -1), // 6
- + new THREE.Vector3( 1, 1, -1), // 7
- +);
- ```
- We then need to make triangles, 2 for each face of the cube
- <div class="threejs_center"><img src="resources/cube-triangles.svg" style="width: 500px"></div>
- We do that by creating `Face3` objects and specifying the indices
- of the 3 vertices that make up that face.
- The order we specify the vertices is important. To be pointing toward the
- outside of the cube they must be specified in a counter clockwise direction
- when that triangle is facing the camera.
- <div class="threejs_center"><img src="resources/cube-vertex-winding-order.svg" style="width: 500px"></div>
- Following that pattern we can specify the 12 triangles that make
- the cube like this
- ```js
- geometry.faces.push(
- // front
- new THREE.Face3(0, 3, 2),
- new THREE.Face3(0, 1, 3),
- // right
- new THREE.Face3(1, 7, 3),
- new THREE.Face3(1, 5, 7),
- // back
- new THREE.Face3(5, 6, 7),
- new THREE.Face3(5, 4, 6),
- // left
- new THREE.Face3(4, 2, 6),
- new THREE.Face3(4, 0, 2),
- // top
- new THREE.Face3(2, 7, 6),
- new THREE.Face3(2, 3, 7),
- // bottom
- new THREE.Face3(4, 1, 0),
- new THREE.Face3(4, 5, 1),
- );
- ```
- A few other minor changes to the original code and it should
- work.
- These cubes are twice as large as the `BoxGeometry` we were
- using before so let's move the camera back a little
- ```js
- const fov = 75;
- const aspect = 2; // the canvas default
- const near = 0.1;
- -const far = 5;
- +const far = 100;
- const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
- -camera.position.z = 2;
- +camera.position.z = 5;
- ```
- and let's separate them a little more and I changed their colors just because
- ```js
- const cubes = [
- - makeInstance(geometry, 0x44aa88, 0),
- - makeInstance(geometry, 0x8844aa, -2),
- - makeInstance(geometry, 0xaa8844, 2),
- + makeInstance(geometry, 0x44FF44, 0),
- + makeInstance(geometry, 0x4444FF, -4),
- + makeInstance(geometry, 0xFF4444, 4),
- ];
- ```
- One last thing is we haven't added normals yet so we
- can't do any lighting. Let's change the material
- to something that doesn't need lights.
- ```js
- function makeInstance(geometry, color, x) {
- - const material = new THREE.MeshPhongMaterial({color});
- + const material = new THREE.MeshBasicMaterial({color});
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
- ...
- ```
- and we get cubes we made ourselves.
- {{{example url="../threejs-custom-geometry-cube.html" }}}
- We can specify a color per face by setting the `color` property of
- each face.
- ```js
- geometry.faces[ 0].color = geometry.faces[ 1].color = new THREE.Color('red');
- geometry.faces[ 2].color = geometry.faces[ 3].color = new THREE.Color('yellow');
- geometry.faces[ 4].color = geometry.faces[ 5].color = new THREE.Color('green');
- geometry.faces[ 6].color = geometry.faces[ 7].color = new THREE.Color('cyan');
- geometry.faces[ 8].color = geometry.faces[ 9].color = new THREE.Color('blue');
- geometry.faces[10].color = geometry.faces[11].color = new THREE.Color('magenta');
- ```
- note we need to tell the material we want to use vertex colors
- ```js
- -const material = new THREE.MeshBasicMaterial({color});
- +const material = new THREE.MeshBasicMaterial({vertexColors: true});
- ```
- {{{example url="../threejs-custom-geometry-cube-face-colors.html" }}}
- We can instead set the color of each individual vertex by setting the `vertexColors`
- property of a `Face` to an array of the 3 colors for the 3 vertices.
- ```js
- geometry.faces.forEach((face, ndx) => {
- face.vertexColors = [
- (new THREE.Color()).setHSL(ndx / 12 , 1, 0.5),
- (new THREE.Color()).setHSL(ndx / 12 + 0.1, 1, 0.5),
- (new THREE.Color()).setHSL(ndx / 12 + 0.2, 1, 0.5),
- ];
- });
- ```
- {{{example url="../threejs-custom-geometry-cube-vertex-colors.html" }}}
- To use lighting we need normals. Normals are vectors that specify direction.
- Just like the colors we can specify a normal for the face by setting the `normal`
- property on each face with
- ```js
- face.normal = new THREE.Vector3(...)
- ```
- or we can specify a normal for each vertex by setting the `vertexNormals`
- property with something like
- ```js
- face.vertexNormals = [
- new THREE.Vector3(...),
- new THREE.Vector3(...),
- new THREE.Vector3(...),
- ]
- ```
- but often it's much easier to just ask THREE.js to compute normals
- for us based on the positions we specified.
- For face normals we'd call `Geometry.computeFaceNormals` as in
- ```js
- geometry.computeFaceNormals();
- ```
- Removing the vertex color stuff and changing the material back to `MeshPhongMaterial`
- ```js
- -const material = new THREE.MeshBasicMaterial({vertexColors: true});
- +const material = new THREE.MeshPhongMaterial({color});
- ```
- and now our cubes can be lit.
- {{{example url="../threejs-custom-geometry-cube-face-normals.html" }}}
- Using face normals will always give us a faceted look. We can use
- vertex normals for a smoother look by calling `Geometry.computeVertexNormals`
- ```js
- -geometry.computeFaceNormals();
- +geometry.computeVertexNormals();
- ```
- Unfortunately a cube is not a good candidate for vertex normals since it
- means each vertex gets its normal from the
- normals of all the faces it shares.
- {{{example url="../threejs-custom-geometry-cube-vertex-normals.html" }}}
- Adding texture coordinates, sometimes called UVs, is done via an array of
- layers of parallel arrays to the `faces` array which is set via `Geometry.faceVertexUvs`.
- For our cube we could do something like
- ```js
- geometry.faceVertexUvs[0].push(
- // front
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- // right
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- // back
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- // left
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- // top
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- // bottom
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
- [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
- );
- ```
- It's important to notice `faceVertexUvs` is an array of layers. Each layer
- is another set of UV coordinates. By default there is one layer of UV coordinates,
- layer 0, so we just add our UVs to that layer.
- Let's [add a texture](threejs-textures.html) to our material and switch back to compute face normals
- ```js
- -geometry.computeVertexNormals();
- +geometry.computeFaceNormals();
- +const loader = new THREE.TextureLoader();
- +const texture = loader.load('resources/images/star.png');
- function makeInstance(geometry, color, x) {
- - const material = new THREE.MeshPhongMaterial({color});
- + const material = new THREE.MeshPhongMaterial({color, map: texture});
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
- ...
- ```
- {{{example url="../threejs-custom-geometry-cube-texcoords.html" }}}
- Putting that all together, let's make a simple heightmap based
- terrain mesh.
- A heightmap based terrain is where you have a 2D array of heights
- that you apply them to a grid. An easy way to get a 2D array of heights
- is to draw them in an image editing program. Here's an image I drew.
- It's 96x64 pixels
- <div class="threejs_center"><img src="../resources/images/heightmap-96x64.png" style="width: 512px; image-rendering: pixelated;"></div>
- We'll load that and then generate a heightmap mesh from it.
- We can use the `ImageLoader` to load the image.
- ```js
- const imgLoader = new THREE.ImageLoader();
- imgLoader.load('resources/images/heightmap-96x64.png', createHeightmap);
- function createHeightmap(image) {
- // extract the data from the image by drawing it to a canvas
- // and calling getImageData
- const ctx = document.createElement('canvas').getContext('2d');
- const {width, height} = image;
- ctx.canvas.width = width;
- ctx.canvas.height = height;
- ctx.drawImage(image, 0, 0);
- const {data} = ctx.getImageData(0, 0, width, height);
- const geometry = new THREE.Geometry();
- ```
- We extracted the data from the image, now we'll make a grid of cells.
- The cells are the squares formed by the center points of each pixel
- from the image
- <div class="threejs_center"><img src="resources/heightmap-points.svg" style="width: 500px"></div>
- For each cell we'll generate 5 vertices. One for each corner of the cell
- and one at the center point of the cell with the average height of the 4
- corner heights.
- ```js
- const cellsAcross = width - 1;
- const cellsDeep = height - 1;
- for (let z = 0; z < cellsDeep; ++z) {
- for (let x = 0; x < cellsAcross; ++x) {
- // compute row offsets into the height data
- // we multiply by 4 because the data is R,G,B,A but we
- // only care about R
- const base0 = (z * width + x) * 4;
- const base1 = base0 + (width * 4);
- // look up the height for the for points
- // around this cell
- const h00 = data[base0] / 32;
- const h01 = data[base0 + 4] / 32;
- const h10 = data[base1] / 32;
- const h11 = data[base1 + 4] / 32;
- // compute the average height
- const hm = (h00 + h01 + h10 + h11) / 4;
- // the corner positions
- const x0 = x;
- const x1 = x + 1;
- const z0 = z;
- const z1 = z + 1;
- // remember the first index of these 5 vertices
- const ndx = geometry.vertices.length;
- // add the 4 corners for this cell and the midpoint
- geometry.vertices.push(
- new THREE.Vector3(x0, h00, z0),
- new THREE.Vector3(x1, h01, z0),
- new THREE.Vector3(x0, h10, z1),
- new THREE.Vector3(x1, h11, z1),
- new THREE.Vector3((x0 + x1) / 2, hm, (z0 + z1) / 2),
- );
- ```
- We'll then make 4 triangles from those 5 vertices
- <div class="threejs_center"><img src="resources/heightmap-triangles.svg" style="width: 500px"></div>
- ```js
- // create 4 triangles
- geometry.faces.push(
- new THREE.Face3(ndx + 0, ndx + 4, ndx + 1),
- new THREE.Face3(ndx + 1, ndx + 4, ndx + 3),
- new THREE.Face3(ndx + 3, ndx + 4, ndx + 2),
- new THREE.Face3(ndx + 2, ndx + 4, ndx + 0),
- );
- // add the texture coordinates for each vertex of each face
- const u0 = x / cellsAcross;
- const v0 = z / cellsDeep;
- const u1 = (x + 1) / cellsAcross;
- const v1 = (z + 1) / cellsDeep;
- const um = (u0 + u1) / 2;
- const vm = (v0 + v1) / 2;
- geometry.faceVertexUvs[0].push(
- [ new THREE.Vector2(u0, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v0) ],
- [ new THREE.Vector2(u1, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v1) ],
- [ new THREE.Vector2(u1, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v1) ],
- [ new THREE.Vector2(u0, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v0) ],
- );
- }
- }
- ```
- and finish it up
- ```js
- geometry.computeFaceNormals();
- // center the geometry
- geometry.translate(width / -2, 0, height / -2);
- const loader = new THREE.TextureLoader();
- const texture = loader.load('resources/images/star.png');
- const material = new THREE.MeshPhongMaterial({color: 'green', map: texture});
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
- }
- ```
- A few minor changes to make it easier to view.
- * include the `OrbitControls`
- ```js
- import * as THREE from './resources/three/r132/build/three.module.js';
- +import {OrbitControls} from './resources/threejs/r132/examples/jsm/controls/OrbitControls.js';
- ```
- ```js
- const fov = 75;
- const aspect = 2; // the canvas default
- const near = 0.1;
- -const far = 100;
- +const far = 200;
- const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
- -camera.position.z = 5;
- +camera.position.set(20, 20, 20);
- +const controls = new OrbitControls(camera, canvas);
- +controls.target.set(0, 0, 0);
- +controls.update();
- ```
- add 2 lights
- ```js
- -{
- +function addLight(...pos) {
- const color = 0xFFFFFF;
- const intensity = 1;
- const light = new THREE.DirectionalLight(color, intensity);
- - light.position.set(-1, 2, 4\);
- + light.position.set(...pos);
- scene.add(light);
- }
- +addLight(-1, 2, 4);
- +addLight(1, 2, -2);
- ```
- and we deleted the code related to spinning the cubes.
- {{{example url="../threejs-custom-geometry-heightmap.html" }}}
- I hope that was a useful instruction to making your own
- geometry using `Geometry`.
- In [another article](threejs-custom-buffergeometry.html) we'll go over `BufferGeometry`.
|