PURPOSE
Hosts workbench-emitted live-shader components in the shipping game. Compiles a fragment GLSL string and binds its declared uniforms per frame, rendering to an offscreen WebGL2 canvas that the main Canvas 2D pipeline composites via drawImage — the same pattern used by fire-engine.ts and nebula-engine.ts. Reserved for 1-3 hero effects (beams, chain lightning, cone fire) at roughly 0.3-0.5 ms per instance on iPhone 12-class mobile at half-res. Not for every projectile.
OWNS
LiveShaderRuntimeclass — one instance per registered live-shader component. Owns its ownHTMLCanvasElement,WebGL2RenderingContext, compiledWebGLProgram,WebGLVertexArrayObject,WebGLBuffer, and a cache ofWebGLUniformLocationlookups keyed by uniform name.- The shared vertex shader (
SHARED_VS) — fullscreen triangle strip witha_posattribute andv_uvpassthrough varying, GLSL ES 3.00. - The shared quad vertex buffer (
QUAD_VERTICES) — four-vertex triangle strip in clip space. - The module-level runtime registry (
_runtimesMap keyed by component id) and the idempotency flag_registryLoaded. - The default scale factor constant
DEFAULT_LIVE_SHADER_SCALE(0.5 — half-res offscreen target). - The local shader compile pipeline (
compileProgram,compileShader) — throws on compile or link failure with id-tagged error messages.
READS FROM
/src/starship-survivors/data/vfx/live-shaders/manifest.json— fetched once at boot byinitLiveShaderRuntimes(). EachLiveShaderManifestEntryprovidescomponentId,fragGlsl,blendMode, plus metadata (name,category,uniformSchema,previewSampleInputs,vertGlsl,estCostMs,anchors,shippedAt).- Caller-supplied per-frame uniform values via
setUniform/setUniforms. - Caller-supplied viewport dimensions via
resize(screenW, screenH). - Caller-supplied time scalar via
render(time).
PUSHES TO
- Each runtime’s own offscreen
HTMLCanvasElement— sized toscreenW * scalebyscreenH * scale, cleared to transparent RGBA(0,0,0,0) everyrender()call. The canvas is exposed viagetCanvas()so the caller canctx.drawImage(canvas)it onto the main Canvas 2D surface. - WebGL2 uniforms on the bound program —
u_time(float) andu_resolution(vec2) are always pushed byrender(); arbitrary caller uniforms viasetUniformuseuniform1ffor numbers anduniform2fv/uniform3fv/uniform4fvfor arrays based on length.
DOES NOT
- Does not composite onto the main canvas itself — the caller is responsible for the
drawImagestep. - Does not own the main game canvas or its 2D context.
- Does not parse or validate GLSL beyond what the WebGL2 driver reports — compile and link errors are surfaced as thrown
Errorobjects with id-tagged messages. - Does not pre-create uniform locations from the manifest’s
uniformSchema— locations are looked up lazily on firstsetUniformcall and cached. - Does not enforce the manifest’s declared uniform types —
setUniforminfers vector size from JS array length only. - Does not provide a Canvas 2D fallback path itself. If
init()returnsfalse(WebGL2 unavailable or shader compile failure), the caller must skip the live shader entirely. - Does not free or recreate the program on resize — only the offscreen canvas dimensions and the GL viewport are updated.
- Does not load or compile custom vertex shaders — the manifest’s
vertGlslfield is ignored; every runtime usesSHARED_VS.
Signals
init()returnsboolean—falsewhencanvas.getContext('webgl2', ...)yields null or whencompileProgramthrows. On failure, aconsole.warnis emitted with the runtime id and the underlying error.isReady()returnstrueonly afterinit()has succeeded and neither the GL context nor the program has been destroyed.initLiveShaderRuntimes()returns the count of successfully registered runtimes. Returns0on manifest fetch failure or non-okHTTP response; returns the cached size on repeat calls (idempotent via_registryLoaded).getLiveShaderRuntime(id)returns the runtime ornullwhen the id is not registered.getAllLiveShaderRuntimes()returns a fresh array snapshot of all registered runtimes.destroy()deletes the program, VBO, and VAO via the GL context, then nulls the local references. Subsequentrender()/setUniformcalls become no-ops because they early-return whenglorprogramis null.
Entry points
LiveShaderRuntimeclass constructor —(id, fragSource, blendMode, scale = DEFAULT_LIVE_SHADER_SCALE). Creates the canvas element but does not touch WebGL untilinit()is called.LiveShaderRuntime.init()— boots the GL context (alpha: true,premultipliedAlpha: true, no antialias/depth/stencil), compiles and links the program, sets up the VAO/VBO with the shared quad, configures the blend func (ONE, ONEfor additive;ONE, ONE_MINUS_SRC_ALPHAfor alpha).LiveShaderRuntime.resize(screenW, screenH)— must be called beforerenderwhenever the viewport changes.LiveShaderRuntime.setUniform(name, value)/setUniforms(values)— bulk setter wraps the single setter.LiveShaderRuntime.render(time)— clears, setsu_time+u_resolution, draws a 4-vertex triangle strip.LiveShaderRuntime.getCanvas()— handle forctx.drawImagecompositing.LiveShaderRuntime.destroy()— releases GL resources.initLiveShaderRuntimes()— module-level async boot entry. Idempotent.getLiveShaderRuntime(id)/getAllLiveShaderRuntimes()— module-level registry accessors.- Exports:
LiveShaderUniformDecl(interface),DEFAULT_LIVE_SHADER_SCALE(const),LiveShaderRuntime(class),initLiveShaderRuntimes,getLiveShaderRuntime,getAllLiveShaderRuntimes.
Pattern notes
- Mirrors
fire-engine.ts/nebula-engine.ts: offscreen GL canvas + per-framectx.drawImagecomposite onto the main Canvas 2D surface. The main pipeline stays Canvas 2D; live shaders are scoped composite layers. - Half-res offscreen target (
scale = 0.5) is the documented sweet spot for soft effects — beams, fire, chain lightning. The scale is per-runtime and constructor-injected, so individual hero effects can override it. - Fragment-only authoring: the manifest carries a
vertGlslfield but the runtime always uses the shared fullscreen-quad VS. Authors only ship the fragment shader. - Shared uniform contract with the workbench preview surface:
u_time(float, seconds) andu_resolution(vec2, canvas pixels) are guaranteed every frame. All other uniforms are author-defined and pushed by the caller. - Uniform location cache uses
undefinedvs.nullto distinguish “not yet looked up” from “looked up and program does not declare it” — oncenullis cached, subsequent sets early-return cheaply. - Vector type is inferred from JS array length, not the manifest’s
uniformSchema. Length 2/3/4 map directly; anything else falls through touniform3fvas a default, which silently mismatches GLSL types — authors are expected to send the right shape. - Blend mode is fixed at construction. Additive uses
(ONE, ONE)for HDR-style glow accumulation; alpha uses(ONE, ONE_MINUS_SRC_ALPHA)assuming premultiplied alpha (matches thepremultipliedAlpha: truecontext attribute). - Registry is module-scoped singleton state with a fetch-once boot pattern. The
manifestUrlis a hard-coded absolute path (/src/starship-survivors/data/vfx/live-shaders/manifest.json) — assumes a dev/Vite-style URL layout served from the repo root. - Manifest fetch failures (network error, non-OK status, malformed JSON) are swallowed and return
0— there is no telemetry hook for “live shaders failed to load.” The caller path is expected to fall back automatically because no runtimes get registered. - Per-runtime
init()failures log viaconsole.warn(not telemetry). Failed runtimes are simply skipped and never added to the registry —getLiveShaderRuntime(id)will then returnnullfor them, and callers must treat that as the fallback signal. destroy()is provided but the boot path never calls it. Runtimes are assumed to live for the full session.- Spec reference:
2026-04-20 §10 B(lifecycle),live-shadersdata dir as the authored source. Companion authoring directory is/src/starship-survivors/data/vfx/live-shaders/(includesdynamic_cone_fire.live.ts).