Course outline
Editing Geometry
In one line
A geometry is not frozen. You can change vertices every frame (and must tell the GPU with needsUpdate), draw only part of a shape with setDrawRange, and merge many shapes into one so the GPU draws them in a single call.
Think of it like this
A puppeteer moves the strings, and the audience sees the puppet move only because the stage lights are on it. Editing a vertex array is moving the strings. needsUpdate is telling the stage manager "the puppet moved, light it again". If you forget, the puppet moves backstage and the audience sees nothing change.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- needsUpdate
- A flag on an attribute (or texture). Setting it to true makes the renderer upload the changed data to the GPU on the next draw.
- version
- A number on an attribute that goes up each time
needsUpdateis set. The renderer compares it with the version it last uploaded. - draw range
- The slice of a geometry that is drawn, set with
setDrawRange(start, count). - mergeGeometries
- A helper that joins several geometries with the same attributes into one geometry, so they are drawn in one call.
Step by step
01Move every vertex
wave(seconds) sets each vertex's height from two sine and cosine waves of its x and z. position.setY(i, ...) writes into the array. Nothing has reached the GPU yet.
02Tell the GPU
position.needsUpdate = true raises the version. computeVertexNormals() recomputes the arrows for lighting and flags them too. The console shows the version go from 1 to 4 after three edits.
03Draw part of a ring
The torus has 3,456 index entries (1,152 triangles). setDrawRange(0, count) with a multiple of three draws that many entries. Rendering once per setting and reading renderer.info.render.triangles gives 1,152, 576, 288 and 0.
04Merge twelve spokes
Each spoke is a thin cylinder moved so one end is at the origin and then fanned round with rotateZ. Twelve meshes make twelve draw calls. mergeGeometries of the same twelve geometries makes one geometry and one draw call. The two attribute mismatches at the end return null and log an error.
Try it yourself
- 1
A hundred and twenty spokes
In the merge example change both
12inspokeGeometries(i < 12andi / 12) to120. The console says120 draw calls, 2880 triangles(the label still says 12) and, after the merge,1 draw call, 2880 triangles: the same triangles in 119 fewer draw calls. - 2
Start the draw range in the middle
In the draw-range example change
geometry.setDrawRange(0, count);insideshowtogeometry.setDrawRange(1500, count);. The100%line now says652 triangles drawn: the slice starts at entry 1500 and stops at the end of the 3,456 entries (1,956 entries). The50%line still says 576.
Code & diagrams
Every frame edits all 625 vertices. In the status bar the draw call count is 1. Set `SET_NEEDS_UPDATE` to false and run again.
Console
Run the scene to see its console.Expected console output
vertices: 625 | numbers uploaded each time: 1875
version before: 1
version after 3 edits: 4The ring grows and shrinks by changing only the draw range. Watch the triangle count in the status bar go up and down while the geometry count stays 1.
Console
Run the scene to see its console.Expected console output
index entries: 3456 = triangles: 1152
100% -> 1152 triangles drawn
50% -> 576 triangles drawn
25% -> 288 triangles drawn
0% -> 0 triangles drawn
count 10 -> 3.3333333333333335 triangles countedThe wheel on the right is the merged geometry (spokes only, no rim). The two [error] lines are what mergeGeometries really logs.
Console
Run the scene to see its console.Expected console output
12 meshes: 12 draw calls, 288 triangles
1 merged: 1 draw call, 288 triangles
THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index 1. Make sure all geometries have the same number of attributes.
different attributes -> null
THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index 1. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.
indexed + not indexed -> null
merged geometry: 480 verticesBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget needsUpdate
In the cloth example set SET_NEEDS_UPDATE to false.
Myth vs fact
Myth
Changing position.array is enough to move a mesh's vertices on screen.
Fact
It changes only your copy. The GPU copy is replaced when you set needsUpdate = true (or never, if you forget).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Each
needsUpdatere-uploads the whole array by default.attribute.addUpdateRange(start, count)(in numbers, not vertices) limits it to a slice, andattribute.setUsage(THREE.DynamicDrawUsage)hints that the data changes often so the driver can place it in suitable memory. Do both for a large mesh that you edit every frame. - ▸
Many identical objects (a hundred spokes, a thousand trees) are better as an
InstancedMesh, which draws them in one call and still lets each instance move. Merging is right for different shapes that stay put; instancing is right for the same shape in many places (Phase 17).
Remember this
- 1
The problem this solves: waving cloth, a stretching rubber band, a growing line, a ring that fills up like a progress bar, a hundred small parts that should be one: all need geometry that changes, or geometry that is joined. Knowing the three tools (
needsUpdate,setDrawRange,mergeGeometries) covers almost every case. - 2
Two copies of every array. Your
Float32Arraylives in JavaScript memory. The GPU has its own copy in its own memory, made the first time the mesh is drawn. Changing a number in your array changes only your copy. Settingattribute.needsUpdate = trueraises a version number; at the next draw the renderer sees the new version and uploads the whole array again. - 3
What it costs. Uploading is a copy from the CPU to the GPU. The cloth's 625 vertices are 1,875 numbers, which is tiny. A million-vertex mesh edited every frame would copy about 12 MB sixty times a second, which is far too much for a phone. For big arrays, edit only what changed (
attribute.addUpdateRange(start, count)marks a part) or move the work to a shader (Phase 12). - 4
**
setDrawRange(start, count).** It tells the GPU to draw only a slice of the geometry: with an index it counts index entries (three per triangle), without one it counts vertices. The ring in the second example draws 100%, 50%, 25% and 0% of its 1,152 triangles without changing a single vertex. It is a cheap way to grow a line, fill a progress ring or reveal a model. - 5
Merging.
mergeGeometries([g1, g2, ...])fromthree/addons/utils/BufferGeometryUtils.jsjoins geometries into one. Twelve spoke meshes are twelve draw calls; merged they are one, with the same 288 triangles. The cost is independence: the merged result moves, hides and is clicked as a single object. Bake each part's position into its geometry first (geometry.translate,rotateZ,applyMatrix4) because a merged geometry has no per-part transform.
Explain it without notes
Why must you set needsUpdate = true after editing a position array?
When would you use setDrawRange and when mergeGeometries?
Practice
Make the cloth's wave stop when the user prefers reduced motion. Which line changes?
Write a function that grows a polyline: it keeps a big Float32Array for 1,000 points and draws only the first n.
Done when you can
Edit a position array and make the change reach the screen
Explain what
versionandneedsUpdatedoDraw part of a geometry with
setDrawRange