Polygon Evolution Roadmap

Industry-Grade Rendering Within Three.js · Incremental · Each Step Independent · Converges Toward Lithos · 14/18 tasks built
The polygon path is the production renderer today. While the SDF/Lithos migration proceeds in parallel, the polygon path can be dramatically improved using the same techniques the game industry developed over the past decade. These are incremental — each phase works independently, no big-bang rewrite. Each phase also makes the eventual Lithos migration easier by surfacing the same distinctions (static vs. dynamic, CPU vs. GPU, per-object vs. per-pixel) that the emitter needs.
0 Current State Where You Are
Production polygon renderer with completed first-round optimizations
~456
Scene Objects
~26
JS Modules
~6
Shared Materials
~12ms
Frame Time
OptimizationWhat Was DoneImpact
Material consolidation ~200 materials consolidated to ~6 shared materials Reduced shader state switches by 97%
InstancedMesh (fish) 183 fish sprites collapsed to 5 InstancedMesh calls 178 fewer draw calls
Backdrop-filter disabled CSS backdrop-filter was causing GPU compositing stalls Eliminated compositor overhead
Distance-gated orbits Orbit computation skipped when camera is far from object Eliminated unnecessary JS per-frame work
Sun shader LOD Simplified sun shader at distance, full detail up close Reduced GPU fragment cost when sun is small
2-5ms GC pauses every ~500ms ~200 draw calls/frame ~120MB memory footprint

Phase 1: Static Batching + Merge

1 Static Batching + Merge Industry 2015-era 4/4 BUILT
Reduce draw calls from 456 to ~50. One draw call per material instead of one per object.
Static Geometry Merge ✓ BUILT

Identify objects that never move: terrain, rocks, ruins, walls, farmhouse. Merge into a single BufferGeometry per material. One draw call per material instead of one per object.

Library: tools/bake-static.mjs

Exports: identifyStatic, mergeStaticGeometry, createMergedScene

InstancedMesh Everything Repeated ✓ BUILT

Trees (same geometry, different transforms), fence posts, vineyard posts, flowers — anything that repeats gets one InstancedMesh with N instances instead of N separate draw calls.

Library: shared/instanced.mjs

Exports: convertRepeatedMeshes, createScatterInstanced

Material Atlas / Dedup ✓ BUILT

Combine texture-based materials into texture atlases. One material = one draw call for all objects using it. UV coordinates remapped to atlas regions.

Library: shared/material_optimizer.mjs

Exports: deduplicateMaterials, poolMaterials

Geometry LOD ✓ BUILT

Two-level LOD for complex objects. Near: full detail mesh. Far: simplified mesh (50% fewer triangles). Three.js LOD object handles switching automatically based on camera distance.

Library: shared/lod.mjs

Exports: autoLOD, createSimplifiedGeometry, LODManager

456 draws → ~50 draws 12ms frame → ~8ms frame 4/4 tasks BUILT

Phase 2: GPU Instancing + Shader Optimization

2 GPU Instancing + Shader Optimization Industry 2018-era 5/5 BUILT
Push remaining per-frame work from CPU to GPU. Eliminate JS per-object overhead.
Shader-Based Wind ✓ BUILT

Move wind displacement from JS tick (per-object sin()) to vertex shader (per-vertex sin()). Eliminates ~50 JS sin calls per frame. The GPU handles wind for free — vertex shaders run in parallel across all vertices.

Library: shared/wind.mjs

Exports: createWindMaterial, WIND_PRESETS

Shader-Based Animation ✓ BUILT

Breathing, tail flick, head turn via vertex shader uniforms instead of JS matrix updates. One uniform update replaces full matrix recomputation per object per frame.

Library: shared/shader_animation.mjs

Exports: createAnimatedMaterial, ANIMATION_PRESETS

Shader Uber-Material ✓ BUILT

Combine all shading logic into one shader with material ID as uniform. Reduces material state switches to zero. All objects use the same shader program, branching on a material index.

Library: shared/material_optimizer.mjs

Exports: createUberMaterial

Frustum Culling Optimization ✓ BUILT

Three.js default frustum cull is per-object. With merged geometry, frustum cull at chunk level instead — terrain chunks, vegetation chunks. Fewer bounding volume tests, same visual result.

Library: shared/culling.mjs

Exports: createChunkedCuller, CullingPipeline

Occlusion Culling (Software) ✓ BUILT

Simple depth-based occlusion: don't render objects behind the farmhouse when camera faces it. HZB (hierarchical z-buffer) in JS. Cheap approximation, significant draw call reduction for specific camera angles.

Library: shared/culling.mjs

Exports: createOcclusionCuller

~50 draws → ~20 draws 8ms frame → ~6ms frame 5/5 tasks BUILT

Phase 3: Compute-Driven

3 Compute-Driven Rendering Industry 2020-era · Requires WebGPU
GPU decides what to render. CPU submits one call. Zero per-object JS work.
WebGPU Migration

Replace WebGLRenderer with WebGPURenderer. Three.js r175 has experimental support.

INVARIANT: cosmos.html stays on r160/WebGL. home.html can migrate. Risk: ShaderMaterial incompatibility (documented in .wire).

GPU Frustum Culling

Compute shader reads bounding boxes, writes indirect draw buffer. CPU submits one indirect draw call. The GPU decides visibility instead of JS testing 456 bounding spheres.

GPU Particle Update

Fireflies, dust, pollen computed on GPU via compute shaders. Zero JS per-particle work. Particle count can scale to millions with no CPU cost increase.

Indirect Multi-Draw

All objects in one buffer, GPU selects which to draw via compute pass. One drawIndirect call replaces the entire draw call loop. CPU cost becomes constant regardless of scene complexity.

