Module: Effect Renderer
Package path:
packages/babylon-lite/src/effect/
Purpose
The effect renderer module provides a Lite-native equivalent of Babylon.js EffectRenderer / EffectWrapper for fullscreen shader work. It is intentionally WebGPU/WGSL-first and keeps the simplest swapchain path out of the scene frame graph:
- effects are pure-state wrapper handles;
- behaviour is exposed through standalone functions;
- uniforms, textures, and samplers are declared explicitly through
EffectBindingLayout; - fullscreen geometry is the standard single triangle generated from
@builtin(vertex_index); - swapchain-only effects register as a direct engine rendering context, so no
SceneContextor default sceneRenderTaskis needed; - offscreen render-to-texture effects are scheduled as a
Taskin the existing sceneFrameGraph; - uniform-only offscreen effects can use the smaller
UniformEffectWrapperpath when a fullscreen shader needs exactly one uniform buffer and no textures or samplers; - frame-graph task targets are existing
RenderTargets; - user-facing resources remain Lite handles (
Texture2D,RenderTarget), never raw WebGPU handles.
This module is meant for post-processes, procedural fullscreen passes, render-to-texture effects, copy/blit utilities, and future replacement of ad hoc fullscreen passes.
Public API Surface (types, functions, constants — full signatures)
export type EffectBindingKind = "uniform" | "texture" | "sampler";
export interface EffectBindingLayout { name?: string; binding: number; kind: EffectBindingKind; visibility?: GPUShaderStageFlags; uniformByteLength?: number; textureSampleType?: GPUTextureSampleType; samplerType?: GPUSamplerBindingType; textureBinding?: string | number;}
export interface EffectWrapperOptions { name?: string; fragmentWGSL: string; vertexWGSL?: string; bindings?: EffectBindingLayout[]; blend?: GPUBlendState;}
export interface EffectWrapper { readonly name: string; readonly options: EffectWrapperOptions;}
// ─── Direct swapchain renderer (no SceneContext / frame graph) ───────
export interface EffectRendererOptions { name?: string; clear?: boolean; clearColor?: GPUColorDict;}
export interface EffectRenderer extends RenderingContext { readonly name: string;}
export function createEffectRenderer(engine: EngineContext, effect: EffectWrapper, options?: EffectRendererOptions): EffectRenderer;export function registerEffectRenderer(er: EffectRenderer): void;export function unregisterEffectRenderer(er: EffectRenderer): void;export function disposeEffectRenderer(er: EffectRenderer): void;
// ─── Frame-graph task (use for offscreen RTT workflows) ──────────────
export interface EffectRenderTaskConfig { name: string; effect: EffectWrapper; target: RenderTarget; clear?: boolean; clearColor?: GPUColorDict;}
export interface EffectRenderTask extends Task { readonly name: string; /** Optional draw region, normalized to the target, origin bottom-left (the `NormalizedViewport` convention). */ scissor?: NormalizedViewport | null; readonly _config: EffectRenderTaskConfig; readonly _rt: RenderTarget;}
export function createEffectWrapper(engine: EngineContext, options: EffectWrapperOptions): EffectWrapper;export function setEffectUniforms(wrapper: EffectWrapper, data: ArrayBuffer | ArrayBufferView | Record<string | number, ArrayBuffer | ArrayBufferView>): void;export function setEffectTexture(wrapper: EffectWrapper, bindingNameOrIndex: string | number, texture: Texture2D): void;export function createEffectRenderTask(config: EffectRenderTaskConfig, engine: EngineContext, scene: SceneContext): EffectRenderTask;export function disposeEffectWrapper(wrapper: EffectWrapper): void;
// ─── Uniform-only frame-graph task (smaller bundle path) ─────────────
export interface UniformEffectWrapperOptions { name?: string; fragmentWGSL: string; vertexWGSL?: string; uniformByteLength: number;}
export interface UniformEffectWrapper { readonly name: string; readonly options: UniformEffectWrapperOptions;}
export interface UniformEffectRenderTaskConfig { name: string; effect: UniformEffectWrapper; target: RenderTarget; clear?: boolean; clearColor?: GPUColorDict;}
export interface UniformEffectRenderTask extends Task { readonly name: string; readonly _config: UniformEffectRenderTaskConfig; readonly _rt: RenderTarget;}
export function createUniformEffectWrapper(engine: EngineContext, options: UniformEffectWrapperOptions): UniformEffectWrapper;export function setUniformEffectUniforms(wrapper: UniformEffectWrapper, data: ArrayBuffer | ArrayBufferView): void;export function createUniformEffectRenderTask(config: UniformEffectRenderTaskConfig, engine: EngineContext, scene?: SceneContext): UniformEffectRenderTask;export function disposeUniformEffectWrapper(wrapper: UniformEffectWrapper): void;Internal Architecture (data structures, memory layouts)
EffectWrapper is a plain public state object with internal slots hidden from the exported type. Internally it owns:
- one combined WGSL shader module (
vertexWGSL + fragmentWGSL); - one bind-group layout derived from the explicit
EffectBindingLayout[]; - one pipeline layout;
- a lazy per-wrapper pipeline cache keyed by
targetSignatureKey(RenderTargetSignature); - uniform slots keyed by binding number/name;
- texture slots keyed by binding number/name;
- one cached bind group rebuilt when uniforms/textures change.
No module-level Map, WeakMap, or Set allocation is used. Pipeline caches live on wrappers and are created lazily.
Uniform data is copied into wrapper-owned uniform buffers. setEffectUniforms(wrapper, data) supports:
- a single
ArrayBuffer/ typed-array payload, written to the first uniform binding; - a record whose keys are binding names or numeric binding indices, written to matching uniform bindings.
setEffectTexture(wrapper, bindingNameOrIndex, texture) stores the Texture2D handle on the texture binding. Sampler bindings use either:
- the texture identified by
textureBinding, if supplied; - the first texture slot, otherwise.
This allows the common texture + sampler pair without exposing GPUTextureView or GPUSampler in the public API.
UniformEffectWrapper is the same fullscreen-triangle effect shape specialized for the smallest uniform-only case: it creates one uniform buffer at group 0 / binding 0, one bind-group layout, one bind group, and one render task. It intentionally omits texture/sampler binding discovery, blend configuration, direct swapchain rendering, and multi-uniform lookup so simple procedural frame-graph demos do not pay for the generic EffectWrapper binding machinery.
Pipeline Configuration (vertex/fragment stages, bind groups, depth/stencil)
Default vertex stage:
struct EffectVertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>,};
@vertexfn effectFullscreenVertex(@builtin(vertex_index) vertexIndex: u32) -> EffectVertexOutput { var positions = array<vec2<f32>, 3>(vec2<f32>(-1.0, -1.0), vec2<f32>(3.0, -1.0), vec2<f32>(-1.0, 3.0)); let p = positions[vertexIndex]; var out: EffectVertexOutput; out.position = vec4<f32>(p, 0.0, 1.0); out.uv = p * 0.5 + vec2<f32>(0.5, 0.5); return out;}Pipeline state:
- topology:
triangle-list; - draw count:
3; - no vertex/index buffers;
- no depth/stencil attachment;
- color target format and sample count come from the renderer/task target;
- blend is
options.blendor disabled; - culling is off.
The fragment entry point is always effectFragment.
Shader Logic (WGSL outline or pseudocode with exact math)
User fragments are supplied as WGSL and must define:
@fragmentfn effectFragment(input: EffectVertexOutput) -> @location(0) vec4<f32> { // input.uv follows Babylon.js post-process coordinates: // bottom-left triangle vertex maps to (0,0), top-left screen pixels approach y=1.}If a custom vertexWGSL is supplied, it must provide an @vertex entry point named effectFullscreenVertex.
State Machine / Lifecycle
Swapchain-only path (EffectRenderer)
createEffectWrapper(engine, options)returns a pure-state wrapper. Shader/layout objects are created lazily on first use.- User code calls
setEffectUniformsand/orsetEffectTexture. createEffectRenderer(engine, effect, options?)creates anEffectRendererthat implementsRenderingContextdirectly. It owns an internal swapchainRenderTargetand noSceneContextis needed.registerEffectRenderer(er)registers the renderer with the engine.startEngine(engine)starts the render loop. Each frame:_record()rebuilds the RT if the canvas resized, patches swapchain views, encodes one draw call.disposeEffectRenderer(er)unregisters and frees the swapchainRenderTarget.disposeEffectWrapper(wrapper)destroys wrapper-owned uniform buffers and clears GPU references.
Offscreen RTT path (createEffectRenderTask)
createEffectWrapper(engine, options)— same as above.createRenderTargetTexture(engine, descriptor)returns{ rt, texture }for the offscreen target.createEffectRenderTask(config, engine, scene)creates a frame-graphTask. Iftargetis aRenderTarget, the task renders into it; theTexture2Dfrom step 2 can then be bound to a scene material.addTaskAtStart(scene, task)(oraddTask) schedules the pass before the scene's default render pass.registerScene/startEngineas normal.- Optional: set
task.scissor(any frame) to rasterize only that normalized rectangle of the target without changing the fullscreen viewport or UVs.execute()converts it to pixels with the same rounding as a post-process viewport (flooron the low edges,ceilon the high edges), clamps it to the target, and callssetScissorRectbefore the draw. A non-positive normalized width/height or an empty clamped rectangle ends the pass without drawing and returns0; a draw returns1. A clear still happens whenclearis on, since the clear belongs to the attachment.nullor absent draws the whole target. The region and current target dimensions are read on each execution, so mutations and target resizing require no re-recording. disposeEffectWrapper(wrapper)when done.
Uniform-only frame-graph path (createUniformEffectRenderTask)
createUniformEffectWrapper(engine, { fragmentWGSL, uniformByteLength })creates a smaller effect wrapper with one uniform buffer at binding 0.- User code calls
setUniformEffectUniforms(wrapper, data)before each draw that needs updated uniform data. createUniformEffectRenderTask(config, engine, scene?)creates a frame-graphTaskfor an explicitRenderTarget. It can be scheduled in a scene frame graph or a standaloneFrameGraphContext.- The task records a single fullscreen triangle pipeline and binds the wrapper's uniform bind group at group 0.
disposeUniformEffectWrapper(wrapper)destroys the wrapper-owned uniform buffer.
Babylon.js Equivalence Map
| Babylon.js | Babylon Lite |
|---|---|
EffectWrapper | EffectWrapper pure-state handle |
EffectRenderer.render(wrapper) (swapchain) | createEffectRenderer(engine, effect) + registerEffectRenderer — no scene needed |
EffectRenderer.render(wrapper, outputTexture) (RTT) | createEffectRenderTask({ effect, target: rt }) scheduled in FrameGraph |
| uniform-only fullscreen pass | createUniformEffectWrapper + createUniformEffectRenderTask |
| fullscreen quad/index buffer | vertex-index fullscreen triangle |
onApplyObservable | user calls setEffectUniforms / setEffectTexture before the pass executes |
| current framebuffer / RTT | direct EffectRenderer / frame-graph RenderTarget task |
effect.setTexture("name", texture) | setEffectTexture(wrapper, "name", texture2D) |
The API intentionally does not implement Babylon.js shader-store lookup, GLSL include processing, observables, raw render-target wrappers, or WebGL compatibility.
Dependencies
engine/engine.tsforEngineContext/ internal device access;engine/render-target.tsforRenderTarget,buildRenderTarget,disposeRenderTarget, andtargetSignatureKey;frame-graph/task.tsfor task polymorphism;resource/gpu-pool.tsfor sampler reuse throughTexture2D.sampler;scene/scene-core.tsfor scene ownership and frame-graph scheduling;texture/texture-2d.tsfor public texture handles.
Test Specification
effect-render-task-scissor.test.tsrecords render-pass calls to verify absent/null behavior, bottom-left origin, outward pixel rounding, clamping on every edge, zero/negative dimensions (including fractional edges), off-target rectangles, live updates/resizing, draw counts, and attachment clears for empty regions. Scissoring never changes the viewport or fullscreen UV mapping.- Scene 74 renders a deterministic fullscreen procedural effect through Babylon.js
EffectRendererand Babylon Lite's direct effect renderer. - Scene 75 renders an effect into a
RenderTargetand maps that texture onto a sphere, matching the Babylon.js playground-style RTT workflow. - Scene 76 binds a
Texture2DthroughsetEffectTexture()and samples it with an associated sampler binding through the direct effect renderer. - The parity tests capture/use the
reference/lite/scene74-effect-renderer,reference/lite/scene75-effect-rtt-sphere, andreference/lite/scene76-effect-texturegoldens, then assert full-image MAD againstscene-config.json. - Bundle-size accounting uses scene-specific
maxRawKBentries for the new effect scenes only; existing ceilings are untouched.
File Manifest
tests/lite/unit/effect-render-task-scissor.test.tspackages/babylon-lite/src/effect/effect-renderer.tspackages/babylon-lite/src/effect/uniform-effect-renderer.tsdocs/lite/architecture/30-effect-renderer.mdlab/lite/src/bjs/scene74.tslab/lite/babylon-ref-scene74.htmllab/lite/src/lite/scene74.tslab/lite/scene74.htmltests/lite/parity/scenes/scene74-effect-renderer.spec.tsreference/lite/scene74-effect-renderer/babylon-ref-golden.pnglab/public/thumbnails/scene74.jpglab/lite/src/bjs/scene75.tslab/lite/babylon-ref-scene75.htmllab/lite/src/lite/scene75.tslab/lite/scene75.htmltests/lite/parity/scenes/scene75-effect-rtt-sphere.spec.tsreference/lite/scene75-effect-rtt-sphere/babylon-ref-golden.pnglab/public/thumbnails/scene75.jpglab/lite/src/bjs/scene76.tslab/lite/babylon-ref-scene76.htmllab/lite/src/lite/scene76.tslab/lite/scene76.htmltests/lite/parity/scenes/scene76-effect-texture.spec.tsreference/lite/scene76-effect-texture/babylon-ref-golden.pnglab/public/thumbnails/scene76.jpg