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
| Property | Default | Description |
|---|---|---|
sunDirection | [0.2, -1.0, 0.1] | FROM-light direction (steep overhead) |
sunColor | [1.0, 0.95, 0.85] | Warm white sun |
sunIntensity | 1.0 | Sun brightness multiplier |
ambientColor | [0.15, 0.15, 0.2] | Cool ambient fill |
ambientIntensity | 0.3 | Ambient brightness |
globalBrightness | 1.85 | Final 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:
| Mode | lightingMode | debugMode | Output |
|---|---|---|---|
| Standard | dynamic | 0 | Full sun + ambient + dynamic lights |
| Unlit | unlit | 0 | Albedo color only, no lighting |
| Normals | dynamic | 1 | World-space normals (normal*0.5+0.5) |
| Depth | dynamic | 2 | Distance-based depth gradient |
Particle Lighting Integration
Particles interact with lighting in three ways:
- Receive — Particles sample sun + ambient from frame uniforms
- Emit — Hot particles (fire/plasma) become dynamic point lights via
ParticleLightEmission.js - 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:
- SDF Shader outputs
vec4(litColor, alpha)— non-premultiplied - SDF Pipeline renders into half-res
bgra8unormtexture with alpha blend - Half-res texture starts cleared to
(0,0,0,0); after alpha blend: RGB = lit × alpha (pre-multiplied) - 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.
| Mode | Technique | Output |
|---|---|---|
| Depth Buffer | Linearize texture_depth_2d + particle depth composite | Grayscale: near=white, far=black (Unreal convention) |
| Normal Maps | Finite-difference world normals from composite depth | RGB = 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).
| Mode | Renderer | Output |
|---|---|---|
| Albedo/Color | ParticleAlbedoRenderer | Raw particle color |
| Lighting Only | ParticleLightingRenderer | Sun + ambient on synthesized sphere normals |
| Velocity | ParticleVelocityRenderer | Speed heatmap (blue→red) |
| Age/Lifetime | ParticleAgeRenderer | Normalized age gradient (green→yellow→red) |
| Emissive | ParticleEmissiveRenderer | Luminance-based emission intensity |
| Size | ParticleSizeRenderer | Normalized size gradient |
| Thermal | ParticleThermalRenderer | Blackbody color from temperature (Kelvin) |
Other Debug Systems
- VGPUDebugDraw — Immediate-mode lines, boxes, spheres, frustums
- Gizmo renderers — Translation/rotation/scale handles (editor)
- Collision debug — Wireframe collider visualization
Key Files
| File | Purpose |
|---|---|
render/LightManager.js | Lighting uniforms, sun/ambient/dynamic lights |
render/mesh/EntityMeshRenderer.js | GPU-instanced entity mesh rendering with dynamic lighting |
render/passes/ShadowAtlas.js | Unified 4096² shadow depth atlas (entity + rope + particle) |
render/passes/SceneDebugVisualizerPass.js | Fullscreen post-pass: scene depth + particle depth → debug viz |
render/passes/BloomPass.js | Multi-pass bloom for emissive particles |
render/passes/TonemapPass.js | ACES tonemapping + vignette |
render/passes/RenderPassManager.js | Render target allocation and view mode management |
render/particles/ParticleSdfRenderer.js | SDF billboard particle rendering (volumetric) |
render/particles/ParticleShadowCaster.js | Round billboard particle shadows |
render/particles/ParticleHalfResComposite.js | Half-res → full-res particle composite |
render/particles/Particle*Renderer.js | 9 debug renderers (depth, normals, albedo, lighting, velocity, age, emissive, size, thermal) |
render/shaders/ShaderComposer.js | Modular WGSL shader composition |
render/shaders/modules/passes/scene_debug_visualizer.js | WGSL shader: depth linearize + normal reconstruct from depth |
render/CameraMath.js | View/projection matrix computation |