| Optimization | What Was Done | Impact |
|---|---|---|
| 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 |
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
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
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
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
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
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
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
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
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
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).
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.
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.
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.
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
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
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
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
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.
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.
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.
shared/polygon_optimize.mjsimport { 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.
node tools/analyze-scene.mjs virgo
node tools/precompute-lod.mjs taurus
node tools/bake-static.mjs --analyze earth
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)
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.
| 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. |