Open Asterune
Terrain surfaces and chunk updates

Terrain surfaces and chunk updates

Trace voxel storage, Surface Nets meshing, material blending, render LOD, worker publication, and collision cache updates.

Asterune derives terrain from a sparse occupancy grid. Surface Nets extracts the solid boundary from that grid for rendering and collision. Water uses a separate surface representation. Editing changes voxel data first; revision tracking then identifies the meshes, colliders, and neighboring boundaries that need rebuilding.

Render detail and physical detail have separate lifetimes. Visible terrain meshes refresh on background workers and change level with distance. World collision rebuilds from full-detail grid samples when its cache is requested. A visible mesh is therefore a derived resource rather than the authoritative terrain state.

Sparse cells and shared snapshots

A cell spans 4 studs on each axis and stores a terrain material plus an 8-bit occupancy value. Occupancy ranges from 0 through 255. Its fractional value is occupancy / 255; air or zero occupancy canonicalizes to one empty-cell value. Solid and water sampling read separate fractions from that same material record.

Cells are grouped into 32 × 32 × 32 chunks, each spanning 128 studs. Only written chunks occupy the sparse grid map. An unwritten cell reads as empty. Cell arrays are shared through reference-counted storage, so a snapshot can share unchanged chunks with an editor, renderer, or worker.

A write copies shared chunk storage before mutation. Region edits first compute which cells actually differ, then apply those writes together, stamp the chunk, and record their local bounds. An unchanged edit does not create a new chunk revision. An emptied chunk is removed and recorded in the removal log.

Grid generation and revision answer different questions. Revision orders edits within one grid. A cloned grid has a fresh generation even when it shares every chunk, preventing a reader from interpreting revisions from a different grid as its own continuation. Selective worker snapshots retain only requested chunks and their required neighbors rather than an entire large terrain.

Change tracking and neighbor invalidation

Each reader remembers a generation and revision. The grid returns chunks changed or removed since that point. A generation mismatch, a revision newer than the grid, or a revision older than retained removal history requests a full refresh. This fallback prevents missed deletions when a reader has fallen too far behind.

A chunk retains the bounds of its 6 most recent edits. A reader can combine those bounds to distinguish an interior edit from an edit near a boundary. If the required history has expired, the entire chunk becomes the conservative changed extent.

Mesh invalidation includes neighboring chunks only where the sampled border can reach an edit. Full-detail meshing reads 2 cells beyond a chunk; each coarser level doubles that reach. An edit close to a face can invalidate its neighbor, and an edit close to several boundaries can invalidate diagonal neighbors too. Removing a chunk conservatively invalidates its surrounding boundaries.

A chunk owns surface edges whose lower sample lies inside that chunk. It still reads neighboring samples to construct vertices around those edges. This rule avoids two chunks independently owning the same quad while keeping equal-detail vertices aligned across their shared boundary.

Surface Nets extraction

The solid surface is the 0.5 crossing of solid occupancy sampled at cell centers. For each cube of 8 samples, the mesher classifies its corners as above or below that threshold. A cube entirely inside or entirely outside contributes no vertex. A cube that straddles the surface contributes one vertex.

For each of that cube's 12 edges that crosses the threshold, linear interpolation finds the crossing point. The vertex is the average of those crossings. This places a surface vertex from the local occupancy field rather than selecting one fixed corner or emitting all the faces of occupied cells.

A sample edge that crosses the threshold connects the vertices of its 4 surrounding cubes into a quad. Winding follows which side is solid, and each quad becomes 2 triangles. Surface normals come from central differences of the occupancy field, interpolated at the vertex, then negated and normalized to point out of the solid.

A chunk reads a padded field before extraction. The padding supplies both the neighboring cubes required by boundary quads and samples for central-difference normals. A chunk with no written cells can still own surface geometry generated from a written neighbor, so meshing candidates include neighboring coordinates.

Material sets on surface vertices

The mesher accumulates solid-material contributions from a vertex's 8 samples. A quad selects up to 4 materials from the combined contributions of its vertices. All vertices on that quad use the same sorted material set, allowing the shader to treat material indices as constant across each triangle.

Each vertex then normalizes its weights for that set into 8-bit values summing to 255. The largest contribution receives the rounding remainder. The renderer can interpolate the weights while retaining a stable interpretation of their material slots; interpolating unrelated material IDs would not produce a valid surface mixture.

