Skip to content

Render Pipeline

Visualization architecture for the particle simulation.

Overview

The render pipeline transforms particle physics state into visual output through a multi-stage process optimized for WebGPU.

Pipeline Architecture

Four-Pass Rendering

Pass 1: Compute

Physics simulation (covered in Compute Shader Design).

Pass 2: Trail

wgsl
@fragment
fn fragmentMain() -> vec4f {
    return vec4f(0.0, 0.0, 0.0, TRAIL_FADE_ALPHA);
}

Fades the persistent offscreen texture by 5% each frame.

Pass 3: Render

Draws particles as colored points:

wgsl
@vertex
fn vertexMain(vertexIndex: u32) -> VertexOutput {
    let particle = particles[vertexIndex];
    let ndc = vec2f(
        particle.x / width * 2.0 - 1.0,
        particle.y / height * 2.0 - 1.0
    );

    let speed = length(particle.velocity);
    let t = clamp(speed / MAX_SPEED, 0.0, 1.0);

    return VertexOutput(ndc, t);
}

@fragment
fn fragmentMain(speedFactor: f32) -> vec4f {
    let color = mix(CYAN, PURPLE, speedFactor);
    let brightness = 0.5 + speedFactor * 0.5;
    return vec4f(color * brightness, 1.0);
}

Pass 4: Present

Composites the offscreen texture to the screen with bilinear sampling.

Color Mapping

SpeedColorRGB
0Cyan(0, 1, 1)
400Interpolated(0.45, 0.65, 1)
800Purple(0.9, 0.3, 1)

Brightness scaling: 50% at rest → 100% at max speed.

Offscreen Texture Strategy

Benefits:

  • Trail persistence without swapchain dependency
  • Consistent behavior across browsers
  • Easy HiDPI handling

HiDPI Support

typescript
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
DisplayCanvas SizeParticles Rendered
1080p @ 1x1920×1080Standard density
1080p @ 2x (Retina)3840×21604x pixel density
4K @ 1x3840×21604x area

Point Rendering

wgsl
// Vertex shader output
@builtin(position) position: vec4f

// Automatic point size (WebGPU default: 1.0)
// Particles render as single pixels

For larger particles, modify the vertex shader:

wgsl
@vertex
fn vertexMain(...) -> VertexOutput {
    output.pointSize = 2.0; // 2x2 pixel particles
    return output;
}

Pipeline Configuration

typescript
const renderPipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vertexMain',
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fragmentMain',
    targets: [
      {
        format: offscreenTextureFormat,
        blend: {
          color: { srcFactor: 'src-alpha', dstFactor: 'one' },
          alpha: { srcFactor: 'one', dstFactor: 'one' },
        },
      },
    ],
  },
  primitive: {
    topology: 'point-list',
  },
});

Blend Modes

PassBlend ModePurpose
Trailsrc-alpha * src + dst * (1 - src)Fade overlay
Rendersrc + dst (additive)Bright particle accumulation
Presentsrc (replace)Direct copy

Performance Notes

OptimizationImpact
Point primitivesMinimal vertex processing
Offscreen texture reuseNo per-frame allocation
Additive blendingNo readback needed
Single bufferDirect vertex access

Source Files

FilePurpose
src/shaders/render.wgslParticle rendering
src/shaders/trail.wgslTrail fade effect
src/shaders/present.wgslScreen compositing
src/core/pipelines.tsPipeline creation

Next Steps

Built with VitePress