Module: Frame-Graph Post-Process
Package paths:
packages/babylon-lite/src/frame-graph/,packages/babylon-lite/src/post-process/
Purpose
Frame-graph post-process tasks run a full-screen WGSL fragment pass over a source render-target color texture and write the result into either a caller-provided render target or an internally-owned render target. The helper is intentionally smaller than Babylon.js FrameGraphPostProcessTask: it has no ThinPostProcess, no shader store lookup, no effect abstraction, and no raw WebGPU handles in the public API.
The reusable helper exists because most post-processes share the same shape:
- one source color texture;
- one sampler selected by
sourceSamplingMode; - one optional uniform block;
- one full-screen triangle;
- one color output target;
- optional alpha blending and normalized viewport/scissor.
Mesh blending is deliberately implemented as a dedicated task rather than
through createPostProcessTask: its tag input is texture_2d<u32>, every input
uses exact textureLoad, it owns deterministic rg8unorm blue noise, and its
quality/debug/base-color options specialize a large shader. The dedicated
module remains fully tree-shakable and follows the same source/target,
viewport, clear, alpha-mode, device-rebuild, and owned-internal-target
conventions. See 55-mesh-blending.md for the complete contract.
Public API Surface (types, functions, constants — full signatures)
export type PostProcessSamplingMode = "nearest" | "linear";export type PostProcessAlphaMode = 0 | 1 | 2 | 7;
export interface PostProcessShaderConfig { fragmentWGSL: string; uniformWGSL?: string; uniformByteLength?: number; uniformBinding?: number; writeUniforms?: (data: Float32Array) => void; extraTextureWGSL?: string; extraTextures?: readonly RenderTarget[];}
export interface PostProcessTaskSettings { name?: string; sourceTexture: RenderTarget; sourceSamplingMode?: PostProcessSamplingMode; targetTexture?: RenderTarget | null; alphaMode?: PostProcessAlphaMode; viewport?: NormalizedViewport | null; clear?: boolean;}
export interface PostProcessTaskConfig extends PostProcessTaskSettings { _shader: PostProcessShaderConfig;}
export interface PostProcessTask extends Task, PostProcessTaskSettings { readonly name: string; sourceSamplingMode: PostProcessSamplingMode; targetTexture: RenderTarget | null; alphaMode: PostProcessAlphaMode; viewport: NormalizedViewport | null; outputTexture: RenderTarget; updateUniforms(): void;}
export function createPostProcessTask(config: PostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): PostProcessTask;
export interface BlackAndWhitePostProcessTaskConfig extends Omit<PostProcessTaskConfig, "_shader"> { degree?: number;}
export interface BlackAndWhitePostProcessTask extends PostProcessTask { degree: number;}
export function createBlackAndWhitePostProcessTask(config: BlackAndWhitePostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): BlackAndWhitePostProcessTask;
export interface AnaglyphPostProcessTaskConfig extends Omit<PostProcessTaskConfig, "_shader"> { leftTexture: RenderTarget;}
export function createAnaglyphPostProcessTask(config: AnaglyphPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): PostProcessTask;
export interface BlurPostProcessTaskConfig extends Omit<PostProcessTaskConfig, "_shader"> { direction?: { x: number; y: number }; kernel?: number;}
export interface BlurPostProcessTask extends PostProcessTask { direction: { x: number; y: number }; kernel: number;}
export function createBlurPostProcessTask(config: BlurPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): BlurPostProcessTask;
export interface ExtractHighlightsPostProcessTaskConfig extends Omit<PostProcessTaskConfig, "_shader"> { threshold?: number; exposure?: number;}
export interface ExtractHighlightsPostProcessTask extends PostProcessTask { threshold: number; exposure: number;}
export function createExtractHighlightsPostProcessTask( config: ExtractHighlightsPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): ExtractHighlightsPostProcessTask;
export interface ChromaticAberrationPostProcessTaskConfig extends Omit<PostProcessTaskConfig, "_shader"> { aberrationAmount?: number; direction?: { x: number; y: number }; radialIntensity?: number; centerPosition?: { x: number; y: number };}
export interface ChromaticAberrationPostProcessTask extends PostProcessTask { aberrationAmount: number; direction: { x: number; y: number }; radialIntensity: number; centerPosition: { x: number; y: number };}
export function createChromaticAberrationPostProcessTask( config: ChromaticAberrationPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): ChromaticAberrationPostProcessTask;
export interface BloomPostProcessTaskConfig extends PostProcessTaskSettings { weight?: number; kernel?: number; threshold?: number; exposure?: number; bloomScale?: number;}
export interface BloomPostProcessTask extends Task, PostProcessTaskSettings { readonly name: string; sourceTexture: RenderTarget; targetTexture: RenderTarget | null; outputTexture: RenderTarget; weight: number; kernel: number; threshold: number; exposure: number; readonly bloomScale: number; updateUniforms(): void;}
export function createBloomPostProcessTask(config: BloomPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): BloomPostProcessTask;
export enum MeshBlendQuality { Low = 0, Medium = 1, High = 2, Cinematic = 3,}
export enum MeshBlendDepthType { View = 0, Screen = 1,}
export enum MeshBlendDebugMode { Off = 0, PackedTag = 1, CandidateDirectionDistance = 2, SeamFade = 3, RejectionReason = 4, StageWork = 5, Continuation = 6, TinyObject = 7, MultiTarget = 8, TargetColor = 9, ShadowAttenuation = 10, ColorInterpolation = 11, WorldPosition = 12,}
export interface MeshBlendingPostProcessTaskConfig { name?: string; sourceTexture: RenderTarget; meshBlendTagTexture: RenderTarget; depthTexture: RenderTarget; baseColorTexture?: RenderTarget | null; targetTexture?: RenderTarget | null; camera: Camera; quality?: MeshBlendQuality; depthType?: MeshBlendDepthType; debugMode?: MeshBlendDebugMode; radiusClasses?: readonly MeshBlendRadiusDefinition[]; slopeFactor?: number; enabled?: boolean; alphaMode?: PostProcessAlphaMode; viewport?: NormalizedViewport | null; clear?: boolean;}
export function createMeshBlendingPostProcessTask(config: MeshBlendingPostProcessTaskConfig, engine: EngineContext, scene?: SceneContext): MeshBlendingPostProcessTask;
export interface FrameGraphContextOptions { name?: string; clearColor?: GPUColorDict; update?: (deltaMs: number) => void;}
export interface FrameGraphContext extends RenderingContext { readonly name: string; readonly engine: EngineContext; readonly frameGraph: FrameGraph; clearColor: GPUColorDict;}
export function createFrameGraphContext(engine: EngineContext, options?: FrameGraphContextOptions): FrameGraphContext;export function registerFrameGraphContext(ctx: FrameGraphContext): void;export function unregisterFrameGraphContext(ctx: FrameGraphContext): void;export function disposeFrameGraphContext(ctx: FrameGraphContext): void;PostProcessShaderConfig and PostProcessTaskConfig are implementation-facing types used by concrete post-process factories; _shader is intentionally internal and is omitted from public concrete post-process configs.
The Babylon.js property names are preserved on PostProcessTask: sourceTexture, sourceSamplingMode, targetTexture, alphaMode, viewport, and outputTexture. The contract is RenderTarget-focused because Lite's public Texture2D wrapper does not carry enough attachment metadata to create a same-format/same-size render target. updateUniforms() is the shared explicit hook concrete post-processes use when the caller mutates exposed parameters such as BlackAndWhitePostProcessTask.degree.
Internal Architecture (data structures, memory layouts)
createPostProcessTask returns a plain task record with internal slots hidden from the exported interface:
_internalTarget: the render target used whentargetTextureisnull;_pipeline: one render pipeline for the current target format/sample count/alpha mode;_bindGroup: source texture + sampler + optional uniform buffer + optional extra textures;_uniformBuffer: optional uniform buffer sized by_shader.uniformByteLength;_renderPassDescriptorand_colorAttachment: cached render-pass state patched each frame for swapchain targets;_shaderModule: WGSL module combining the helper vertex stage, source bindings, optional uniform declarations, userfragmentWGSL, and the generated fragment entry point.
The helper creates no module-level caches. Pipeline state belongs to the task and is rebuilt during record() after the source producer has allocated the source texture and after the target has been allocated.
Uniform memory is caller-defined. If uniformByteLength is set, it is rounded up to 16 bytes, a Float32Array scratch buffer of that size is created, writeUniforms(data) fills it during record() and whenever updateUniforms() is called. Uniform bytes default to binding 2 + extraTextures.length, or to _shader.uniformBinding when a concrete task needs an explicit slot. Extra textures are implementation-facing RenderTargets paired with _shader.extraTextureWGSL declarations; anaglyph binds the left-eye texture, and bloom merge binds the blurred highlight texture.
Pipeline Configuration (vertex/fragment stages, bind groups, depth/stencil)
Pipeline:
- topology:
triangle-list; - draw count:
3; - vertex buffers: none;
- depth/stencil: none;
- multisample count:
outputTexture._descriptor.sampleCount; - color format:
outputTexture._descriptor.colorFormat; - blend: derived from
alphaMode.
Bind group 0:
| Binding | Resource | Visibility |
|---|---|---|
0 | source sampler (nearest or linear) | fragment |
1 | source color texture (texture_2d<f32>) | fragment |
2+ | optional extra textures (texture_2d<f32>) | fragment |
2 + extraTextures.length or _shader.uniformBinding | optional uniform buffer | fragment |
alphaMode mapping follows the existing Lite/Babylon.js numeric modes used by node-material pipelines:
0: opaque/no blending;1: additive (src-alpha,one);2: standard combine (src-alpha,one-minus-src-alpha);7: premultiplied (one,one-minus-src-alpha).
viewport, when present, is normalized like camera viewports and follows Babylon's bottom-origin convention. It is converted to pixel viewport and scissor rectangles using the output target dimensions. clear controls the color attachment load operation and defaults to true; multi-viewport demos render several post-process tasks into the same target by clearing only the first task.
Shader Logic (WGSL outline or pseudocode with exact math)
The helper owns the vertex stage and fragment wrapper:
struct PostProcessVertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f,};
@vertexfn postProcessVertex(@builtin(vertex_index) vertexIndex: u32) -> PostProcessVertexOutput { var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); let p = positions[vertexIndex]; var out: PostProcessVertexOutput; out.position = vec4f(p, 0.0, 1.0); // V is flipped: clip space is +Y up, texture space is +Y down. out.uv = vec2f(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5); return out;}
@group(0) @binding(0) var sourceSampler: sampler;@group(0) @binding(1) var sourceTextureSampler: texture_2d<f32>;
fn samplePostProcessSource(uv: vec2f) -> vec4f { return textureSample(sourceTextureSampler, sourceSampler, uv);}
// Texel-snapped read. Takes a NORMALIZED uv, like samplePostProcessSource —// not a pixel coordinate — and clamps to the texel centres so a snapped fetch// cannot bleed outside the source.fn readPostProcessSource(uv: vec2f) -> vec4f { let dims = vec2f(textureDimensions(sourceTextureSampler)); let p = clamp(floor(uv * dims) + vec2f(0.5), vec2f(0.5), dims - vec2f(0.5)); return textureSampleLevel(sourceTextureSampler, sourceSampler, p / dims, 0);}
// user fragmentWGSL must define:fn applyPostProcess(color: vec4f, uv: vec2f) -> vec4f;
@fragmentfn postProcessFragment(input: PostProcessVertexOutput) -> @location(0) vec4f { return applyPostProcess(samplePostProcessSource(input.uv), input.uv);}The black-and-white task declares a uniform degree and uses Babylon.js shader math:
let luminance = dot(color.rgb, vec3f(0.3, 0.59, 0.11));let grayscale = vec3f(luminance);return vec4f(mix(color.rgb, grayscale, clamp(params.degree, 0.0, 1.0)), color.a);The current concrete post-processes mirror their Babylon.js thin post-process shader math:
- black-and-white mixes the source color with luminance by
degree; - anaglyph samples the configured left-eye texture and the source/right-eye texture and combines red/cyan channels;
- blur writes Babylon-style optimized Gaussian offsets/weights into uniforms and samples along
direction / textureDimensions(source); - extract-highlights keeps source pixels whose exposure-scaled luminance is above
threshold^(1/2.2)and blacks out the rest; - chromatic aberration applies the Babylon radial channel offsets using
aberrationAmount,direction,radialIntensity,centerPosition, and the source texture dimensions captured duringrecord(). - bloom is a composite task that records/executes four internal post-process passes: reusable extract-highlights, horizontal Gaussian blur, vertical Gaussian blur, and merge (
source.rgb + blurred.rgb * weight). Its internal extract/blur render targets are sized tofloor(sourceSize * bloomScale)on every record, and itskernelsetter scales the internal blur kernels bybloomScale, matching Babylon.js frame-graph bloom.
State Machine / Lifecycle
- Caller creates a producer whose target owns a single-sample color texture. The producer can be a scene
RenderTask, anEffectRenderTask, or any custom task. - Scene pipelines use the scene's frame graph as before. Scene-less pipelines create
const ctx = createFrameGraphContext(engine, { update }); the update callback runs once per frame before graph execution and is the right place to update effect uniforms. - Caller creates the post-process task with
sourceTextureset to the producer target. IftargetTextureis omitted, the task creates an internal color-only target using the source descriptor's format, sample count, and size. If the post-process writes the canvas, use a target withresolveToSwapchain: true. - Caller schedules tasks in order with
addTask(sceneOrFrameGraph, producer), thenaddTask(sceneOrFrameGraph, postProcess), then any consumer/output task. - Scene-less graphs are started with
registerFrameGraphContext(ctx), which buildsctx.frameGraphand registers the rendering context with the engine. Later canvas resizes rebuild the graph through the context_resizehook. FrameGraph.build()calls the producerrecord()first, allocating source texture resources.- Post-process
record()validates the source color texture, allocates/rebuilds the output target, creates the pipeline, bind group, and optional uniform buffer, initializes uniforms, and records no separatePassbecause the task executes directly. - If a caller mutates exposed post-process parameters after recording, they call
updateUniforms()to rewrite the task UBO before the next draw. - Each frame,
execute()patches swapchain attachments if needed, applies viewport/scissor if requested, binds pipeline/group, and draws one full-screen triangle. dispose()destroys the optional uniform buffer and disposes only the internally-owned target. Caller-ownedsourceTextureandtargetTexturelifetimes remain with their owners.disposeFrameGraphContext(ctx)unregisters a scene-less graph and disposes all tasks in it; repeated disposal is a no-op.
Babylon.js Equivalence Map
| Babylon.js | Babylon Lite |
|---|---|
FrameGraphPostProcessTask | PostProcessTask plain task record |
sourceTexture handle | RenderTarget source with sampled color attachment |
targetTexture handle | optional RenderTarget output |
| auto-created output texture | internal RenderTarget copied from source descriptor |
sourceSamplingMode | "nearest" / "linear" sampler selection |
alphaMode | numeric modes 0, 1, 2, 7 mapped to WebGPU blend state |
viewport | normalized viewport/scissor on the output pass |
ThinPostProcess | not present; shader config is direct WGSL |
FrameGraphBlackAndWhiteTask | createBlackAndWhitePostProcessTask |
FrameGraphAnaglyphTask / ThinAnaglyphPostProcess | createAnaglyphPostProcessTask |
FrameGraphBlurTask / ThinBlurPostProcess | createBlurPostProcessTask |
FrameGraphExtractHighlightsTask / ThinExtractHighlightsPostProcess | createExtractHighlightsPostProcessTask |
FrameGraphChromaticAberrationTask / ThinChromaticAberrationPostProcess | createChromaticAberrationPostProcessTask |
FrameGraphBloomTask / BloomEffect | createBloomPostProcessTask |
Dependencies
engine/engine.tsfor internal device/encoder/swapchain state;engine/render-target.tsfor source/output target allocation and disposal;resource/samplers.tsfor shared nearest/linear samplers;scene/scene-core.tsonly for scene-owned frame graphs; standalone frame-graph contexts avoid runtime scene imports;camera/camera.tsonly for the publicNormalizedViewporttype.
Test Specification
- Scene 142 renders a deterministic colored mesh scene into offscreen
RenderTargets, then writes four viewport quadrants with black-and-white, anaglyph, 128px blur, and chromatic aberration. The Babylon.js reference uses the matching Babylon post-process classes. - Scene 143 renders Sponza through two 16px blur tasks (
direction=(1,0)thendirection=(0,1)) followed by chromatic aberration, demonstrating post-process pipelining through internally-created intermediate targets. The Babylon.js reference uses the matching camera-attached Babylon post-process classes. - Scene 144 renders the Tarisland dragon asset from Babylon.js Playground
#SUEU9U#114, freezesQishilong_attack01at frame 180, and runs the composite bloom post-process with weight2, threshold0.1, kernel64, and bloom scale0.5. The Babylon.js reference usesBloomEffectthrough aPostProcessRenderPipeline. - The parity spec
tests/lite/parity/scenes/scene142-black-and-white-post-process.spec.tscaptures/usesreference/lite/scene142-post-process-viewports/babylon-ref-golden.pngand compares Lite output againstscene-config.json. - The parity spec
tests/lite/parity/scenes/scene143-pipelined-post-processes.spec.tscaptures/usesreference/lite/scene143-pipelined-post-processes/babylon-ref-golden.pngand compares Lite output againstscene-config.json. - The parity spec
tests/lite/parity/scenes/scene144-bloom-post-process.spec.tscaptures/usesreference/lite/scene144-bloom-post-process/babylon-ref-golden.pngand compares Lite output againstscene-config.json. - Bundle-size coverage adds Scene 142, Scene 143, and Scene 144 ceiling entries without changing existing ceilings.
File Manifest
packages/babylon-lite/src/frame-graph/post-process-task.tspackages/babylon-lite/src/post-process/black-and-white.tspackages/babylon-lite/src/post-process/anaglyph.tspackages/babylon-lite/src/post-process/blur.tspackages/babylon-lite/src/post-process/extract-highlights.tspackages/babylon-lite/src/post-process/chromatic-aberration.tspackages/babylon-lite/src/post-process/bloom.tsdocs/lite/architecture/29-post-process.mdlab/lite/src/bjs/scene142.tslab/lite/src/lite/scene142.tslab/lite/src/bjs/scene143.tslab/lite/src/lite/scene143.tslab/lite/src/bjs/scene144.tslab/lite/src/lite/scene144.tslab/lite/babylon-ref-scene142.htmllab/lite/scene142.htmllab/lite/babylon-ref-scene143.htmllab/lite/scene143.htmllab/lite/babylon-ref-scene144.htmllab/lite/scene144.htmltests/lite/parity/scenes/scene142-black-and-white-post-process.spec.tstests/lite/parity/scenes/scene143-pipelined-post-processes.spec.tstests/lite/parity/scenes/scene144-bloom-post-process.spec.ts