Shaders & WGSL
Modular WGSL shader system with composition, preprocessing, a shared library of reusable modules,
and the ShaderComposer for assembling complex shaders from parts.
Shader Organization
engine/render/shaders/
├── ShaderComposer.js ← Assembles shaders from modules
├── ShaderLoader.js ← File-based shader loading
├── ShaderSchema.js ← Shader metadata and validation
├── ShaderSources.js ← Inline shader source strings
├── WgslPreprocessor.js ← #define, #if, #include preprocessing
├── core/ ← Core shader modules (lighting, transforms)
├── modules/
│ ├── core/
│ │ ├── particles_sdf_billboard.js ← Main particle SDF shader (700+ lines)
│ │ ├── phase_vfx.js ← Per-phase visual effects
│ │ └── ...
│ ├── passes/ ← Fullscreen post-process & debug pass shaders
│ │ ├── scene_debug_visualizer.js ← Depth/normal viz from hardware depth buffer
│ │ ├── depth_visualizer.js ← Legacy particle depth visualization
│ │ ├── particle_depth.js ← Particle depth pass (linear depth output)
│ │ ├── particle_normals.js ← Synthesized sphere normals
│ │ ├── particle_albedo.js ← Raw particle color
│ │ ├── particle_lighting.js ← Diffuse lighting on sphere normals
│ │ ├── particle_velocity.js ← Speed heatmap
│ │ ├── particle_age.js ← Age/lifetime gradient
│ │ ├── particle_emissive.js ← Emission intensity
│ │ ├── particle_size.js ← Size gradient
│ │ └── particle_thermal.js ← Blackbody color from Kelvin temperature
│ └── lib/ ← Reusable WGSL function libraries
│ ├── density/falloff.js ← Gaussian, linear, cubic, layered falloffs
│ ├── depth/linearize.js ← normalizeLinearDepth()
│ └── depth/reconstruct.js ← reconstructWorldPos(), linearizeDepth()
├── materials/ ← Material shaders (PBR, unlit, etc.)
├── effects/ ← Post-process effect shaders
└── debug/ ← Debug visualization shaders
ShaderComposer
The ShaderComposer assembles complex shaders from a library of reusable WGSL modules. Modules are registered by name and injected into shaders at compile time.
import { ShaderComposer } from './engine/render/shaders/ShaderComposer.js';
// Compose a shader with library dependencies
const source = ShaderComposer.compose({
libs: ['particles/phase_vfx', 'lighting/pbr'],
main: myShaderCode
});
The composer resolves the LIBRARY_MAP to inject WGSL function definitions before your main code. This avoids duplicating common functions like lighting calculations across shaders.
WGSL Preprocessor
The WGSLPreprocessor supports C-style preprocessing directives before compilation:
// In your WGSL source:
#define MAX_LIGHTS 16
#define ENABLE_SHADOWS
#if ENABLE_SHADOWS
// Shadow mapping code included
#endif
// Compile with defines:
vgpu.shader.compile('lit', source, { MAX_LIGHTS: 8, ENABLE_SHADOWS: 1 });
Shader Reflection
VGPUShaderReflection parses WGSL source to extract bind group layouts, struct definitions, and entry points. This enables automatic pipeline layout generation from shader source.
Compilation via vGPU
All shader compilation goes through vgpu.shader:
// Compile and cache
const module = vgpu.shader.compile('name', wgslCode);
// Hot reload (dev mode)
vgpu.shader.recompile('name', updatedCode);
// Check compilation errors
const info = await module.getCompilationInfo();
Important: Use textureSampleLevel(..., 0.0) instead of textureSample() in compute shaders and non-uniform control flow. WebGPU validation rejects textureSample outside uniform control flow.
Key Shader Modules
| Module | Content |
|---|---|
particles_sdf_billboard.js | 700+ line SDF particle shader (raymarch, volumetric, phases) |
phase_vfx.js | Per-phase visual effects injection |
scene_debug_visualizer.js | Scene-wide depth + normal visualization. Reads texture_depth_2d (hardware depth) + texture_2d<f32> (particle depth). Linearizes depth, reconstructs world normals via finite differences, composites both sources. |
particle_depth.js | Outputs vec4(linearDepth, density, normalizedDepth, 1.0) per particle billboard |
particle_*.js | 9 debug pass shaders with shared billboard vertex stage + circular clipping |
LightManager.getShaderDefs() | Lighting struct definitions for WGSL injection |
LightManager.getShaderFunctions() | Lighting calculation functions for WGSL injection |
Common Patterns
Billboard Circle Clipping
All particle debug shaders render billboard quads with vertices at (-1,-1) to (1,1). The fragment shader must discard corners to produce circular particles:
// In every particle_*.js fragment shader:
let dist = length(input.localPos);
if (dist > 1.0) { discard; } // Hard circle clip
let falloff = falloffGaussian(dist, 0.8);
if (falloff < 0.01) { discard; } // Soft edge
Without the hard clip: falloffGaussian(1.414, 0.8) ≈ 0.21 still passes the 0.01 threshold, making particles appear as squares instead of circles.