PURPOSE
WebGL2 procedural fire renderer. Draws animated fire effects using FBM (fractal Brownian motion) noise on an offscreen canvas, sized at half viewport resolution. The offscreen canvas is composited onto the main Canvas 2D surface via drawImage with additive blend. Mirrors the architecture of nebula-engine.ts: own GL context, own shaders, batched instanced draw.
OWNS
- Module-private offscreen
HTMLCanvasElement(_canvas) sized atFIRE_SCALE(0.5) of the viewport - A
WebGL2RenderingContext(_gl) created withalpha: true,premultipliedAlpha: true,antialias: false - The compiled
WebGLProgram(_program) using the inlinedFIRE_VSvertex shader andFIRE_FSfragment shader - A
WebGLVertexArrayObject(_vao) binding a static 6-corner vertex buffer plus a dynamic per-instance buffer - A dynamic instance buffer (
_instanceBuffer) and CPU-sideFloat32Array(_instanceData) sized forMAX_INSTANCES(64) instances ofFLOATS_PER_INSTANCE(5) floats each:sx, sy, sw, sh, alpha - Cached uniform locations:
_uViewport,_uTime,_uColor - Tint state:
_colorR,_colorG,_colorB(default orange1.0, 0.4, 0.0) - Cached camera state for world-to-screen conversion:
_camX,_camY,_camZoom,_halfW,_halfH - The per-frame instance queue (
_queue: FireInstance[]) - Viewport dimensions (
_viewW,_viewH)
READS FROM
- Caller-supplied world or screen coordinates passed to
add()/addScreen() - Caller-supplied camera state via
setCamera(x, y, zoom, viewW, viewH)(must be called once per frame beforeadd()) - Caller-supplied
timevalue passed toflush()(used as theu_timeuniform driving shader animation) - Tint colour set via
setColor(r, g, b) - The DOM (
document.createElement('canvas')) and the WebGL2 context returned by the canvas
PUSHES TO
- The offscreen WebGL canvas (
_canvas), exposed viagetCanvas()so the main 2D renderer can composite it withctx.drawImage - The GPU instance buffer via
gl.bufferSubData(eachflush()call uploadscount * FLOATS_PER_INSTANCEfloats) console.erroron shader compile failure or program link failure (prefixed[FireEngine])
DOES NOT
- Does not composite itself onto the main canvas — callers must invoke
getCanvas()anddrawImageit - Does not own any world-space simulation state (no fire lifetimes, no fire entities) — every fire instance must be re-queued each frame
- Does not read the camera or time globals — both are passed in explicitly
- Does not allocate beyond
MAX_INSTANCES— extraadd()/addScreen()calls past 64 are silently dropped via early return - Does not clear the queue on resize or on missing GL context —
flush()clears it after upload, or after a failed init - Does not support multiple tint colours per frame —
_colorR/G/Bis a single uniform applied to all instances in a flush - Does not depend on any other engine module — pure self-contained WebGL primitive
Signals
None. The module is a procedural API; it does not emit events.
Entry points
Exported as the FireEngine singleton object with these methods:
add(worldX, worldY, width, height, intensity = 0.3)— queue a world-space fire; converted to screen space at queue time using cached camera; fire is anchored so it extends upward from(worldX, worldY)addScreen(screenX, screenY, width, height, intensity = 0.3)— queue a screen-space fire (for HUD); height is internally multiplied by 1.6 and y-offset by-height * 0.3so the fire extends above the target elementsetColor(r, g, b)— set the tint colour applied to all subsequent flushessetCamera(x, y, zoom, viewW, viewH)— update cached camera; must be called once per frame before any world-spaceadd()flush(time)— pack the queue into the instance buffer, upload, render one instanced draw call, then clear the queue; lazy-inits the GL context on first call using cached or fallback viewport (800x600)getCanvas()— return the offscreenHTMLCanvasElement(ornullif not initialized) for compositingresize(w, h)— update viewport dimensions and resize the offscreen canvas tow * FIRE_SCALEbyh * FIRE_SCALEisReady()— returnstrueonce the GL context is initialized
Pattern notes
- Module-level singleton. All state is held in module-private
letbindings; the exportedFireEngineobject is a thin facade. - Lazy init.
_init()runs on the firstflush()call rather than at module load, so the module is safe to import in non-browser contexts up to that point. Init returnsfalseand the queue is cleared if WebGL2 is unavailable or the viewport is degenerate. - Half-resolution offscreen. All queued coordinates are pre-multiplied by
FIRE_SCALE(0.5) at queue time; the offscreen canvas is sized at half viewport. The 2D compositor is expected to stretch it back to full size viadrawImage. - One instanced draw per flush. Uses
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count)with a 6-vertex unit quad and per-instance attributesa_rect(vec4) anda_alpha(float), each withvertexAttribDivisor = 1. - Additive blending.
gl.blendFunc(gl.ONE, gl.ONE)so multiple overlapping fires brighten rather than alpha-blend. - Premultiplied alpha context. The GL context is created with
premultipliedAlpha: true; the fragment shader outputsvec4(col * fire, fire * v_alpha)to match. - Hard cap, silent drop.
MAX_INSTANCESis enforced via early return in bothadd()andaddScreen(); the 65th-and-beyond fire of a frame is simply discarded. - Fire shape baked into fragment shader. The vertical falloff (
fade = (1 - v_uv.y)^2), horizontal taper, FBM noise (3 octaves), downward UV scroll (u_time * 0.8), and white-core mix into the tint are all inFIRE_FS. No CPU-side animation state. - World-to-screen at queue time, not flush time.
add()immediately converts using the cached camera, so out-of-ordersetCamera()calls within a frame will yield stale coordinates for already-queued fires. - Fire emanates downward in UV space, upward in world space. The shader scrolls UV downward so the noise pattern moves with the source;
add()offsetssyby-heightso the bottom of the rect anchors atworldY. - No GL resource cleanup. Once initialized, the GL context, program, VAO, and buffers persist for the lifetime of the page;
resize()only adjusts canvas dimensions.