~20 draws → 1 indirect draw 6ms frame → ~4ms frame Blocked on WebGPU + ShaderMaterial compatibility

Phase 4: Hybrid SDF + Polygon (The Bridge)

4 Hybrid SDF + Polygon The Bridge · Industry-Proven (UE5 Lumen) 4/4 BUILT
Use SDF where it's better, polygon where it's better. Best visual quality from both techniques combined.
SDF Terrain + Polygon Objects ✓ BUILT

Render terrain as SDF (one fullscreen quad for ground), place polygon objects on top. Best of both: infinite-resolution terrain via distance fields, mesh-based creatures and structures with full material detail.

Library: shared/hybrid_renderer.mjs

Exports: createHybridRenderer, installHybridMode

SDF Shadows ✓ BUILT

Use SDF for shadow computation (soft shadows via sphere tracing), polygon for primary rendering. Much cheaper than shadow maps, produces physically accurate penumbra. One shadow ray per pixel vs. rendering the entire scene from the light's perspective.

Library: shared/sdf_shadows.mjs

Exports: SDF_SHADOW_GLSL, createShadowPass

SDF Ambient Occlusion ✓ BUILT

Cone-traced AO from the SDF, applied to polygon objects. More accurate than SSAO (screen-space ambient occlusion) because it uses actual 3D distance rather than depth buffer approximation.

Library: shared/sdf_shadows.mjs

Exports: SDF_AO_GLSL, createAOPass

SDF Fog / Atmosphere ✓ BUILT

Volumetric fog via SDF ray marching, polygon objects embedded in it. Fog density varies with distance field values, creating physically plausible scattering around solid objects.

Library: shared/sdf_shadows.mjs

Exports: SDF_FOG_GLSL

Industry precedent: Unreal Engine 5 Lumen already does this — uses SDF for global illumination while rendering polygons for primary visibility. This is not experimental. It shipped in Fortnite.
Best visual quality Frame time depends on SDF/polygon ratio 4/4 tasks BUILT

Phase 5: Where Polygon Meets Lithos

5 Polygon Objects Emit Through Lithos Convergence
Gradual migration. Each home moves its simplest objects first. Only objects that NEED triangles stay polygon.
One-at-a-Time Migration

Polygon objects that can be expressed as SDF get migrated individually. The emitter already supports mixing: sceneSDF handles terrain + trees while the farmhouse remains polygon. No all-or-nothing switchover.

Gradual Per-Home Transition

Each home migrates its simplest objects first — terrain, rocks, basic shapes. Complex animated creatures and text stay polygon until the SDF animation system matures. Transition is measured, not rushed.

End State

Only objects that genuinely need triangles remain polygon: text rendering, 2D UI overlays, particle billboards. Everything else lives in sceneSDF(vec3 p). The polygon renderer becomes a thin overlay on top of the Lithos SDF substrate.

456 polygon objects hybrid: SDF terrain + polygon objects sceneSDF + polygon overlay (text, UI, particles)

Central Library

Single Entry Point — shared/polygon_optimize.mjs

import { optimizeScene } from '../shared/polygon_optimize.mjs';
const result = optimizeScene(THREE, scene, camera, { phase: 1 });

All 14 built libraries are accessible through this unified interface. Pass { phase: 1 } for static batching only, { phase: 2 } to include GPU shader optimizations, or { phase: 4 } to enable hybrid SDF+polygon mode. Each phase includes all previous phases.

Analysis Tools

CLI tools for auditing and pre-processing

node tools/analyze-scene.mjs virgo
Audit any home — reports draw calls, material count, static vs. dynamic objects, instancing candidates, and optimization opportunities.
node tools/precompute-lod.mjs taurus
Pre-generate LOD meshes for a home's complex objects. Writes simplified geometry to disk for fast load.
node tools/bake-static.mjs --analyze earth
Identify static geometry in a home without modifying anything. Reports which objects qualify for merge and estimated draw call reduction.

Summary Timeline

Now
456 draws
~12ms frame
Phase 0
Static Batch
~50 draws
~8ms frame
Phase 1 · 4/4 BUILT
GPU Shaders
~20 draws
~6ms frame
Phase 2 · 5/5 BUILT
Compute
1 draw
~4ms frame
Phase 3 · when WebGPU
Hybrid
SDF + poly
varies
Phase 4 · 4/4 BUILT
Lithos
pure SDF
~2ms frame
Phase 5 · convergence

Implementation Progress: 14 / 18 Tasks Built

14
Tasks Built
78%
Complete
4
Remaining (Phase 3)
3
Phases Fully Built

Phase 1: 4/4 built  ·  Phase 2: 5/5 built  ·  Phase 3: 0/4 (blocked on WebGPU)  ·  Phase 4: 4/4 built (+ 1 bonus from Phase 4 shared with Phase 2)

Key Insight

Each phase makes the polygon path better AND makes the Lithos migration easier. Static batching identifies which objects are static (= bakeable into SDF constants). Shader-based wind separates static position from dynamic displacement (= emitter-compatible decomposition). GPU compute moves logic off CPU (= same architectural direction as Lithos). The polygon roadmap doesn't compete with Lithos — it converges toward it.

What Each Phase Teaches the Emitter

Phase Polygon Improvement Lithos Insight
1 — Static Batch Identifies static vs. dynamic objects Static objects = SDF bake candidates. The merge list IS the emitter's input list.
2 — Shader Wind Moves displacement to vertex shader Separates base position from animation. Base positions bake to constants, animation stays parametric.
3 — Compute GPU decides visibility Same as Lithos frustum culling — the emitter already does this server-side.
4 — Hybrid SDF + polygon coexist Proves the mixing model. sceneSDF handles terrain while polygon handles the rest.
5 — Emit Polygon objects become SDF Migration complete. Polygon renderer becomes thin overlay for text/UI.