Poly-Gone Spheres
A procedural planet editor for building a base shape with nodes, sculpting it non-destructively, and rendering it through a sparse GPU SDF.
Store the surface, not the empty planet
A signed-distance field, or SDF, stores the distance to the nearest surface. Negative values are inside the planet and positive values are outside, so both raymarching and mesh extraction can find the surface where the sign changes. A dense field is simple, but most samples in a planet-sized box describe empty space and still consume memory.
I split the field into chunks and smaller bricks. A brick covers 16³ cells and
stores an 18³ sample footprint: one extra sample on every side forms a gutter for
continuous filtering and surface extraction across brick boundaries. Each chunk groups 4³, or 64, bricks.
Physical 16-bit floating-point texture storage is committed only where the SDF can contain a
surface. The allocator packs 8³, or 512, brick slices into a lazily allocated
bindless 3D texture block. A packed address uses its lower nine bits for the local brick and
the remaining bits for the block identifier, allowing shaders to reach every resident brick
through the same descriptor set. A texture budget puts an explicit ceiling on physical storage
without fixing the logical planet size to one dense allocation.
struct BrickAddress {
// bits 0-8: local brick; bits 9-31: bindless block
uint packed;
uint flags; // resident, allocated, dirty, pending-clear
};
struct GpuBlock { uint texture_index; uint flags; };
From a sculpt stroke to GPU work
A sculpt stroke begins on the main thread, but the editor should not wait there while the whole sparse structure is reconsidered. The render feature queues the edit, and a bookkeeping worker updates the journal, queries affected regions, changes residency, and assembles a compact frame plan. CPU classification can itself use the thread pool when a change spans many chunks.
Frame plans are double-buffered. While the GPU consumes one plan, background work can prepare the next. The render graph imports the required buffers and bindless textures, uploads compact generation records, runs the dirty-brick compute pass, and then makes the new data available to the main rendering and debug passes.
Resource reuse follows the GPU rather than the CPU. A generation ring and completion fences prevent a job slot or brick address from being recycled while an older command buffer may still reference it. This is what makes background planning safe instead of merely concurrent.
Residency and incremental regeneration
Surface masks first classify which bricks can cross zero and therefore deserve physical storage. The chunk keeps two 32-bit words for its 64 brick flags. The allocator can then commit newly relevant bricks, retain existing ones, and recycle storage that is no longer needed.
A resident brick records how far through the ordered sculpt journal it has been generated. The common incremental path applies only newer overlapping operations. A new or invalidated brick takes the full path, evaluating the procedural base program and replaying all relevant edits in order. Jobs sent to the GPU contain the target address, generation mode, and a compact range of relevant operations rather than the complete global journal.
struct BrickGenerationJob {
uint4 chunk_address_local; // chunk, address, local brick, LOD
uint4 edit_offset_count; // relevant ordered journal range
uint4 generation_mode; // full or incremental, source, target
};Asynchronous completion needs more than a dirty flag. Snapshot versions and residency generations ensure an old task cannot clear a newer edit or write into an address that has already been recycled. Per-frame limits for incremental, full-generation, and streaming jobs also keep one large sculpt from monopolizing a frame; remaining work stays queued for later plans.
Sculpt journal and spatial pruning
Sculpting is non-destructive. The journal stores additive union and subtractive difference stamps in sequence, so undo and redo change which records are active instead of trying to reverse values already baked into voxels. The order must be preserved because constructive solid geometry operations are not generally interchangeable.
Replaying every stamp for every brick would eventually erase the benefit of sparse updates. A
contiguous, serializable BVH reduces a region query to the stamps whose bounds overlap that
brick. It uses binned surface-area-heuristic rebuilds, cheaper refits when edits move, and
local tree rotations to recover query quality without rebuilding every time. A balanced query
is typically O(log N + K) for K returned edits, although a
degenerate tree can still reach O(N + K).
The interactive view below is a simplified 2D explanation of that tradeoff. Drag the leaves and compare the tree before and after rotation; it illustrates the search-area cost rather than reproducing the editor's full 3D benchmark.
Kopta et al. BVH Tree Rotations
Evaluates the 7 Kensler rotations. Drag leaves, then click Optimize.
Local SAH Cost
Sum of internal bounding box areas (excluding root).
Skip empty space on the GPU
The viewport rasterizes one planet-scale proxy mesh to start rays only where the planet can appear. A fine chunk-pointer grid maps logical positions to resident chunks, while a coarser macrocell grid summarizes groups of four chunks. Dirty ranges are uploaded incrementally, so a local sculpt does not resend the whole lookup structure.
Inside the proxy, hierarchical DDA first crosses empty macrocells, then chunks, and finally individual bricks. Only a populated brick begins fixed-step or cached sphere tracing of the SDF. Once a sign change is found, a short refinement step improves the hit position before the normal and material are evaluated. This combines grid traversal for large empty distances with SDF sampling close to the actual surface.
This interactive preview intentionally reduces the renderer to a 2D grid. Try both modes to see why hierarchical traversal takes larger steps through known-empty regions; it is an algorithm demonstration, not a frame-time benchmark.
Algorithm Comparison
Click to set ray origin.
Macro Skips
Micro Skips
Surface Hit
Export and evidence
Volume export serializes the procedural pipeline together with its sculpt journal, preserving the information needed to regenerate the planet at another resolution. Mesh export snapshots the same program and journal, then performs the extraction in background CPU work so the live editor does not have to surrender its active state.
The mesh path is presented in the UI as marching cubes, while the extraction itself decomposes each cell into six tetrahedra. Parallel slabs share a sampler that keeps two rolling sample planes, uses the sculpt BVH to prune irrelevant operations, and deduplicates indexed vertices before writing positions, normals, indices, and a material to GLB. The implementation caps an export axis at 512 cells and supports cancellation.
Targeted tests cover conservative surface masks, ordered journal queries, stale dirty snapshots, cross-brick CSG continuity, subtraction and undo, and resource-retirement masks. Benchmarks exercise journal construction and queries, refitting, region-pruned picking, edit classification, and procedural noise.
Limits and next steps
Residency classification currently happens in a background CPU task fanned-out over multiple worker threads, but can still add up for a very large sculpt. A future GPU request buffer could discover missing bricks lazily, deduplicate requests, and move that classification closer to the generation workload.
Clipmaps are another natural extension because the non-destructive journal can regenerate the same region at different resolutions. I would also like to revisit per-chunk proxy meshes with GPU-driven occlusion culling: the earlier proxy cubes resulted in too much reduntant work, but it could be beneficial for planets that for example have a large tower on one side, this would currently result in only that side of the aabb expanding, this would still result in a decent amount of empty space if the tower is slim, proxy chunks could reduce the amount of empty space here. Alternative empty-space structures, such as a sparse voxel DAG, are a potential future experiment as well.