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

ModuleContent
particles_sdf_billboard.js700+ line SDF particle shader (raymarch, volumetric, phases)
phase_vfx.jsPer-phase visual effects injection
scene_debug_visualizer.jsScene-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.jsOutputs vec4(linearDepth, density, normalizedDepth, 1.0) per particle billboard
particle_*.js9 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.