Course outline
BufferGeometry from Scratch
In one line
Under every shape is a BufferGeometry: typed arrays of numbers. You write one triangle by hand, then a square two ways (indexed and not), learn why a triangle can vanish when its corners go the wrong way round, and see what breaks when an attribute is missing or the wrong size.
Think of it like this
A dot-to-dot puzzle. Each dot has a number and a place on the page. The instructions say "join 0 to 1 to 2". The picture is only the dots and the instructions. A BufferGeometry is the dots (the position attribute) and, if you want, the instructions (the index).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- attribute
- One kind of data stored for every vertex: position, normal or uv. Stored in a
BufferAttribute. - itemSize
- How many numbers one vertex takes in an attribute: 3 for a position, 2 for a uv.
- Float32Array
- A JavaScript array of 32-bit decimals in one block of memory. The format the GPU reads.
- winding order
- Whether a triangle's corners go counter-clockwise or clockwise as seen from one side. Counter-clockwise is the front.
- back-face culling
- The GPU skipping triangles whose back faces the camera. It saves work on closed shapes.
Step by step
01Write the corner numbers
Three corners, three numbers each, in one Float32Array. They go counter-clockwise as the camera sees them: bottom left, bottom right, top. Comments at the end of each row say which corner it is: you will want them in a month.
02Make the geometry and the attribute
new THREE.BufferGeometry() starts empty. setAttribute("position", new THREE.BufferAttribute(positions, 3)) adds the corners under the name position, which is the name every built-in material looks for. A different name is simply ignored by the material, and then three.js cannot find the positions at all.
03Go from triangle to square, two ways
The square has four corners. Indexed: list them once, then setIndex([0, 1, 2, 0, 2, 3]). Not indexed: copy the corners out in the order 0, 1, 2, 0, 2, 3 so six vertices are stored. Both draw the same two triangles; only the memory differs.
04Turn the corners round
setIndex([0, 2, 1, 0, 3, 2]) uses the same corners the other way round. The status bar counts all 8 triangles (the four squares have two each), but the white square never appears: the GPU saw its back and skipped it. The green one uses DoubleSide and does.
Try it yourself
- 1
Swap two corners
In the triangle example swap the second and third corners: put
0, 1, 0,on the second line and1, -1, 0,on the third. The triangle disappears, yet the console still says1 triangle, 1 draw call. The corners are now clockwise, so the GPU sees the back and skips it. - 2
Fix the white square
In the square example change the white quad's material from
THREE.FrontSidetoTHREE.DoubleSide. The white square now appears like the others. The count of triangles counted does not change (8): back-face culling only decided whether the pixels were drawn.
Code & diagrams
The triangle swings a little to show it is a real 3D object. Open the scene tree: one Mesh, no children.
Console
Run the scene to see its console.Expected console output
array length: 9 | itemSize: 3 | vertices: 3
second vertex: 1 -1 0
index: null | triangles drawn: 1
status bar says: 1 triangle, 1 draw callFour quads in a row. From the left: indexed (orange), not indexed (blue), backwards winding (white: missing), backwards with DoubleSide (green).
Console
Run the scene to see its console.Expected console output
indexed 4 vertices, 2 triangles, 48 bytes of positions
not indexed 6 vertices, 2 triangles, 72 bytes of positions
backwards winding 4 vertices, 2 triangles, 48 bytes of positions
backwards, DoubleSide 4 vertices, 2 triangles, 48 bytes of positions
triangles counted: 8 (the backwards one is sent to the GPU, then skipped)Four mistakes, none of which stops the page (case 4 throws, and is caught). Case 1 draws nothing. Case 2 is not drawn on purpose: three.js would log a long warning with an object dump. Case 3 reads past the last vertex: what you see is up to the browser and driver. Here a triangle with a made-up third corner is drawn; elsewhere it may be skipped with a WebGL warning.
Console
Run the scene to see its console.Expected console output
1. no position attribute
2. itemSize 2 -> vertices: 4.5
3. index 5 with only 3 vertices
4. THREE.BufferAttribute: array should be a Typed Array.
triangles counted: 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A plain array
In the triangle example change new Float32Array([ to ([ (and keep the closing ])).
Break #2
A typo in the attribute name
In the triangle example change setAttribute("position" to setAttribute("positions".
Myth vs fact
Myth
If the triangle count says 1, the triangle is on screen.
Fact
renderer.info counts triangles submitted. A triangle with the wrong winding, behind the camera or outside the screen is counted and not drawn.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Winding is decided in screen space, after the projection: a triangle seen from the front is counter-clockwise on screen. Mirroring an object with a negative scale (
scale.x = -1) flips the winding, and three.js compensates by flipping the front-face rule for that object. A hand-made mesh with the wrong winding will be inside out in the same way. - ▸
setIndexwith a normal array choosesUint16Arraywhen every number is below 65,535, otherwiseUint32Array. A 16-bit index is half the memory. If you build an index yourself for a large mesh, pick the type explicitly.
Remember this
- 1
The problem this solves: built-in shapes cover boxes and balls. A customer's bike will also need a ribbon-shaped chain guard, a map, a graph of data, a model loaded from a file. All of them are the same thing under the hood, and when one looks wrong you must be able to read its numbers. Writing one triangle yourself is the quickest way to learn them.
- 2
Attributes. An attribute is one list of data with one entry per vertex.
positionhas 3 numbers per vertex (x, y, z);normalhas 3 anduvhas 2. The numbers sit in a flat typed array, for examplenew Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0]), andnew THREE.BufferAttribute(array, itemSize)says how many numbers belong to one vertex.countisarray.length / itemSize. - 3
Not indexed. With no index, every three vertices in a row are one triangle. One triangle needs 3 vertices; a square needs 6, because two corners are listed twice.
- 4
Indexed. With an index (
geometry.setIndex([0, 1, 2, 0, 2, 3])) the vertices are listed once and the index names them in threes. A square is 4 vertices (48 bytes of positions) and 6 index numbers instead of 6 vertices (72 bytes). The saving grows fast on real models, where each vertex is shared by about six triangles. The cost: a shared vertex has one normal and one uv, which matters in Topic 4.4. - 5
Winding and back faces. A triangle has a front and a back. The front is the side where the corners go counter-clockwise as you see them. By default (
side: THREE.FrontSide) the GPU skips triangles seen from behind, which is called back-face culling. List the corners the other way and the triangle is still sent (the status bar still counts it) but never drawn.side: THREE.DoubleSidedraws both sides at twice the pixel work.
Explain it without notes
What does new THREE.BufferAttribute(array, 3) tell three.js, and what happens to count if you pass 2 instead?
Why can a triangle that is counted by the status bar still be invisible?
Practice
Make a triangle in the xz plane lying flat (corners at (−1, 0, 1), (1, 0, 1), (0, 0, −1)) that is visible from above. Which order of corners must you use?
Write an indexed quad with a uv attribute that shows the whole picture, and print uv.count and position.count.
Done when you can
Write a triangle's positions as a Float32Array and add them to a BufferGeometry
Turn a non-indexed quad into an indexed one and compare the memory
Say what winding order is and fix a triangle that disappeared