Rendering Pipeline

GPU-driven rendering with meshes, lights, materials, shader modes, and multi-pass compositing. All rendering is built on the VirtualGPU abstraction.

Frame Structure

A typical frame in the editor/game follows this pass order:

// Simplified frame from Viewport.js (~6K lines)
1.  Update camera matrices (view, proj, viewProj, invViewProj)
2.  Upload frame uniforms (camera, time, lighting)
3.  [Debug] Particle debug pre-pass (if particle-specific mode active)
4.  [Debug] Particle depth pre-pass (if scene debug mode: depth/normals)
5.  Hybrid volume compute pass (density grid splatting)
6.  Main render pass:
        a. Entity mesh rendering (GPU instanced, dynamic lighting)
        b. Grid rendering (ground plane with sun + ambient)
        c. SDF collider visualization
        d. Cloth / Rope rendering
        e. Particle rendering (full-res or deferred half-res)
        f. Volumetric smoke rendering
        g. Gizmos, wireframes, debug overlays
7.  Depth buffer copy (for soft particles + debug viz)
8.  [Debug] Scene Debug Visualizer post-pass (depth/normals from actual depth buffer)
9.  Shadow Atlas (unified depth → per-category composite)
10. Half-res particle composite (if deferred)
11. Distortion pass (heat haze)
12. SPH fluid surface pass (screen-space fluid rendering)
13. Bloom pass (emissive glow)
14. Tonemap pass (ACES + vignette)
15. Reconstruction (TSR/FSR/SVGF upscaling) + GPU submit

Scene debug modes (steps 4, 8) skip all post-processing (steps 9–14) so the debug output is clean and unmodified.

Lighting System

The LightManager handles all lighting via a single GPU uniform buffer (832 bytes). It supports sun (directional), ambient, and dynamic point lights.

Light Properties

PropertyDefaultDescription
sunDirection[0.2, -1.0, 0.1]FROM-light direction (steep overhead)
sunColor[1.0, 0.95, 0.85]Warm white sun
sunIntensity1.0Sun brightness multiplier
ambientColor[0.15, 0.15, 0.2]Cool ambient fill
ambientIntensity0.3Ambient brightness
globalBrightness1.85Final multiplier on all lighting

Direction convention: sunDirection is the FROM-light direction. The EntityMeshRenderer shader negates it internally (normalize(-lighting.sunDirection)). SDF/Cloth/Rope renderers receive the already-negated TO-light direction from the Viewport.

Shader Modes

The renderer supports 4 debug modes controlled by lightingMode and debugMode:

ModelightingModedebugModeOutput
Standarddynamic0Full sun + ambient + dynamic lights
Unlitunlit0Albedo color only, no lighting
Normalsdynamic1World-space normals (normal*0.5+0.5)
Depthdynamic2Distance-based depth gradient

Particle Lighting Integration

Particles interact with lighting in three ways:

  1. Receive — Particles sample sun + ambient from frame uniforms
  2. Emit — Hot particles (fire/plasma) become dynamic point lights via ParticleLightEmission.js
  3. Shadow/Tint — Smoke dims sunlight (particleSunShadow), fire tints it warm (particleSunTint)

Mesh Rendering

Entity meshes are rendered by EntityMeshRenderer which reads from ECS Renderable and Transform components. Each entity gets a per-object uniform buffer containing its model matrix, and a bind group linking the uniform to the shader.

Mesh Pipeline

// EntityMeshRenderer creates pipelines via vGPU
const pipeline = vgpu.pipeline.render({
  vertex: { module: meshShader, entryPoint: 'vs_main', buffers: vertexLayouts },
  fragment: { module: meshShader, entryPoint: 'fs_main', targets: [{ format }] },
  depthStencil: true,
  label: 'EntityMesh'
});

Half-Resolution Particle Compositing

