Birdoggydog's Builds

Generating rocks and trees in Blender to a scatter budget

A Godot trial that places 40,000 stand-ins to price each class of piece, then basalt from joint planes, arches as spans, and tree crowns of alpha-tested cards.

Blobber’s model kit can now make rocks and plants from a rule and a few numbers, to triangle budgets that a scatter trial on the 10 km island measured first. The trial, the rock kit and the flora kit were built on 9 October, and the rocks’ rough surface on the 10th. None of the pieces is in the game yet. The work is done by AI coding agents that I direct.

What can a rock or a tree cost on a 10 km island?

A trial scatters stand-in rocks, trees, shrubs, ground clutter and huts over the island as the party moves, by rule, with nothing saved. Its output is a table of what each class of piece may cost.

Twelve stills from the game. A bare red hillside, then the same hillside covered in blocky stand-in trees and boulders, the stand-ins tinted green, yellow and orange by level of detail, and the island from the air with and without far trees.
The trial off and on, the levels of detail in colour, and the far ring from the air.

I wanted the budgets known before any generator was written to the wrong one. The stand-ins are made in script at honest costs: a 300 triangle rock, a 1,204 triangle tree, a 5,016 triangle hut with a doorway.

Placement is a pure function of place and world seed, so the same rock is in the same place on every visit. Sites lie on lattices: structures every 48 m, trees and rocks together every 6 m, shrubs every 3 m, clutter every 1.5 m. Whether a site holds a piece, and which, comes from a table by biome, slope, how much meat is on the ground, and patches of noise.

Cells come in four rings:

CellOut toHolds
32 m72 mclutter, near shrubs
64 m128 mnear trees and rocks, everything solid
128 m288 m
512 m (a terrain region)1,536 mfar stand-ins

A cell is worked out on a worker thread, four at a time, and stood up on the main thread inside 1.5 ms a frame. Drawing is one MultiMesh for each cell, variant and level. One node a piece was thrown out at once: 18,609 nodes took 115 ms to make and 52 ms to free.

Godot’s automatic levels of detail do apply to a MultiMesh, but as one level for the whole batch. Hiding whole cells by distance swaps 64 to 512 m of forest at once. Levels are swapped piece by piece in the vertex shader (scatter_piece.gdshader).

Collision is one physics-server body for a whole 64 m cell, only within 40 m of the party and under monsters: five or six bodies for 65 to 113 pieces. Trunks, rocks and hut walls block. Shrubs, clutter and doorways don’t. Blocking pieces are kept at least 2.4 m apart, the widest monster plus 0.4 m.

My machine (an RTX 4080 SUPER at 1280 by 720) can’t find its own limit. The island without scatter is 1.4 ms a frame. At four times the trial’s density, 127,000 pieces and 8.5 million triangles in the frame, it is 1.9 ms. So the budget comes from unit costs (a draw call about 1.1 microseconds, a million triangles 0.11 ms on the video card) and one assumption I accepted: the weakest machine the game is for is 8 times slower, and scatter may have a quarter of its frame.

ClassNearMidFar
Clutter30
Shrub15040
Rock30070
Tree1,20030022
Building5,00060060

Three variants of a class at once, two for buildings. Draw calls run out before triangles do. Each generated piece ships one JSON file with its footprint, pivot, collision shapes, levels of detail and how far it may be sunk.

The film’s first recording used F6, F7 and F8 as its switches. Those are the game’s save keys, and it overwrote three of my save slots. The switches moved to F10 to F12.

Making rocks from how their stone breaks

tools/blender/lib/rock_kit.py takes a stone family and a few numbers and makes a boulder, a litter stone, a cluster of basalt columns, an outcrop, a spire, an arch or a stack.

Fourteen rocks on grass: three small dark clinker stones, two basalt boulders, a cluster of six-sided columns, a low outcrop, a spire, a rim block, a ledge, an arch of stacked blocks, a spire split by red flesh, and a pink granite boulder and tor.
The first pass's fourteen pieces, black basalt and a pink granite.

A generator writes one line and calls build(), finish() and export():

Rock(kit, spec(piece="boulder", family="plug_basalt", size=(2.2, 1.9, 1.8), seed=3))

A family is a row in a table: bed direction and thicknesses, joint sets with directions and spacings, a pattern (columns), weathering, colours. Every piece of a family takes its planes from that row, turned as the piece is turned, so twenty pieces read as one stone.

The body is one surface over a distance field: an envelope of melted masses, cut by the family’s planes and stepped bed by bed and block by block. Each cell of the joint lattice sets its faces in or out by its own amount, which is what a ledge is. The skin is a ball of quads, or rings along an axis for a spire or an arch, shot outward by rays to the field’s surface.

The baseline was a ball pushed about by noise. Planes beat it at 12 m as well as close, because one face is lit and the next is dark.

The first attempt at planes failed. Rays never land exactly on an edge, so every edge of the rock came out as a sawtooth across the mesh. The fix is a snap: after the skin is shot on, each point is moved to where the planes its neighbours lie on meet, so an edge of the rock is an edge of the mesh.

Joints cut as grooves were thinner than the ray spacing. Cracks as paint were gone by 5 m, so steps in the surface carry a joint instead. Godot’s automatic levels of detail stopped at 541 triangles when 400 were wanted. Level 2 and the far stand-in are made from level 1’s own mesh after the bake, so all three share one texture.

Every piece carries its own buried foot. One that stands plumb continues below its ground line as a flared skirt deep enough for a 30 degree slope. One that lies along the slope is a whole closed body sunk part way, so it can be tipped onto another face. One boulder turned and tipped passes for six.

I’m a huge fan of the stones. A boulder is 300 triangles near and 70 at the middle level, and a column cluster 1,081, 400 and 48.

