Rendering Pipeline
The rendering stack is the largest subsystem in Web Engine Dev, spanning multiple packages across Layers 6 and 7. It follows a WebGPU-first strategy (ADR-003) with WebGPU-only runtime execution.
Architecture Overview
Package Responsibilities
| Package | Layer | Responsibility |
|---|---|---|
render-graph | 6 | DAG scheduling of render passes, resource lifetime management |
shader-compiler | 6 | WGSL tooling: preprocessor, shader variants, manifests, and optional transpilation utilities |
renderer | 7 | Forward rendering, materials, lighting, shadows, post-processing, culling, GPU-driven rendering |
gltf | 7 | glTF 2.0 / GLB loading with PBR materials and skinning |
particles | 7 | GPU-accelerated particle systems |
sprites | 7 | 2D sprite batching and animation |
text | 7 | Text rendering (SDF and bitmap fonts) |
terrain | 7 | CDLOD terrain with instanced rendering |
gizmos | 7 | Editor gizmo rendering (translate, rotate, scale handles) |
Device Abstraction
The renderer uses a unified GpuDevice interface with a WebGPU runtime backend. Device creation still uses preferredBackend: 'auto' for API compatibility:
import { createDevice } from '@web-engine-dev/renderer';
const { device, backend } = await createDevice({
canvas: document.querySelector('canvas')!,
preferredBackend: 'auto',
powerPreference: 'high-performance',
});
console.log(`Using ${backend} backend`); // always 'webgpu'Key Interfaces
| Interface | Purpose |
|---|---|
GpuDevice | GPU device abstraction (create buffers, textures, pipelines) |
GpuBuffer | GPU buffer (vertex, index, uniform, storage) |
GpuTexture / GpuTextureView | Texture resources and views |
GpuRenderPipeline / GpuComputePipeline | Shader pipelines |
GpuBindGroup / GpuBindGroupLayout | Resource bindings |
Device Capabilities
device.capabilities exposes validated WebGPU limits and feature flags such as:
maxTextureDimension2D,maxBindGroups,maxUniformBufferBindingSizesupportsTimestampQuery,supportsRenderBundles,supportsDeferredRenderingsupportsTextureCompressionBC/supportsTextureCompressionETC2/supportsTextureCompressionASTCisMobile,isSafari
If WebGPU is unavailable, createDevice() throws an explicit error.
Forward Rendering Pipeline
The core rendering path uses a forward renderer with the following frame structure:
Frame Execution Order
- Shadow Pass -- Renders shadow maps for all shadow-casting lights (cascaded shadows for directional lights, cubemap shadows for point lights, perspective shadows for spot lights)
- Forward Pass -- Renders all visible objects with full lighting, sorted by render layer:
- Opaque (front-to-back, minimizes overdraw)
- Alpha Test (front-to-back with alpha-to-coverage when MSAA is enabled)
- Transparent (back-to-front for correct blending)
- Post-Processing -- HDR effects pipeline (bloom, tonemapping, FXAA/TAA, SSAO, DOF, color grading)
- Output -- Final composited frame to the canvas
Render Queues and Sorting
Objects are sorted into render queues based on their material's blend mode:
| Render Layer | Sort Order | Depth Write | Use Case |
|---|---|---|---|
Default (Opaque) | Front-to-back | Yes | Solid geometry |
AlphaTest | Front-to-back | Yes | Cutout materials (foliage, fences) |
Transparent | Back-to-front | No | Glass, water, particles |
Within each layer, objects are further sorted by material to minimize GPU state changes (bind group switches).
Render Graph
The render-graph package provides a DAG-based render pass scheduler. Passes declare their inputs, outputs, and dependencies, and the graph resolves execution order and resource lifetimes automatically:
import { RenderGraph } from '@web-engine-dev/render-graph';
const graph = new RenderGraph(adapter);
graph.addPass('shadow', {
attachments: { depth: shadowTexture },
execute: (encoder) => shadowPass.render(encoder, scene),
});
graph.addPass('forward', {
attachments: { color: colorTexture, depth: depthTexture },
dependencies: ['shadow'],
execute: (encoder) => forwardRenderer.render(encoder, scene),
});
graph.addPass('postprocess', {
attachments: { color: outputTexture },
dependencies: ['forward'],
execute: (encoder) => postProcess.apply(encoder, colorTexture),
});
graph.execute();Benefits
- Automatic dependency resolution -- Passes execute in the correct order based on declared dependencies
- Resource lifetime management -- Transient textures are allocated only when needed and released after use
- Parallel pass execution -- Independent passes can execute concurrently (when supported by the backend)
- Backend-agnostic abstraction -- The graph API is backend-independent while the engine runtime uses WebGPU
Material System
Materials define the visual appearance of objects through shaders, properties, and textures.
Built-in Material Types
| Material | Description |
|---|---|
| PBR Standard | Metallic-roughness workflow (glTF 2.0 aligned) |
| PBR Advanced | Standard + clearcoat, transmission, sheen, iridescence, specular extensions |
| Unlit | No lighting calculations, solid color or textured |
Shader Variant System
The renderer uses a template-based shader variant system. Base shader templates contain placeholders that are filled based on enabled features and material defines:
{{DEFINES}}
struct VertexInput {
{{VERTEX_INPUTS}}
}
@vertex fn vs_main(in: VertexInput) -> VertexOutput {
{{VERTEX_TRANSFORM}}
}
@fragment fn fs_main(in: FragmentInput) -> @location(0) vec4f {
{{FRAGMENT_PRE_LIGHTING}}
// Lighting calculations...
{{FRAGMENT_POST_LIGHTING}}
}Features like skinning, normal mapping, and parallax occlusion are injected at the appropriate markers. The ShaderSystem manages variant compilation and caching.
Bind Group Layout
The renderer uses a 4-group layout to keep update frequency boundaries explicit and predictable:
| Group | Purpose | Update Frequency | Contents |
|---|---|---|---|
| 0 | Camera | Per-frame | View/projection matrices, camera position |
| 1 | Model | Per-object | Model matrix, normal matrix |
| 2 | Material | Per-material | Material properties, textures, samplers |
| 3 | Lighting | Per-frame | Light array, shadows, IBL environment, fog |
Draw calls are sorted by material (group 2) to minimize bind group switches.
Shader Compilation Pipeline
Shaders are authored in WGSL and compiled directly for WebGPU. The shader-compiler package provides preprocessing, variant tooling, manifest generation, and optional transpilation utilities for external workflows:
Compilation & Tooling Details
The shader compiler handles:
- Preprocessing --
#define, conditionals, and#includeexpansion - Variant composition -- Feature-driven shader permutations
- Manifest workflows -- Build-time generation and runtime cache integration
- Optional transpilation utilities -- WGSL transform/transpile tooling for non-renderer workflows
Preprocessor
The shader preprocessor supports:
#define/#ifdef/#ifndef/#else/#endiffor conditional compilation#include "filename"for shared shader modules- Feature defines injected by
ShaderSystembased on material capabilities and device limits
Lighting System
The renderer supports three light types with GPU buffer management:
| Light Type | Shadow Technique | Buffer Size |
|---|---|---|
| Directional | Cascaded Shadow Maps (1-4 cascades) | 48 bytes |
| Point | Cube shadow maps | 48 bytes |
| Spot | Perspective shadow maps | 64 bytes |
Lights are uploaded to a single GPU buffer with a header indicating counts:
// Light buffer layout
// Header: vec4(directionalCount, pointCount, spotCount, 0)
// Followed by: DirectionalLight[], PointLight[], SpotLight[]Clustered Lighting
For scenes with many lights, the renderer supports clustered forward shading. The view frustum is divided into 3D clusters, and each cluster stores a list of affecting lights. This reduces per-fragment light iteration from O(N) to O(K) where K is the number of lights affecting the cluster.
Image-Based Lighting (IBL)
The IBL pipeline converts HDR environment maps into prefiltered cubemaps for PBR rendering:
Equirect HDR --> Source Cubemap (with mip chain)
|
+--------------+--------------+
| | |
Irradiance Prefiltered BRDF LUT
(diffuse) (specular) (integration)Anti-firefly techniques follow the Filament reference implementation: K=4 LOD bias and soft HDR compression.
Post-Processing Effects
The post-processing pipeline processes HDR render targets through a chain of effects:
| Effect | Description |
|---|---|
| Bloom | Bright area glow with threshold and intensity control |
| Tonemapping | HDR-to-LDR conversion (Reinhard, ACES, Filmic) |
| FXAA | Fast approximate anti-aliasing |
| TAA | Temporal anti-aliasing with motion vectors |
| SSAO | Screen-space ambient occlusion |
| SSGI | Screen-space global illumination |
| Depth of Field | Bokeh-based depth blur |
| Color Grading | LUT-based color correction |
| Vignette | Screen-edge darkening |
| CAS/FSR | Contrast-adaptive sharpening and AMD FidelityFX upscaling |
Effects are composable and can be enabled/disabled at runtime:
import { PostProcessPipeline, BloomEffect, TonemappingEffect } from '@web-engine-dev/renderer';
const postProcess = new PostProcessPipeline(device, {
hdrEnabled: true,
effects: [
new BloomEffect(device),
new TonemappingEffect(device),
],
});GPU-Driven Rendering
For large scenes, the renderer supports GPU-driven indirect rendering:
- Instance buffer -- All object transforms stored in a single SoA-layout buffer
- GPU culling -- Compute shader performs frustum and occlusion culling, writing visible instances to an indirect draw buffer
- Indirect draw -- A single
drawIndexedIndirectcall renders all visible instances of a given mesh/material combination
This reduces CPU-side draw call overhead from O(N) per object to O(1) per material batch.
ECS Integration
The renderer provides full ECS integration through components, resources, and systems:
Components
import {
Transform3D, // Position, rotation, scale
CameraComponent, // Camera settings
DirectionalLightComponent, // Directional light
PointLightComponent, // Point light
SpotLightComponent, // Spot light
MeshComponent, // Mesh registry reference
MaterialComponent, // Material registry reference
RenderableTag, // Marks entity for rendering
} from '@web-engine-dev/renderer';Systems
import {
TransformPropagationSystem, // Parent-child transform hierarchy
FrustumCullingSystem, // Visibility culling
ForwardRenderSystem, // Main render pass
} from '@web-engine-dev/renderer';Registries
Meshes, materials, and textures are stored in registries (ECS resources) and referenced by ID from components:
// Register a mesh in the registry
const meshRegistry = world.getResource(MeshRegistryResource);
const meshId = meshRegistry.add(mesh);
// Reference it from a component
world.spawn(
Transform3D.create({ position: [0, 1, 0] }),
MeshComponent.create({ meshId }),
MaterialComponent.create({ materialId }),
RenderableTag,
);WebGPU Best Practices
Buffer Alignment (std140)
All uniform buffers follow std140 layout rules. The most common pitfall is vec3f alignment:
| Type | Size | Alignment |
|---|---|---|
f32 | 4 | 4 |
vec2f | 8 | 8 |
vec3f | 12 | 16 |
vec4f | 16 | 16 |
mat4x4f | 64 | 16 |
// Correct: pack f32 after vec3f to fill padding
struct ModelUniforms {
position: vec3f, // 12 bytes at offset 0
intensity: f32, // 4 bytes at offset 12 (fills vec3 padding)
color: vec3f, // 12 bytes at offset 16
_pad: f32, // 4 bytes at offset 28
}Resource Lifecycle
GPU resources must be properly disposed to prevent memory leaks. The renderer supports deferred deletion -- old resources are queued for disposal 2-3 frames after replacement, allowing in-flight GPU commands to complete:
// Pattern for dynamic resource changes
const newTexture = await loadTexture(url);
deferredDeletionQueue.queue(() => oldTexture.dispose(), 3); // Dispose after 3 framesFurther Reading
- Rendering Concepts -- Practical guide to rendering with the engine
- Renderer Package -- Full API reference
- Shader Compiler Package -- WGSL tooling and transpilation utilities
- ADR-003: WebGPU-First Rendering -- Decision record