Particles are rendered at half resolution for performance, then composited onto the main scene:

  1. SDF Shader outputs vec4(litColor, alpha) — non-premultiplied
  2. SDF Pipeline renders into half-res bgra8unorm texture with alpha blend
  3. Half-res texture starts cleared to (0,0,0,0); after alpha blend: RGB = lit × alpha (pre-multiplied)
  4. Composite pass uses fully additive blend (one + one) onto the scene

Implication: With additive composite, particles can only brighten the scene, never darken it. Fire needs very low alpha (0.03–0.08) + bright emission (2–5×) to avoid saturating the half-res buffer into a solid red wall.

Grid Renderer

GridRenderer.js draws the ground plane as a solid fill (alpha 0.85) with sun + ambient lighting. It receives lighting data from LightManager via setLighting() each frame.

Shadow Atlas

The ShadowAtlas provides unified shadow rendering for the entire scene. All shadow casters (entities, ropes, particles) render into a shared 4096² depth texture, then shadows are composited per category with independent PCF, bias, and strength settings.

// Shadow caster categories
'entity'   → EntityMeshRenderer.flushShadowDepth()
'rope'     → RopeMeshRenderer shadow pass
'particle' → ParticleShadowCaster (round billboard shadows)

Debug Visualization

The engine provides a two-tier debug visualization system accessible via the editor’s view mode dropdown:

Scene-Wide Modes (Post-Process)

These modes read the actual hardware depth buffer (containing meshes + ropes) and composite with a particle depth color texture. They run as a fullscreen post-pass via SceneDebugVisualizerPass.

ModeTechniqueOutput
Depth BufferLinearize texture_depth_2d + particle depth compositeGrayscale: near=white, far=black (Unreal convention)
Normal MapsFinite-difference world normals from composite depthRGB = XYZ world-space normals (industry standard)

Why two sources? Particles use depthWrite: false (they’re transparent billboards), so they never appear in the hardware depth buffer. The scene debug visualizer composites the hardware depth (meshes/ropes) with a separate particle depth color texture, picking whichever is closer at each pixel.

Particle-Specific Modes (Pre-Pass)

These modes render particles to a separate rgba16float texture using specialized debug renderers, then display as a fullscreen overlay. All use circular billboard clipping (dist > 1.0 discard).

ModeRendererOutput
Albedo/ColorParticleAlbedoRendererRaw particle color
Lighting OnlyParticleLightingRendererSun + ambient on synthesized sphere normals
VelocityParticleVelocityRendererSpeed heatmap (blue→red)
Age/LifetimeParticleAgeRendererNormalized age gradient (green→yellow→red)
EmissiveParticleEmissiveRendererLuminance-based emission intensity
SizeParticleSizeRendererNormalized size gradient
ThermalParticleThermalRendererBlackbody color from temperature (Kelvin)

Other Debug Systems

Key Files

FilePurpose
render/LightManager.jsLighting uniforms, sun/ambient/dynamic lights
render/mesh/EntityMeshRenderer.jsGPU-instanced entity mesh rendering with dynamic lighting
render/passes/ShadowAtlas.jsUnified 4096² shadow depth atlas (entity + rope + particle)
render/passes/SceneDebugVisualizerPass.jsFullscreen post-pass: scene depth + particle depth → debug viz
render/passes/BloomPass.jsMulti-pass bloom for emissive particles
render/passes/TonemapPass.jsACES tonemapping + vignette
render/passes/RenderPassManager.jsRender target allocation and view mode management
render/particles/ParticleSdfRenderer.jsSDF billboard particle rendering (volumetric)
render/particles/ParticleShadowCaster.jsRound billboard particle shadows
render/particles/ParticleHalfResComposite.jsHalf-res → full-res particle composite
render/particles/Particle*Renderer.js9 debug renderers (depth, normals, albedo, lighting, velocity, age, emissive, size, thermal)
render/shaders/ShaderComposer.jsModular WGSL shader composition
render/shaders/modules/passes/scene_debug_visualizer.jsWGSL shader: depth linearize + normal reconstruct from depth
render/CameraMath.jsView/projection matrix computation