custom-geometry.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. Title: Three.js Custom Geometry
  2. Description: How to make your own geometry.
  3. TOC: Custom Geometry
  4. <div class="warning">
  5. <strong>NOTE!</strong> This article is deprecated. Three.js r125
  6. removed support for <code>Geometry</code>. Please refer to
  7. the article on <a href="threejs-custom-buffergeometry.html">custom BufferGeometry</a>.
  8. </div>
  9. A [previous article](threejs-primitives.html) gave a tour of
  10. the various built in primitives included in THREE.js. In this
  11. article we'll cover making our own geometry.
  12. Just to be clear, if you are serious about making 3D content,
  13. the most common way is to use a 3D modeling package like
  14. [Blender](https://blender.org),
  15. [Maya](https://www.autodesk.com/products/maya/overview),
  16. [3D Studio Max](https://www.autodesk.com/products/3ds-max/overview),
  17. [Cinema4D](https://www.maxon.net/en-us/), etc...
  18. You'd build a model and then export to [gLTF](threejs-load-gltf.html)
  19. or [.obj](threejs-load-obj.html) and load them up.
  20. Whichever one you choose, expect to spend 2 or 3 weeks going through
  21. their respective tutorials as all of them have a learning curve
  22. to be useful.
  23. Still, there are times when we might want to generate our own
  24. 3D geometry in code instead of using a modeling package.
  25. First let's just make a cube. Even though three.js already
  26. provides us with `BoxGeometry` and `BoxGeometry` a
  27. cube is easy to understand so let's start there.
  28. There are 2 ways to make custom geometry in THREE.js. One
  29. is with the `Geometry` class, the other is `BufferGeometry`.
  30. Each has their advantages. `Geometry` is arguably easier to
  31. use but slower and uses more memory. For few 1000s triangles
  32. it's a great choice but for 10s of thousands of triangles
  33. it might be better to use `BufferGeometry`.
  34. `BufferGeometry` is arguably harder to use but uses less
  35. memory and is faster. If quick rule of thumb might be
  36. if you're going to generate more than 10000 triangles
  37. consider using `BufferGeometry`.
  38. Note when I say `Geometry` is slower I mean it is slower to
  39. start and slower to modify but it is not slower to draw so
  40. if you're not planning on modifying your geometry then
  41. as long as it's not too large there will only be slightly more
  42. delay for your program to start using `Geometry` vs using
  43. `BufferGeometry`. We'll go over both eventually. For now
  44. though let's use geometry as it's easier to understand IMO.
  45. First let's make a cube with `Geometry`. We'll start
  46. with an example from [the article on responsiveness](threejs-responsive.html).
  47. Let's remove the part that uses `BoxGeometry` and replace it with
  48. a `Geometry`.
  49. ```js
  50. -const boxWidth = 1;
  51. -const boxHeight = 1;
  52. -const boxDepth = 1;
  53. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  54. +const geometry = new THREE.Geometry();
  55. ```
  56. Now let's add the 8 corners of a cube. Here are the 8 corners.
  57. <div class="threejs_center"><img src="resources/cube-vertex-positions.svg" style="width: 500px"></div>
  58. Centered around the origin we can add the vertex positions like this
  59. ```js
  60. const geometry = new THREE.Geometry();
  61. +geometry.vertices.push(
  62. + new THREE.Vector3(-1, -1, 1), // 0
  63. + new THREE.Vector3( 1, -1, 1), // 1
  64. + new THREE.Vector3(-1, 1, 1), // 2
  65. + new THREE.Vector3( 1, 1, 1), // 3
  66. + new THREE.Vector3(-1, -1, -1), // 4
  67. + new THREE.Vector3( 1, -1, -1), // 5
  68. + new THREE.Vector3(-1, 1, -1), // 6
  69. + new THREE.Vector3( 1, 1, -1), // 7
  70. +);
  71. ```
  72. We then need to make triangles, 2 for each face of the cube
  73. <div class="threejs_center"><img src="resources/cube-triangles.svg" style="width: 500px"></div>
  74. We do that by creating `Face3` objects and specifying the indices
  75. of the 3 vertices that make up that face.
  76. The order we specify the vertices is important. To be pointing toward the
  77. outside of the cube they must be specified in a counter clockwise direction
  78. when that triangle is facing the camera.
  79. <div class="threejs_center"><img src="resources/cube-vertex-winding-order.svg" style="width: 500px"></div>
  80. Following that pattern we can specify the 12 triangles that make
  81. the cube like this
  82. ```js
  83. geometry.faces.push(
  84. // front
  85. new THREE.Face3(0, 3, 2),
  86. new THREE.Face3(0, 1, 3),
  87. // right
  88. new THREE.Face3(1, 7, 3),
  89. new THREE.Face3(1, 5, 7),
  90. // back
  91. new THREE.Face3(5, 6, 7),
  92. new THREE.Face3(5, 4, 6),
  93. // left
  94. new THREE.Face3(4, 2, 6),
  95. new THREE.Face3(4, 0, 2),
  96. // top
  97. new THREE.Face3(2, 7, 6),
  98. new THREE.Face3(2, 3, 7),
  99. // bottom
  100. new THREE.Face3(4, 1, 0),
  101. new THREE.Face3(4, 5, 1),
  102. );
  103. ```
  104. A few other minor changes to the original code and it should
  105. work.
  106. These cubes are twice as large as the `BoxGeometry` we were
  107. using before so let's move the camera back a little
  108. ```js
  109. const fov = 75;
  110. const aspect = 2; // the canvas default
  111. const near = 0.1;
  112. -const far = 5;
  113. +const far = 100;
  114. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  115. -camera.position.z = 2;
  116. +camera.position.z = 5;
  117. ```
  118. and let's separate them a little more and I changed their colors just because
  119. ```js
  120. const cubes = [
  121. - makeInstance(geometry, 0x44aa88, 0),
  122. - makeInstance(geometry, 0x8844aa, -2),
  123. - makeInstance(geometry, 0xaa8844, 2),
  124. + makeInstance(geometry, 0x44FF44, 0),
  125. + makeInstance(geometry, 0x4444FF, -4),
  126. + makeInstance(geometry, 0xFF4444, 4),
  127. ];
  128. ```
  129. One last thing is we haven't added normals yet so we
  130. can't do any lighting. Let's change the material
  131. to something that doesn't need lights.
  132. ```js
  133. function makeInstance(geometry, color, x) {
  134. - const material = new THREE.MeshPhongMaterial({color});
  135. + const material = new THREE.MeshBasicMaterial({color});
  136. const cube = new THREE.Mesh(geometry, material);
  137. scene.add(cube);
  138. ...
  139. ```
  140. and we get cubes we made ourselves.
  141. {{{example url="../threejs-custom-geometry-cube.html" }}}
  142. We can specify a color per face by setting the `color` property of
  143. each face.
  144. ```js
  145. geometry.faces[ 0].color = geometry.faces[ 1].color = new THREE.Color('red');
  146. geometry.faces[ 2].color = geometry.faces[ 3].color = new THREE.Color('yellow');
  147. geometry.faces[ 4].color = geometry.faces[ 5].color = new THREE.Color('green');
  148. geometry.faces[ 6].color = geometry.faces[ 7].color = new THREE.Color('cyan');
  149. geometry.faces[ 8].color = geometry.faces[ 9].color = new THREE.Color('blue');
  150. geometry.faces[10].color = geometry.faces[11].color = new THREE.Color('magenta');
  151. ```
  152. note we need to tell the material we want to use vertex colors
  153. ```js
  154. -const material = new THREE.MeshBasicMaterial({color});
  155. +const material = new THREE.MeshBasicMaterial({vertexColors: true});
  156. ```
  157. {{{example url="../threejs-custom-geometry-cube-face-colors.html" }}}
  158. We can instead set the color of each individual vertex by setting the `vertexColors`
  159. property of a `Face` to an array of the 3 colors for the 3 vertices.
  160. ```js
  161. geometry.faces.forEach((face, ndx) => {
  162. face.vertexColors = [
  163. (new THREE.Color()).setHSL(ndx / 12 , 1, 0.5),
  164. (new THREE.Color()).setHSL(ndx / 12 + 0.1, 1, 0.5),
  165. (new THREE.Color()).setHSL(ndx / 12 + 0.2, 1, 0.5),
  166. ];
  167. });
  168. ```
  169. {{{example url="../threejs-custom-geometry-cube-vertex-colors.html" }}}
  170. To use lighting we need normals. Normals are vectors that specify direction.
  171. Just like the colors we can specify a normal for the face by setting the `normal`
  172. property on each face with
  173. ```js
  174. face.normal = new THREE.Vector3(...)
  175. ```
  176. or we can specify a normal for each vertex by setting the `vertexNormals`
  177. property with something like
  178. ```js
  179. face.vertexNormals = [
  180. new THREE.Vector3(...),
  181. new THREE.Vector3(...),
  182. new THREE.Vector3(...),
  183. ]
  184. ```
  185. but often it's much easier to just ask THREE.js to compute normals
  186. for us based on the positions we specified.
  187. For face normals we'd call `Geometry.computeFaceNormals` as in
  188. ```js
  189. geometry.computeFaceNormals();
  190. ```
  191. Removing the vertex color stuff and changing the material back to `MeshPhongMaterial`
  192. ```js
  193. -const material = new THREE.MeshBasicMaterial({vertexColors: true});
  194. +const material = new THREE.MeshPhongMaterial({color});
  195. ```
  196. and now our cubes can be lit.
  197. {{{example url="../threejs-custom-geometry-cube-face-normals.html" }}}
  198. Using face normals will always give us a faceted look. We can use
  199. vertex normals for a smoother look by calling `Geometry.computeVertexNormals`
  200. ```js
  201. -geometry.computeFaceNormals();
  202. +geometry.computeVertexNormals();
  203. ```
  204. Unfortunately a cube is not a good candidate for vertex normals since it
  205. means each vertex gets its normal from the
  206. normals of all the faces it shares.
  207. {{{example url="../threejs-custom-geometry-cube-vertex-normals.html" }}}
  208. Adding texture coordinates, sometimes called UVs, is done via an array of
  209. layers of parallel arrays to the `faces` array which is set via `Geometry.faceVertexUvs`.
  210. For our cube we could do something like
  211. ```js
  212. geometry.faceVertexUvs[0].push(
  213. // front
  214. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  215. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  216. // right
  217. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  218. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  219. // back
  220. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  221. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  222. // left
  223. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  224. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  225. // top
  226. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  227. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  228. // bottom
  229. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  230. [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  231. );
  232. ```
  233. It's important to notice `faceVertexUvs` is an array of layers. Each layer
  234. is another set of UV coordinates. By default there is one layer of UV coordinates,
  235. layer 0, so we just add our UVs to that layer.
  236. Let's [add a texture](threejs-textures.html) to our material and switch back to compute face normals
  237. ```js
  238. -geometry.computeVertexNormals();
  239. +geometry.computeFaceNormals();
  240. +const loader = new THREE.TextureLoader();
  241. +const texture = loader.load('resources/images/star.png');
  242. function makeInstance(geometry, color, x) {
  243. - const material = new THREE.MeshPhongMaterial({color});
  244. + const material = new THREE.MeshPhongMaterial({color, map: texture});
  245. const cube = new THREE.Mesh(geometry, material);
  246. scene.add(cube);
  247. ...
  248. ```
  249. {{{example url="../threejs-custom-geometry-cube-texcoords.html" }}}
  250. Putting that all together, let's make a simple heightmap based
  251. terrain mesh.
  252. A heightmap based terrain is where you have a 2D array of heights
  253. that you apply them to a grid. An easy way to get a 2D array of heights
  254. is to draw them in an image editing program. Here's an image I drew.
  255. It's 96x64 pixels
  256. <div class="threejs_center"><img src="../resources/images/heightmap-96x64.png" style="width: 512px; image-rendering: pixelated;"></div>
  257. We'll load that and then generate a heightmap mesh from it.
  258. We can use the `ImageLoader` to load the image.
  259. ```js
  260. const imgLoader = new THREE.ImageLoader();
  261. imgLoader.load('resources/images/heightmap-96x64.png', createHeightmap);
  262. function createHeightmap(image) {
  263. // extract the data from the image by drawing it to a canvas
  264. // and calling getImageData
  265. const ctx = document.createElement('canvas').getContext('2d');
  266. const {width, height} = image;
  267. ctx.canvas.width = width;
  268. ctx.canvas.height = height;
  269. ctx.drawImage(image, 0, 0);
  270. const {data} = ctx.getImageData(0, 0, width, height);
  271. const geometry = new THREE.Geometry();
  272. ```
  273. We extracted the data from the image, now we'll make a grid of cells.
  274. The cells are the squares formed by the center points of each pixel
  275. from the image
  276. <div class="threejs_center"><img src="resources/heightmap-points.svg" style="width: 500px"></div>
  277. For each cell we'll generate 5 vertices. One for each corner of the cell
  278. and one at the center point of the cell with the average height of the 4
  279. corner heights.
  280. ```js
  281. const cellsAcross = width - 1;
  282. const cellsDeep = height - 1;
  283. for (let z = 0; z < cellsDeep; ++z) {
  284. for (let x = 0; x < cellsAcross; ++x) {
  285. // compute row offsets into the height data
  286. // we multiply by 4 because the data is R,G,B,A but we
  287. // only care about R
  288. const base0 = (z * width + x) * 4;
  289. const base1 = base0 + (width * 4);
  290. // look up the height for the for points
  291. // around this cell
  292. const h00 = data[base0] / 32;
  293. const h01 = data[base0 + 4] / 32;
  294. const h10 = data[base1] / 32;
  295. const h11 = data[base1 + 4] / 32;
  296. // compute the average height
  297. const hm = (h00 + h01 + h10 + h11) / 4;
  298. // the corner positions
  299. const x0 = x;
  300. const x1 = x + 1;
  301. const z0 = z;
  302. const z1 = z + 1;
  303. // remember the first index of these 5 vertices
  304. const ndx = geometry.vertices.length;
  305. // add the 4 corners for this cell and the midpoint
  306. geometry.vertices.push(
  307. new THREE.Vector3(x0, h00, z0),
  308. new THREE.Vector3(x1, h01, z0),
  309. new THREE.Vector3(x0, h10, z1),
  310. new THREE.Vector3(x1, h11, z1),
  311. new THREE.Vector3((x0 + x1) / 2, hm, (z0 + z1) / 2),
  312. );
  313. ```
  314. We'll then make 4 triangles from those 5 vertices
  315. <div class="threejs_center"><img src="resources/heightmap-triangles.svg" style="width: 500px"></div>
  316. ```js
  317. // create 4 triangles
  318. geometry.faces.push(
  319. new THREE.Face3(ndx + 0, ndx + 4, ndx + 1),
  320. new THREE.Face3(ndx + 1, ndx + 4, ndx + 3),
  321. new THREE.Face3(ndx + 3, ndx + 4, ndx + 2),
  322. new THREE.Face3(ndx + 2, ndx + 4, ndx + 0),
  323. );
  324. // add the texture coordinates for each vertex of each face
  325. const u0 = x / cellsAcross;
  326. const v0 = z / cellsDeep;
  327. const u1 = (x + 1) / cellsAcross;
  328. const v1 = (z + 1) / cellsDeep;
  329. const um = (u0 + u1) / 2;
  330. const vm = (v0 + v1) / 2;
  331. geometry.faceVertexUvs[0].push(
  332. [ new THREE.Vector2(u0, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v0) ],
  333. [ new THREE.Vector2(u1, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v1) ],
  334. [ new THREE.Vector2(u1, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v1) ],
  335. [ new THREE.Vector2(u0, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v0) ],
  336. );
  337. }
  338. }
  339. ```
  340. and finish it up
  341. ```js
  342. geometry.computeFaceNormals();
  343. // center the geometry
  344. geometry.translate(width / -2, 0, height / -2);
  345. const loader = new THREE.TextureLoader();
  346. const texture = loader.load('resources/images/star.png');
  347. const material = new THREE.MeshPhongMaterial({color: 'green', map: texture});
  348. const cube = new THREE.Mesh(geometry, material);
  349. scene.add(cube);
  350. }
  351. ```
  352. A few minor changes to make it easier to view.
  353. * include the `OrbitControls`
  354. ```js
  355. import * as THREE from './resources/three/r132/build/three.module.js';
  356. +import {OrbitControls} from './resources/threejs/r132/examples/jsm/controls/OrbitControls.js';
  357. ```
  358. ```js
  359. const fov = 75;
  360. const aspect = 2; // the canvas default
  361. const near = 0.1;
  362. -const far = 100;
  363. +const far = 200;
  364. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  365. -camera.position.z = 5;
  366. +camera.position.set(20, 20, 20);
  367. +const controls = new OrbitControls(camera, canvas);
  368. +controls.target.set(0, 0, 0);
  369. +controls.update();
  370. ```
  371. add 2 lights
  372. ```js
  373. -{
  374. +function addLight(...pos) {
  375. const color = 0xFFFFFF;
  376. const intensity = 1;
  377. const light = new THREE.DirectionalLight(color, intensity);
  378. - light.position.set(-1, 2, 4\);
  379. + light.position.set(...pos);
  380. scene.add(light);
  381. }
  382. +addLight(-1, 2, 4);
  383. +addLight(1, 2, -2);
  384. ```
  385. and we deleted the code related to spinning the cubes.
  386. {{{example url="../threejs-custom-geometry-heightmap.html" }}}
  387. I hope that was a useful instruction to making your own
  388. geometry using `Geometry`.
  389. In [another article](threejs-custom-buffergeometry.html) we'll go over `BufferGeometry`.