FIELD NOTES

NOTE 01 · BUILDING THE FOREST

Building a forest you can actually verify

How the terrain gets baked, why the far field is handled separately, and why towns turned out harder than battlefields.

What follows is a developer's personal record, including internal milestones, technical decisions and failures. It describes the current state — not a final form, and not a release commitment.

A forest is not finished when it looks good

In this project the world has two identities.

One is the forest you see: light shafts, moss, water, distant canopy. The other is the forest online authority sees: a locked heightfield, a semantic hash, a set of integer coordinates.

They have to stay separate or multiplayer breaks. So every change to the forest starts with one question: which of the two am I touching?


1. Terrain is baked, not arranged in the editor

addons/terrain_pipeline/ is a deterministic offline bake chain. Its output is fixed:

  • a continuous 20×20 m terrain mesh;
  • shared material weights and AO masks;
  • simplified collision;
  • connected navigation;
  • 42 slabs, 38 forest instances, 28 ruin instances;
  • 26/26 Sector MultiMeshes carrying serialised buffers, with all 108 transforms valid and spatially distinct.

Why put those numbers in a document? Because they are the only basis I have for judging whether a change broke something. Terrain is not placed by hand — a script produces it, and the same input has to produce the same result or the whole chain is pointless.

The v1 files are hash-pinned and immutable. Doing v2 means new *_v2.gd files, a new runtime option, its own validator and its own evidence directory. The old set does not move by a byte.


2. The terrain semantic hash, and the firewall around it

Online authority is pinned to the terrain v1 semantic hash c1e82575…, sealed inside the P1 content bundle alongside the bundle hash 09a0de4a…, forest 54ccd449… and town 411b6aaa….

Which means: terrain v2 exists for presentation and P0B offline collision, and cannot change P1 authority. Actually swapping it would be a separately approved content-bundle rebuild, not an incidental upgrade.

The hash is pinned in three places that must agree:

  • the backend table in p0b_combat_slice_v1.gd;
  • p0b_combat_slice_v1_structure_test.gd;
  • scripts/automation/validate_p0b_combat_slice_v1.py.

If they diverge the validator fails outright. I added that constraint because I am certain I will forget one day.


3. The map must not end at the edge

The legal edge of the forest field is 25 m. That is fine for gameplay and bad for the image: you can see the world stop, with nothing behind it.

So I added a presentation-only far field. On the field side it is a 192×192 m opaque ground continuation laid beneath and beyond the locked 64×64 m presentation terrain, plus one bounded transparent forest-horizon arc that follows the production camera focus while the ground continuation stays world-fixed. On the town side it is a curved, mipmapped, transparent forest matte living inside the existing TownPresentation subtree — --p1-town-presentation=off removes the whole layer.

Every far-field mesh disables shadows and GI and uses anisotropic mipmap filtering. They contain no collision, navigation, labels, authority state, protocol state or gameplay effects.

The textures are generated (1024×1024 opaque for the field, 2048×768 with alpha for the town). There is a discipline here I care about: the only positive style input is the hashed world reference image. Runtime captures are byte-archived as camera, scale and map-edge diagnostics — they are explicitly not positive style or palette inputs. It sounds like hair-splitting; it is the difference between a style that holds and a style that slowly drifts.

The town texture's bottom edge has to be fully opaque (0/2048 transparent pixels) and pass a high-saturation magenta spill predicate, because alpha keying loves to leave a coloured fringe — and that particular edge lands right at eye level.


4. Towns are harder than battlefields

A battlefield is bounded, controllable, one problem at a time. A town is not.

In a town, all of this happens at once: several travellers present, display names preserved byte-for-byte (Unicode must not be eaten), chat rendered as literal plain text, canopy fading out when a character walks behind a tree, lighting staying readable, and performance not collapsing.

The canopy fade was the awkward one. The camera occlusion fader has a hard contract with terrain scenes: the Modules subtree must be MultiMeshes, TRANSFORM_3D, colours enabled, no custom data, single-surface StandardMaterial3D. Terrain v2 may change its implementation, but those node names and shapes have to survive — otherwise the fade fails silently. No error; one day a character is simply invisible behind a tree.


5. How I decide the forest is not broken

Not by looking at screenshots. Screenshots lie: depth of field can fail to refresh in a static capture, lighting can shift because of an unrelated default. I run validators and look for the *_OK marker:

  • the startup check, startup_check.sh;
  • the forest field export validator (which binds exact hashes for the far field, tree occlusion and ruin opacity helpers);
  • P1 content identity recomputation, build_p1_content_v1.py --check;
  • the structure tests.

Only when those are green do I go and look at the image. Reversing that order is how you end up accepting a wrong state because the screenshot was pretty.


The forest is still growing. Town detail, far-field layering and edge readability are all still iterating; this note describes how it stands today.