custom-buffergeometry.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. Title: Three.js Custom BufferGeometry
  2. Description: How to make your own BufferGeometry.
  3. TOC: Custom BufferGeometry
  4. `BufferGeometry` is three.js's way of representing all geometry. A `BufferGeometry`
  5. essentially a collection *named* of `BufferAttribute`s.
  6. Each `BufferAttribute` represents an array of one type of data: positions,
  7. normals, colors, uv, etc... Together, the named `BufferAttribute`s represent
  8. *parallel arrays* of all the data for each vertex.
  9. <div class="threejs_center"><img src="resources/threejs-attributes.svg" style="width: 700px"></div>
  10. Above you can see we have 4 attributes: `position`, `normal`, `color`, `uv`.
  11. They represent *parallel arrays* which means that the Nth set of data in each
  12. attribute belongs to the same vertex. The vertex at index = 4 is highlighted
  13. to show that the parallel data across all attributes defines one vertex.
  14. This brings up a point, here's a diagram of a cube with one corner highlighted.
  15. <div class="threejs_center"><img src="resources/cube-faces-vertex.svg" style="width: 500px"></div>
  16. Thinking about it that single corner needs a different normal for each face of the
  17. cube. A normal is info about which direction something faces. In the diagram
  18. the normals are presented by the arrows around the corner vertex showing that each
  19. face that shares that vertex position needs a normal that points in a different direction.
  20. That corner needs different UVs for each face as well. UVs are texture coordinates
  21. that specify which part of a texture being drawn on a triangle corresponds to that
  22. vertex position. You can see the green face needs that vertex to have a UV that corresponds
  23. to the top right corner of the F texture, the blue face needs a UV that corresponds to the
  24. top left corner of the F texture, and the red face needs a UV that corresponds to the bottom
  25. left corner of the F texture.
  26. A single *vertex* is the combination of all of its parts. If a vertex needs any
  27. part to be different then it must be a different vertex.
  28. As a simple example let's make a cube using `BufferGeometry`. A cube is interesting
  29. because it appears to share vertices at the corners but really
  30. does not. For our example we'll list out all the vertices with all their data
  31. and then convert that data into parallel arrays and finally use those to make
  32. `BufferAttribute`s and add them to a `BufferGeometry`.
  33. We start with a list of all the data needed for the cube. Remember again
  34. that if a vertex has any unique parts it has to be a separate vertex. As such
  35. to make a cube requires 36 vertices. 2 triangles per face, 3 vertices per triangle,
  36. 6 faces = 36 vertices.
  37. ```js
  38. const vertices = [
  39. // front
  40. { pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], },
  41. { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], },
  42. { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], },
  43. { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], },
  44. { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], },
  45. { pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], },
  46. // right
  47. { pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], },
  48. { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], },
  49. { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], },
  50. { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], },
  51. { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], },
  52. { pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], },
  53. // back
  54. { pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], },
  55. { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], },
  56. { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], },
  57. { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], },
  58. { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], },
  59. { pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], },
  60. // left
  61. { pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], },
  62. { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], },
  63. { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], },
  64. { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], },
  65. { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], },
  66. { pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], },
  67. // top
  68. { pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], },
  69. { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], },
  70. { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], },
  71. { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], },
  72. { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], },
  73. { pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], },
  74. // bottom
  75. { pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], },
  76. { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], },
  77. { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], },
  78. { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], },
  79. { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], },
  80. { pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], },
  81. ];
  82. ```
  83. We can then translate all of that into 3 parallel arrays
  84. ```js
  85. const positions = [];
  86. const normals = [];
  87. const uvs = [];
  88. for (const vertex of vertices) {
  89. positions.push(...vertex.pos);
  90. normals.push(...vertex.norm);
  91. uvs.push(...vertex.uv);
  92. }
  93. ```
  94. Finally we can create a `BufferGeometry` and then a `BufferAttribute` for each array
  95. and add it to the `BufferGeometry`.
  96. ```js
  97. const geometry = new THREE.BufferGeometry();
  98. const positionNumComponents = 3;
  99. const normalNumComponents = 3;
  100. const uvNumComponents = 2;
  101. geometry.setAttribute(
  102. 'position',
  103. new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  104. geometry.setAttribute(
  105. 'normal',
  106. new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  107. geometry.setAttribute(
  108. 'uv',
  109. new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  110. ```
  111. Note that the names are significant. You must name your attributes the names
  112. that match what three.js expects (unless you are creating a custom shader).
  113. In this case `position`, `normal`, and `uv`. If you want vertex colors then
  114. name your attribute `color`.
  115. Above we created 3 JavaScript native arrays, `positions`, `normals` and `uvs`.
  116. We then convert those into
  117. [TypedArrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
  118. of type `Float32Array`. A `BufferAttribute` requires a TypedArray not a native
  119. array. A `BufferAttribute` also requires you to tell it how many components there
  120. are per vertex. For the positions and normals we have 3 components per vertex,
  121. x, y, and z. For the UVs we have 2, u and v.
  122. {{{example url="../threejs-custom-buffergeometry-cube.html"}}}
  123. That's a lot of data. A small thing we can do is use indices to reference
  124. the vertices. Looking back at our cube data, each face is made from 2 triangles
  125. with 3 vertices each, 6 vertices total, but 2 of those vertices are exactly the same;
  126. The same position, the same normal, and the same uv.
  127. So, we can remove the matching vertices and then
  128. reference them by index. First we remove the matching vertices.
  129. ```js
  130. const vertices = [
  131. // front
  132. { pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], }, // 0
  133. { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, // 1
  134. { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, // 2
  135. -
  136. - { pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], },
  137. - { pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], },
  138. { pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], }, // 3
  139. // right
  140. { pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], }, // 4
  141. { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, // 5
  142. -
  143. - { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], },
  144. - { pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], },
  145. { pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, // 6
  146. { pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], }, // 7
  147. // back
  148. { pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], }, // 8
  149. { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, // 9
  150. -
  151. - { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], },
  152. - { pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], },
  153. { pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, // 10
  154. { pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], }, // 11
  155. // left
  156. { pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], }, // 12
  157. { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, // 13
  158. -
  159. - { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], },
  160. - { pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], },
  161. { pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, // 14
  162. { pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], }, // 15
  163. // top
  164. { pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], }, // 16
  165. { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, // 17
  166. -
  167. - { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], },
  168. - { pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], },
  169. { pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, // 18
  170. { pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], }, // 19
  171. // bottom
  172. { pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], }, // 20
  173. { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, // 21
  174. -
  175. - { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], },
  176. - { pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], },
  177. { pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, // 22
  178. { pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], }, // 23
  179. ];
  180. ```
  181. So now we have 24 unique vertices. Then we specify 36 indices
  182. for the 36 vertices we need drawn to make 12 triangles by calling `BufferGeometry.setIndex` with an array of indices.
  183. ```js
  184. geometry.setAttribute(
  185. 'position',
  186. new THREE.BufferAttribute(positions, positionNumComponents));
  187. geometry.setAttribute(
  188. 'normal',
  189. new THREE.BufferAttribute(normals, normalNumComponents));
  190. geometry.setAttribute(
  191. 'uv',
  192. new THREE.BufferAttribute(uvs, uvNumComponents));
  193. +geometry.setIndex([
  194. + 0, 1, 2, 2, 1, 3, // front
  195. + 4, 5, 6, 6, 5, 7, // right
  196. + 8, 9, 10, 10, 9, 11, // back
  197. + 12, 13, 14, 14, 13, 15, // left
  198. + 16, 17, 18, 18, 17, 19, // top
  199. + 20, 21, 22, 22, 21, 23, // bottom
  200. +]);
  201. ```
  202. {{{example url="../threejs-custom-buffergeometry-cube-indexed.html"}}}
  203. `BufferGeometry` has a [`computeVertexNormals`](BufferGeometry.computeVertexNormals) method for computing normals if you
  204. are not supplying them. Unfortunately,
  205. since positions can not be shared if any other part of a vertex is different,
  206. the results of calling `computeVertexNormals` will generate seams if your
  207. geometry is supposed to connect to itself like a sphere or a cylinder.
  208. <div class="spread">
  209. <div>
  210. <div data-diagram="bufferGeometryCylinder"></div>
  211. </div>
  212. </div>
  213. For the cylinder above the normals were created using `computeVertexNormals`.
  214. If you look closely there is a seam on the cylinder. This is because there
  215. is no way to share the vertices at the start and end of the cylinder since they
  216. require different UVs so the function to compute them has no idea those are
  217. the same vertices to smooth over them. Just a small thing to be aware of.
  218. The solution is to supply your own normals.
  219. We can also use [TypedArrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) from the start instead of native JavaScript arrays.
  220. The disadvantage to TypedArrays is you must specify their size up front. Of
  221. course that's not that large of a burden but with native arrays we can just
  222. `push` values onto them and look at what size they end up by checking their
  223. `length` at the end. With TypedArrays there is no push function so we need
  224. to do our own bookkeeping when adding values to them.
  225. In this example knowing the length up front is pretty easy since we're using
  226. a big block of static data to start.
  227. ```js
  228. -const positions = [];
  229. -const normals = [];
  230. -const uvs = [];
  231. +const numVertices = vertices.length;
  232. +const positionNumComponents = 3;
  233. +const normalNumComponents = 3;
  234. +const uvNumComponents = 2;
  235. +const positions = new Float32Array(numVertices * positionNumComponents);
  236. +const normals = new Float32Array(numVertices * normalNumComponents);
  237. +const uvs = new Float32Array(numVertices * uvNumComponents);
  238. +let posNdx = 0;
  239. +let nrmNdx = 0;
  240. +let uvNdx = 0;
  241. for (const vertex of vertices) {
  242. - positions.push(...vertex.pos);
  243. - normals.push(...vertex.norm);
  244. - uvs.push(...vertex.uv);
  245. + positions.set(vertex.pos, posNdx);
  246. + normals.set(vertex.norm, nrmNdx);
  247. + uvs.set(vertex.uv, uvNdx);
  248. + posNdx += positionNumComponents;
  249. + nrmNdx += normalNumComponents;
  250. + uvNdx += uvNumComponents;
  251. }
  252. geometry.setAttribute(
  253. 'position',
  254. - new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  255. + new THREE.BufferAttribute(positions, positionNumComponents));
  256. geometry.setAttribute(
  257. 'normal',
  258. - new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  259. + new THREE.BufferAttribute(normals, normalNumComponents));
  260. geometry.setAttribute(
  261. 'uv',
  262. - new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  263. + new THREE.BufferAttribute(uvs, uvNumComponents));
  264. geometry.setIndex([
  265. 0, 1, 2, 2, 1, 3, // front
  266. 4, 5, 6, 6, 5, 7, // right
  267. 8, 9, 10, 10, 9, 11, // back
  268. 12, 13, 14, 14, 13, 15, // left
  269. 16, 17, 18, 18, 17, 19, // top
  270. 20, 21, 22, 22, 21, 23, // bottom
  271. ]);
  272. ```
  273. {{{example url="../threejs-custom-buffergeometry-cube-typedarrays.html"}}}
  274. A good reason to use typedarrays is if you want to dynamically update any
  275. part of the vertices.
  276. I couldn't think of a really good example of dynamically updating the vertices
  277. so I decided to make a sphere and move each quad in and out from the center. Hopefully
  278. it's a useful example.
  279. Here's the code to generate positions and indices for a sphere. The code
  280. is sharing vertices within a quad but it's not sharing vertices between
  281. quads because we want to be able to move each quad separately.
  282. Because I'm lazy I used a small hierarchy of 3 `Object3D` objects to compute
  283. sphere points. How this works is explained in [the article on optimizing lots of objects](threejs-optimize-lots-of-objects.html).
  284. ```js
  285. function makeSpherePositions(segmentsAround, segmentsDown) {
  286. const numVertices = segmentsAround * segmentsDown * 6;
  287. const numComponents = 3;
  288. const positions = new Float32Array(numVertices * numComponents);
  289. const indices = [];
  290. const longHelper = new THREE.Object3D();
  291. const latHelper = new THREE.Object3D();
  292. const pointHelper = new THREE.Object3D();
  293. longHelper.add(latHelper);
  294. latHelper.add(pointHelper);
  295. pointHelper.position.z = 1;
  296. const temp = new THREE.Vector3();
  297. function getPoint(lat, long) {
  298. latHelper.rotation.x = lat;
  299. longHelper.rotation.y = long;
  300. longHelper.updateMatrixWorld(true);
  301. return pointHelper.getWorldPosition(temp).toArray();
  302. }
  303. let posNdx = 0;
  304. let ndx = 0;
  305. for (let down = 0; down < segmentsDown; ++down) {
  306. const v0 = down / segmentsDown;
  307. const v1 = (down + 1) / segmentsDown;
  308. const lat0 = (v0 - 0.5) * Math.PI;
  309. const lat1 = (v1 - 0.5) * Math.PI;
  310. for (let across = 0; across < segmentsAround; ++across) {
  311. const u0 = across / segmentsAround;
  312. const u1 = (across + 1) / segmentsAround;
  313. const long0 = u0 * Math.PI * 2;
  314. const long1 = u1 * Math.PI * 2;
  315. positions.set(getPoint(lat0, long0), posNdx); posNdx += numComponents;
  316. positions.set(getPoint(lat1, long0), posNdx); posNdx += numComponents;
  317. positions.set(getPoint(lat0, long1), posNdx); posNdx += numComponents;
  318. positions.set(getPoint(lat1, long1), posNdx); posNdx += numComponents;
  319. indices.push(
  320. ndx, ndx + 1, ndx + 2,
  321. ndx + 2, ndx + 1, ndx + 3,
  322. );
  323. ndx += 4;
  324. }
  325. }
  326. return {positions, indices};
  327. }
  328. ```
  329. We can then call it like this
  330. ```js
  331. const segmentsAround = 24;
  332. const segmentsDown = 16;
  333. const {positions, indices} = makeSpherePositions(segmentsAround, segmentsDown);
  334. ```
  335. Because positions returned are unit sphere positions so they are exactly the same
  336. values we need for normals so we can just duplicated them for the normals.
  337. ```js
  338. const normals = positions.slice();
  339. ```
  340. And then we setup the attributes like before
  341. ```js
  342. const geometry = new THREE.BufferGeometry();
  343. const positionNumComponents = 3;
  344. const normalNumComponents = 3;
  345. +const positionAttribute = new THREE.BufferAttribute(positions, positionNumComponents);
  346. +positionAttribute.setUsage(THREE.DynamicDrawUsage);
  347. geometry.setAttribute(
  348. 'position',
  349. + positionAttribute);
  350. geometry.setAttribute(
  351. 'normal',
  352. new THREE.BufferAttribute(normals, normalNumComponents));
  353. geometry.setIndex(indices);
  354. ```
  355. I've highlighted a few differences. We save a reference to the position attribute.
  356. We also mark it as dynamic. This is a hint to THREE.js that we're going to be changing
  357. the contents of the attribute often.
  358. In our render loop we update the positions based off their normals every frame.
  359. ```js
  360. const temp = new THREE.Vector3();
  361. ...
  362. for (let i = 0; i < positions.length; i += 3) {
  363. const quad = (i / 12 | 0);
  364. const ringId = quad / segmentsAround | 0;
  365. const ringQuadId = quad % segmentsAround;
  366. const ringU = ringQuadId / segmentsAround;
  367. const angle = ringU * Math.PI * 2;
  368. temp.fromArray(normals, i);
  369. temp.multiplyScalar(THREE.MathUtils.lerp(1, 1.4, Math.sin(time + ringId + angle) * .5 + .5));
  370. temp.toArray(positions, i);
  371. }
  372. positionAttribute.needsUpdate = true;
  373. ```
  374. And we set `positionAttribute.needsUpdate` to tell THREE.js to use our changes.
  375. {{{example url="../threejs-custom-buffergeometry-dynamic.html"}}}
  376. I hope these were useful examples of how to use `BufferGeometry` directly to
  377. make your own geometry and how to dynamically update the contents of a
  378. `BufferAttribute`.
  379. <!-- needed in English only to prevent warning from outdated translations -->
  380. <a href="resources/threejs-geometry.svg"></a>
  381. <a href="threejs-custom-geometry.html"></a>
  382. <canvas id="c"></canvas>
  383. <script type="module" src="resources/threejs-custom-buffergeometry.js"></script>