Mesh growth
MeshGrowth grows a surface that makes more of itself than it has room for. It takes a mesh, pushes its vertices apart, and keeps the triangles a fixed size by splitting the ones that stretch. The surface gains area, and that area has nowhere to go, so the surface folds.
// Held across frames, seeded once:
let coral = MeshGrowth(mesh: .icosphere(subdivisions: 3),
driver: .chemical(.coral), seed: 7)
// In draw():
coral.step()
material(.dielectric(roughness: 0.4))
drawMesh(coral.mesh)
That is the whole idea, and everything that looks designed about the result comes out of it. The forms it makes are the ones growing things arrive at, such as brain coral, a ruffled leaf margin, a lettuce edge, or something branching.
Contents
- Why it folds
- What drives the growth
- Reaction-diffusion on the surface
- The parameters that matter
- Open surfaces
- Cost and reproducibility
Why it folds
One point explains every result: nothing pushes the surface outward. The only forces run along the mesh's own edges, which lie in the surface. So the form cannot take up new area by inflating the way a balloon does. It has to absorb that area by bending instead, and bending is folding.
Because of that, every driver folds, including the perfectly even one. A driver changes only where the folds appear and how large they are. Growing evenly folds evenly all over at one size, which is the brain-coral look. Growing in patches folds into lobes with smooth ground between them, which is the branching look.
Two other parts of the model set how the folds behave:
- Self-avoidance. Parts of the surface that come near each other, without being near along the surface, push apart. A fold then stacks against its neighbor instead of passing through it.
- Bending resistance. A sheet with no stiffness buckles at the smallest scale available. The folds then come out the size of a triangle, and the form reads as crumpled paper. Resisting bending makes small wrinkles expensive, so the same growth gathers into broader waves. That resistance is the
stiffnessparameter, and it sets the difference between crumpled and ruffled.
What drives the growth
| Driver | Grows | Reads as |
|---|---|---|
.uniform | everywhere equally | even folds all over at one size: brain coral |
.curvature | where the surface already bulges | lobes that sharpen, because a bulge that grows becomes a bigger bulge |
.field { position, normal in … } | wherever your rule says, on a scale of 0 to 1 | whatever you describe, such as a rim, a band, or a noise field |
.chemical(SurfaceChemistry) | where a reaction-diffusion pattern collects | branching coral |
MeshGrowth(mesh: .icosphere(subdivisions: 3), driver: .uniform)
MeshGrowth(mesh: .icosphere(subdivisions: 3), driver: .curvature)
MeshGrowth(mesh: .icosphere(subdivisions: 3), driver: .chemical(.coral))
// Grow only near the equator, the way a leaf grows at its margin.
MeshGrowth(mesh: .icosphere(subdivisions: 3),
driver: .field { position, _ in
1 - smoothstep(0.05, 0.45, abs(position.y))
})
.uniform is the baseline to start from, because it shows what growth alone produces before any pattern steers it.
Reaction-diffusion on the surface
.chemical is the driver that combines two processes. A Gray-Scott reaction runs in the surface while the surface grows, and the surface grows where the reaction has collected. So the pattern decides where to add area, and the new area gives the pattern more room to spread into.
SurfaceChemistry carries the reaction's settings. The most used corners of the parameter space arrive as presets: .coral, the default, plus .spots, .maze, .mitosis, and .worms. Its feed and kill are the same two numbers that the texture-space Sim.reactionDiffusion takes. A pairing that gives a look there gives the same look here.
MeshReactionDiffusion runs the same reaction on a fixed mesh, which is the surface-texture use rather than the growth one. The pattern develops in the mesh itself, so it has no seam, no stretching, and it needs no texture coordinates.
let pattern = MeshReactionDiffusion(mesh: .icosphere(subdivisions: 5),
chemistry: .coral, seed: 7)
pattern.step(12)
drawMesh(pattern.displaced(by: 0.06)) // the pattern as relief
values holds the concentration at each vertex, in the same order as mesh.positions, so you can drive anything with it. spread and secondSpread are how fast the two chemicals travel, and keeping the second well under the first is what makes a pattern form at all, since the pattern's scale comes out of the gap between them. displaced(by:) is the quick way to see it.
Two things about the pace of a chemical growth are worth knowing.
The reaction has to organize into a pattern before it can steer anything, so noise steers most of the opening steps of a chemical growth. Setting settleSteps runs that opening chemistry all at once on the first step. The surface is refined to its target triangle size and held still while the pattern organizes on it. The first folds then land where the pattern says, so chemistry reads as a formed pattern from step one.
The pattern also covers only part of the surface at any time. So at the same growthRate, a chemical growth makes area a few times slower than .uniform. Allow more steps, or raise growthRate. This is a permanent character of the driver rather than something that settling changes.
Both MeshGrowth and MeshReactionDiffusion weld coincident vertices first. Ollin's mesh generators emit flat-shaded geometry whose triangles share no vertices, so an icosphere is thousands of loose triangles by index. Without welding, nothing can spread across it at all.
The parameters that matter
| Parameter | Does | Notes |
|---|---|---|
edgeLength | the triangle size the remesher holds | sets the finest fold the surface can hold, and it is where the cost lives |
growthRate | how fast area is made | at 0 the surface only relaxes |
stiffness | resistance to bending | sets the fold size: raise it for smooth open ruffles, lower it toward 0 for crumpled |
repulsionRadius | how near separate parts may come | past about 3 × edgeLength it starts pushing on neighbors that are properly that close, so the form inflates instead of folding |
maxVertices | the ceiling on the vertex count | growth stops there and the surface only relaxes, so this sets how far a form develops |
settleSteps | opening chemistry run all at once on the first step | .chemical only. The pattern arrives formed instead of organizing during the growth |
Three more set how firmly the surface holds itself together while it grows: springStrength is how hard an edge pulls back toward the length it wants, repulsion how hard unconnected parts push each other apart, and relaxation how much a vertex is evened out against its neighbors, kept in the surface so spacing evens without the shape smoothing away. chemistrySteps is how many chemistry steps run per growth step under .chemical, since the pattern has to develop faster than the surface moves or it never gets ahead of the growth it is steering.
As it runs, the surface reports on itself: vertexCount is how many vertices it holds, stepCount how many steps have run, and growthField the growth rate at each vertex as it stood on the last step, 0 to 1 and empty before the first step. A MeshReactionDiffusion keeps its two chemicals the same way, values and substrate, both lined up with mesh.positions, and seedPatches(count:radius:) lights the pattern with the standard seed, half substrate and a quarter reagent rather than pure reagent.
edgeLength defaults to the mean edge length of the mesh you start from. So a coarse starting mesh grows coarse folds, and a fine one grows fine folds. Passing a starting mesh is usually all the setup you need.
Open surfaces
A closed surface, such as a sphere or a torus, grows into a solid form. An open surface keeps its rim, whether it is a plane, a disc, or a ribbon. The remesher refuses to collapse or flip anything that touches the boundary, and a rim vertex slides along the rim rather than into the surface. So a sheet stays a sheet with its edge intact, and it ruffles along that edge, which is the leaf-margin case.
Cost and reproducibility
Growth is deliberately slow, so a form takes hundreds of steps to develop. Stepping once or twice a frame is the usual rate, and watching it develop is part of the point.
The work per step rises with the vertex count. It is CPU mesh processing, with a full remeshing pass every step, so maxVertices is the control that keeps a sketch interactive. A few thousand vertices step comfortably in real time. Nine thousand vertices cost roughly 30 ms a step on an M2, so run one step a frame rather than several.
A seeded growth reproduces exactly. The same seed grows the same form, because every pass runs in a fixed order rather than in a dictionary's order. The small amount of randomness only breaks the starting symmetry. A perfectly symmetric surface has no reason to buckle one way rather than another, which is why the seed exists at all.
See also
Subdivision surfaces- refine the grown mesh into a smoother surfaceIsosurfaces- the other way to arrive at an organicMeshDifferential growth- the same idea one dimension down, on a line3D- materials, lighting, and shadows for the result