PURPOSE
WebGL2 instanced sprite batch renderer. Draws up to 2048 textured quads per draw call from a shared texture atlas, rendered to a separate offscreen WebGL2 canvas that the game composites onto its main Canvas 2D via ctx.drawImage. Supports normal alpha and additive blend modes per flush. Falls back gracefully (returns false from init) if WebGL2 is unavailable.
OWNS
SpriteBatchclass — encapsulates one offscreen WebGL2 canvas, shader program, VAO, instance buffer, atlas texture, and per-frame camera state.AtlasRegioninterface — UV rectangle (u0, v0, u1, v1) into the atlas texture.- Compiled vertex/fragment shader sources (
VERT_SRC,FRAG_SRC) — instanced quad with rotation, camera transform, UV interpolation, and atlas-tinted output. - Quad corner buffer (6 vertices, static), instance buffer (
MAX_SPRITES * FLOATS_PER_INSTANCEfloats, dynamic). instanceData: Float32Array— CPU-side scratch for the current frame’s sprite instances.- Two module-level singletons:
_batch(alpha-blend) and_glows(additive). Module factory functionsinitSpriteBatch,getSpriteBatch,getGlowBatch.
READS FROM
- Caller-supplied atlas via
uploadAtlas(atlasCanvas)/reuploadAtlas(atlasCanvas)— uploaded as aTEXTURE_2DwithLINEARfiltering andCLAMP_TO_EDGEwrap. - Per-frame caller state in
beginFrame(camX, camY, zoom)— camera position and zoom in CSS-pixel space (must matchCamera.toS). - Per-sprite caller state in
add(x, y, w, h, rotation, region, r, g, b, a)— world position (sprite center), size, radians,AtlasRegion, and unpremultiplied RGBA tint. - Viewport dimensions via
resize(w, h, dpr)— CSS width/height plus device pixel ratio.
PUSHES TO
- Its own offscreen
HTMLCanvasElement(thecanvaspublic field). The compositor reads it viactx.drawImage(spriteBatch.canvas, 0, 0). - GPU instance buffer via
gl.bufferSubDataonce perflush. - GPU framebuffer via
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, spriteCount).
DOES NOT
- Does not composite to the main canvas. Caller must
drawImagethe offscreen canvas itself. - Does not own or load the atlas image — caller supplies the source canvas.
- Does not depth-sort, cull, or clip. Sprites draw in insertion order; overflow past
MAX_SPRITESis silently dropped. - Does not auto-flush. Caller must call
flush(mode)to issue the draw, and may call it multiple times per frame (e.g., once per blend mode). - Does not retain sprite state across frames.
beginFramezeroesspriteCount;flushalso zeroes it after drawing. - Does not handle WebGL context loss / restore.
- Does not premultiply texels — premultiplication of the per-instance color tint happens CPU-side in
add(r*a, g*a, b*a, a).
Signals
ready(getter) — true onceuploadAtlashas been called and a texture exists.initSpriteBatch()returnsfalseif WebGL2 context creation throws.getSpriteBatch()/getGlowBatch()returnnulluntilinitSpriteBatchsucceeds.- Shader compile failure throws
Error('Shader compile failed: ' + info). Link failure throwsError('Shader link failed: ' + info). Missing WebGL2 throwsError('WebGL2 not available')from the constructor (caught byinitSpriteBatch).
Entry points
new SpriteBatch()— creates the offscreen canvas, requests a WebGL2 context (alpha: true,premultipliedAlpha: true,antialias: false,depth: false,stencil: false,preserveDrawingBuffer: true), compiles and links shaders, builds the VAO with per-vertex corners and per-instance attributes (posSize, uvRegion, color, rotation; all withvertexAttribDivisor(_, 1)), and sets default blendONE, ONE_MINUS_SRC_ALPHA.uploadAtlas(atlasCanvas)— first-time atlas upload. Deletes any prior texture, creates a new one, and uploads RGBA pixels.reuploadAtlas(atlasCanvas)— re-uploads pixels into the existing texture (for atlas patching with real sprites after generation).resize(w, h, dpr)— sets backing-store size toround(w*dpr) × round(h*dpr), CSS size tow × h, storescssW/cssHfor the shader uniform, and callsgl.viewportto backing-store size.beginFrame(camX, camY, zoom)— stores camera state, resetsspriteCount = 0, clears the WebGL canvas to transparentrgba(0,0,0,0).add(x, y, w, h, rotation, region, r, g, b, a)— writes 13 floats intoinstanceDataat offsetspriteCount * 13:x, y, w, h, u0, v0, u1, v1, r*a, g*a, b*a, a, rotation. IncrementsspriteCount. Drops the sprite if at cap.flush(mode = 'normal')— early-returns ifspriteCount === 0. Binds program + VAO, uploads only the used prefix ofinstanceDataviabufferSubData, setsu_viewportto CSS pixels,u_cameraandu_zoom, binds the atlas to texture unit 0, sets blend func (ONE, ONEfor additive;ONE, ONE_MINUS_SRC_ALPHAfor normal), and issuesdrawArraysInstanced(TRIANGLES, 0, 6, spriteCount). ZeroesspriteCountafterward.flushPendingGL()— diagnostic helper that callsgl.flush()so a subsequentperformance.now()reading captures GPU work submitted earlier (used by thegpuSyncBeforeShipInitMsprobe in sticker setup).initSpriteBatch()/getSpriteBatch()/getGlowBatch()— module-level factory and accessors for the two singletons.
Pattern notes
- Two singleton batches:
_batchfor alpha-blended content (enemies, ship, particles, debris) and_glowsfor additive content (bullets, ship aura, boss aura, explosion glows). Each owns its own WebGL2 canvas and atlas texture; the glow batch is composited after the alpha batch so additive light layers on top of opaque sprites. - Instance layout: 13 floats per sprite —
posSize(4) +uvRegion(4) +color(4) +rotation(1). Stride52bytes; attribute offsets0, 16, 32, 48with all instance attributes usingvertexAttribDivisor(_, 1). - Quad geometry is two triangles drawn as
TRIANGLES(6 vertices) shared across all instances via attribute 0. - Vertex shader transforms each per-instance quad: corner re-centered to
[-0.5, 0.5], rotated bya_rotation, scaled bya_posSize.zw, translated to world bya_posSize.xy, mapped to screen with(world - u_camera) * u_zoom + u_viewport * 0.5, then to clip space. Y is flipped (1.0 - ...) so Y grows downward in world space, matching the Canvas 2D convention used elsewhere. - Fragment shader is
texture(u_atlas, v_uv) * v_color— atlas alpha gates the tint, and color is already premultiplied CPU-side. - Color premultiplication happens in
add(r*a, g*a, b*a, a) to match the context’spremultipliedAlpha: trueand the blend funcONE, ONE_MINUS_SRC_ALPHA. preserveDrawingBuffer: trueis required because the game reads the WebGL canvas viadrawImageafterflush; without it the browser may discard the framebuffer between paint cycles.- The shader’s
u_viewportuniform is set to CSS pixels (cssW, cssH), not backing-store pixels, so the math agrees with the rest of the camera pipeline (Camera.toS). Thegl.viewportitself is set to backing-store pixels inresize. - Overflow past
MAX_SPRITES = 2048is silently dropped inadd. There is no auto-flush-on-overflow. flushcan be invoked more than once per frame (e.g., one alpha flush then one additive flush on the glow batch). Each flush re-uploads only the used prefix of the instance buffer.- Constructor explicitly disables depth and stencil and turns off antialias — this is a 2D pipeline, ordering is the caller’s responsibility.