At coarser detail levels, a sample averages solid and water occupancy over its fine-cell block. Material selection gives exposed solid cells a greater vote than buried cells, preserving the visible surface material when a coarse sample contains several layers. Surface material state can therefore change without changing the underlying geometry ownership rule.

Distance detail and residency

Terrain supports 4 meshing levels: 32, 16, 8, and 4 samples per chunk axis. The renderer chooses among them using distance from the camera to a chunk's bounding sphere. The nominal thresholds are 320, 720, and 1,500 studs. A 48-stud hysteresis offset depends on the existing level, reducing repeated rebuilding when the camera moves around a boundary.

Only a bounded number of detail transitions enter each scan: up to 48, ordered nearest first. Edited chunks and previously missing meshes also create work. The renderer continues checking while transitions remain beyond that budget, instead of treating an unfinished scan as a settled view.

Candidate scanning reacts to grid changes, changed draw distance, and camera movement of at least 8 studs since the last settled scan. Meshing preloads one chunk beyond the visible draw distance and retains another chunk beyond that. This retention band reduces allocation churn when the camera reverses near the visibility limit. An edited mesh outside preload range is discarded even when it lies inside the retention band, so it cannot reappear as stale terrain.

Coarse meshes add decorative skirts along open border edges. These extend beneath the surface to hide cracks between neighboring detail levels. Skirts belong only to the render mesh; they do not create invisible collision walls. The editor's CPU picking copy also omits those skirts.

Background meshing and stale results

Studio and gameplay both use bounded background meshing. The editor additionally retains CPU geometry for picking against the displayed surface; gameplay omits that copy. Worker jobs operate on shared chunk snapshots, leaving the live grid available for later edits.

Each pending chunk request carries a desired detail level and a serial number. A newer edit or changed request replaces that serial. When a result returns, the renderer uploads it only if its serial still matches the pending request. A worker completing an older request therefore cannot overwrite a more recent edit.

The queue prioritizes chunks near the camera and starts at most 32 chunks in a worker batch. Completed results wait in a bounded publication path. A refresh processes up to 32 results and stops after the elapsed upload time reaches 2 ms, checked between results. That budget bounds the burst of uploads; one individual upload can finish after the budget has elapsed.

A pure solid-LOD request preserves water dirtied by an earlier pending edit. Otherwise, replacing the request could clear the water work before its result was published. Empty mesh results also retain their chosen level, so an empty candidate is not rescheduled on every frame.

Physical surface and flat-patch reduction

World collision requests full-detail meshes from the same occupancy extraction. Its cache tracks the terrain instance, grid generation, revision, and one collider per nonempty chunk surface. Changed chunks and affected neighbors rebuild; unchanged chunk shapes retain their shared references.

Collision meshing shares surface vertices across quads and reduces regular flat patches. A reduced patch keeps every perimeter vertex and triangulates from a center vertex to that boundary. Keeping the perimeter avoids the T-junctions that replacing a large rectangle with only 2 triangles could introduce at adjacent chunk or curved-surface boundaries.

The resulting triangle mesh merges duplicate vertices and fixes internal edges. The character motor, camera collision, spatial queries, and rigid-body solver consume these chunk surfaces. Collision-cache rebuilding is synchronous with the request, although a set larger than 8 affected chunks builds in parallel. Background render-mesh completion does not determine when that physical cache is current.

The solver attaches chunk colliders to one fixed terrain body. It compares shared shape identities and replaces only changed chunk shapes. Terrain solver colliders use grass physical properties; sampled terrain material used by world queries is a separate lookup. Material painting therefore does not imply a per-triangle solver friction update.

Water geometry and solid boundaries

Water extraction constructs top sheets for water cells open above. Top height includes the occupied fraction within the cell. Water also extends over neighboring shore cells so the solid surface cuts its outline, rather than ending the visible sheet at square cell boundaries. Open vertical sides receive water geometry too.

Water always meshes cell by cell, even when the neighboring solid terrain uses a coarse level. Averaging a small water region over a coarse block would spread its sheet across land. Detail-only changes can reuse existing water meshes; voxel edits request updated water geometry.

Shore vertices carry a wave-fade value that approaches zero at the sheet edge. The water renderer can animate the surface without moving the voxel occupancy or adding wave motion to terrain collision. The physical water sample used by buoyancy follows the grid's water data separately from this rendered wave shape.

For scripted terrain operations and their argument rules, use the Terrain reference.