compose-pass.frag.ts
PURPOSE
GLSL ES 3.00 fragment shader source (exported as a JS string) for the VFX-workbench layer-stack composite pass. Samples two RGBA inputs (u_dst = accumulator, u_src = incoming layer) and blends them via one of five premultiplied-alpha blend modes selected by u_mode, writing the result to outColor.
OWNS
- Default export: a single template literal containing the full
#version 300 esfragment-shader source (precisionhighp float). - Five branched blend operations switched on
u_mode:0additive:out = dst + src1alpha (Porter-Duff “over”, premultiplied):out = src + dst * (1 - src.a)2multiply:out = vec4(dst.rgb * src.rgb, max(dst.a, src.a))3screen:out = vec4(1 - (1 - dst.rgb) * (1 - src.rgb), max(dst.a, src.a))4erase:out = dst * (1 - src.a)- fallback (any other int):
out = dst(passthrough)
- Application of
u_opacitytosrconce, before the blend branch (src = texture(u_src, v_uv) * u_opacity).
READS FROM
uniform sampler2D u_dst— accumulator RGBA texture (already-composed layers).uniform sampler2D u_src— incoming layer’s RGBA output (premultiplied alpha).uniform int u_mode— blend-mode selector; encoding must stay in sync withLayerBlendModeincomponent-schema.ts.uniform float u_opacity— 0..1 multiplier applied tosrcbefore blending.in vec2 v_uv— varying UV coordinate from the upstream vertex stage (full-screen quad).
PUSHES TO
out vec4 outColor— the framebuffer color attachment bound by the host pass (the next accumulator target in the ping-pong composite chain).
DOES NOT
- Does not unpremultiply, gamma-correct, or tonemap inputs/outputs.
- Does not clamp output to
[0,1]— additive and screen results can exceed 1.0 on HDR-capable targets (caller’s choice of target format governs clipping). - Does not write depth, stencil, or MRT outputs — single
outColoronly. - Does not declare or sample more than two textures; no per-layer color tints, masks, or LUTs live here.
- Does not branch on
discard— every fragment writes a value (including the fallback passthrough). - Does not define vertex attributes or bind any uniforms itself — the host program is responsible.
Signals
- All inputs are assumed premultiplied alpha (per the file-level JSDoc): layer shaders in this workbench write
color.rgb * alphainto theiroutColorbefore reaching this pass. - The
u_modeinteger encoding is load-bearing and must matchLayerBlendModeincomponent-schema.ts. Drift between the two is a silent bug — the GPU will pick the wrong branch. max(dst.a, src.a)in multiply/screen preserves the more-opaque alpha rather than multiplying alphas — chosen so multiply/screen stay visible when composited onto further layers downstream.- Erase uses
dst * (1 - src.a)(premultiplied), so an opaquesrcfully clearsdstregardless ofsrc.rgb.
Entry points
import composeFrag from './compose-pass.frag'— default export is the GLSL source string, ready to hand togl.shaderSource(or a shader-compile helper in the workbench).- The leading
/* glsl */comment is a tooling hint for editor GLSL syntax highlighting; it is not part of the shader source as far as the GL compiler is concerned (the backtick template literal begins on the same line with#version 300 es).
Pattern notes
- String-export shader. The shader lives as a default-exported template literal rather than a
.fragfile loaded via a Vite plugin — keeps the workbench dependency-free and lets TS see it asstring. - Branch-on-uniform rather than per-mode shader permutations. Acceptable here because the workbench composite is not hot — a handful of layers per frame, not per-pixel particles.
- Premultiplied-alpha contract is shader-wide, not per-branch. Every layer producer in the workbench owes the contract; this pass relies on it for the “over” math to be correct.
- Encoding-with-sibling-file is a coupling.
u_modeintegers are duplicated as the source of truth for the JS-sideLayerBlendModeenum — see EXTRACT-CANDIDATE.
EXTRACT-CANDIDATE
LayerBlendModeinteger encoding (0..4for additive/alpha/multiply/screen/erase) is currently shared by manual convention between this shader andcomponent-schema.ts. Candidate for a single named-constants table (e.g.BLEND_MODE_ADDITIVE = 0, …) imported by the TS schema and stringified into the shader at build time, so the two cannot drift. Worth doing only once a third consumer appears (e.g. a UI dropdown or a serializer that needs the string names) — until then the duplication is two sites and tolerable.