Why did the arch look like masonry?

The first arch was too small, and one side looked too stacked for eroded rock.

Six dark stone arches, each with a small figure for scale, and each again seen along a path: a low bridge, a tall eye, a sea arch with one foot in water, a broken span with fallen blocks, a leaning buttress and a standing stack.
The second pass's six arches, with a figure for scale.

The stepping that makes ledges on a boulder makes courses on a leg. The second pass has a new kind of piece, a span: one worn body with a hole through it, its beds running level through the whole arch instead of round it. The beds were made 2.6 times thicker after a first try still read as a layer cake.

Each of the six is 1,454 to 1,500 triangles and passes a body 2.4 m wide and 2.6 m tall. The first arch doesn’t. The bridge is 9.4 by 4.3 m clear, and the sea arch 11.4 by 6.0.

The same pass tested variation. Fourteen boulders in a field were built from one piece, from three and from fourteen. Three pieces of different proportion (squat, tall, slabby) read as a real field, and fourteen adds little.

Why were the rocks so smooth?

The smoothness was in the textures. The colour map was nearly flat and the normal map leaned the surface about 5 degrees, and removing it changed nothing.

One boulder was pictured with each suspect removed. Roughness, specular and shading were ruled out. The fix is paint and baked relief on the same mesh, in three strengths. Grain in the colour survives to 12 m in both lights. Relief reads to 5 m, and at 12 m only under a lamp. Variation in roughness isn’t seen at any distance. Corners chipped in the mesh weakened the lit face at 12 m, so they were rejected.

On 10 October I picked the roughest of the three. A surface is five numbers from 0 to 1: relief, grain, vary, edges and fracture. rough has the first four at 1.0 and fracture at 0.3, because fracture at 1.0 reads as a drawn net at every distance. It’s baked into the texture and never moves a vertex. All 26 members were rebuilt and every slimmed mesh came out the same bytes. The textures grew 11%, from 14.0 to 15.5 MB.

A small boulder and three clinker stones, each before and after, by day and under a lamp. Before, the small stones carry a few large craters. After, they carry many fine pits.
Left of each pair: pits spaced in metres. Right: pits spaced by the size of the piece.

Pits read as craters on the smallest pieces, because their spacing was the stone family’s alone: 5.5 cm for basalt, 4.0 cm for granite. The cell is now the family’s times the piece’s middle dimension over 1.3 m, never more than the family’s, and never under three texels of the piece’s texture. A pit is about 0.84 of a cell rim to rim and needs a dark core of one texel to read. The middle of the three dimensions was chosen so that a long thin piece keeps the family’s grain. The three clinkers (0.2 to 0.4 m) went from 4 or 5 pits across to 18 to 20. Of the pieces the rule shouldn’t touch, 43 of 43 rebuilt to the same bytes.

The slab, 4.84 by 0.23 by 3.28 m, got one box of collision. Walked at, it stops the party, and jumped onto, it is stood on. The party can’t step up even 23 cm yet.

Why did the trees look like reskinned rocks?

Tree crowns are now leaf cards with holes in the alpha, which is what the first two passes were built to avoid.

One tree three ways, at several distances and from below and above. Left, a crown of big green lumps. Middle, a crown of many small cut-out leaves. Right, a crown of sparse flat shards.
Opaque lobes, alpha-tested leaf cards, and leaf shapes as plain geometry. I chose the middle one.

Growth is space colonisation: points are scattered in an envelope and a skeleton grows toward them, each point claimed by the nearest twig. Limb radii follow the pipe model, so a limb is as thick as the limbs it feeds. A trunk has eight sides.

The first pass made crowns as opaque lobes. The trunks and boughs were great, the lobes looked bad, and the flowers, shrubs and hedges mostly looked like reskinned rocks. The budget says draw calls are the limit and a second material doubles them, so the second pass built leaf shapes as real triangles. That gave sparse shards with boxes showing through.

The third pass uses one alpha-tested material for all flora. The picture is 1024 px square. Its lower half is sixteen rows, one for each surface (bark, dead wood, flesh and so on): across a row is the pattern, up a row is light. A vertex’s height in its row is its occlusion, computed by rays, so there is no bake and a plant builds in about three seconds. The upper half is 8 by 4 tiles of leaf cards. The material exports as glTF MASK with a cut-off of 0.5, and Godot imports it as alpha scissor. Wood sits in the opaque half, so there is still one material and one draw call a piece.

Twelve plants on grass: a leaning tree, its bare dead form, a broad tree, an orchard tree with orange fruit, a flowering shrub, a scrub bush, three leafy hedge pieces, a tussock, a fern and a drift of yellow flowers.
The third pass's plants.

A crown is cards hung on the twig ends, with three crossed dark cards in each group of twigs in place of a closed inner shape. On one tree, 95 cards look thin, 190 full and 300 little better. Levels of detail are rebuilt from the same skeleton with things left out, down to stand-ins of 22 and 10 triangles. The broad tree is 1,183, 283, 22 and 10.

Nobody knew what the alpha cost, so it was measured in Godot with shadows on, over 802 near trees, 3,058 middle and 32,277 far. Opaque lobes took 1.59 ms on the video card. Cards, alpha-tested, took 3.55. The same cards drawn solid took 1.91. Draw calls didn’t change. The test itself is the cost, mostly in the shadow passes. The bench was a flat plane and placed more trees than the scatter’s spacing allows.

The meat layer is one number on any plant. Stage 1 is a yellowed quarter of the crown and a red foot. Stage 2 adds growths. Stage 3 has three variants (spined, thorn, husk) and I haven’t picked one. The hedge’s top is still blotchy from above.

Next

Nothing has been seen in the game or moving in wind. Every mesh carries a sway and a flutter amount in its second UV set, and nothing reads it yet.