Summary
Replace Light2d's current "build a radial gradient on an offscreen canvas, upload as a GL texture, drawImage every frame" pipeline with a single textured quad whose fragment shader computes the radial falloff procedurally.
Current state
Light2d (packages/melonjs/src/renderable/light2d.js) builds a CanvasRenderTarget at construction time, uses canvas's createRadialGradient to paint a soft circle, then each frame calls renderer.drawImage(this.texture.canvas, ...) with the "lighter" blend mode.
This works but has drawbacks:
- Per-light texture memory —
(radiusX × 2) × (radiusY × 2) × 4 bytes per light, sitting in GPU memory whether the light is on screen or not
- Stale texture on radius change — the texture is baked once at construction; calling
light.resize(...) (or changing radiusX/Y after construction) leaves the old gradient texture in place
- Pixelated under zoom — bitmap, so heavy camera zoom turns the gradient blocky
- Doesn't batch with sprites under heavy texture pressure — each light has a unique texture, so beyond the 16-unit multi-texture batcher limit they cause flushes
Proposed solution
A small custom ShaderEffect-style shader that renders the light as a single textured quad with a procedural radial falloff. No texture upload, no offscreen canvas.
uniform vec3 uLightColor;
uniform float uIntensity;
vec4 apply(vec4 color, vec2 uv) {
// uv in [0,1] across the light's bounding rect; recenter to [-1,1]
vec2 c = uv * 2.0 - 1.0;
float d = length(c);
float falloff = clamp(1.0 - d, 0.0, 1.0);
// smooth the edge so it doesn't look like a hard circle
falloff = falloff * falloff;
return vec4(uLightColor, falloff * uIntensity);
}
Light2d.draw() becomes:
draw(renderer) {
// ensure the shader is set; emit a single quad over the light's bounding box
renderer.drawWithShader(this._lightShader, x, y, w, h);
}
Benefits
- Zero per-light texture memory
- Reactive to radius changes — no rebake
- Crisp at any camera zoom — procedural
- Batches — all light quads share the same shader, single flush
- Natural Canvas fallback — keep the existing canvas-texture path for Canvas mode (it already works there)
Considerations
- Custom blend mode — current default is
"lighter" (additive). The shader returns vec4(color, alpha) with the alpha encoding falloff; combined with additive blending, the colour accumulates correctly across overlapping lights.
- Elliptical lights (different
radiusX vs radiusY) — the quad's aspect ratio handles this naturally; the UV-space distance computation stays in [-1,1] per axis so the falloff stays elliptical.
- Color stops beyond two — current Light2d only supports center→edge with a single intensity. If we want richer falloff curves later, the shader can take additional uniforms or a small 1D ramp texture. Out of scope for the first cut.
Pairs with
Files (estimated)
packages/melonjs/src/renderable/light2d.js — replace createGradient + texture with shader emission on WebGL; keep canvas fallback
packages/melonjs/src/video/webgl/shaders/light2d.frag (new) — the falloff shader
tests/light2d.spec.js (new) — unit tests for the rendering path selection
Risks
- Visual parity with the existing gradient bake under various intensity / radius / blend mode combinations needs a side-by-side comparison
- The existing
createGradient produces a slightly soft edge from canvas's gradient interpolation; the shader's falloff curve has to match perceptually
Summary
Replace
Light2d's current "build a radial gradient on an offscreen canvas, upload as a GL texture, drawImage every frame" pipeline with a single textured quad whose fragment shader computes the radial falloff procedurally.Current state
Light2d(packages/melonjs/src/renderable/light2d.js) builds aCanvasRenderTargetat construction time, uses canvas'screateRadialGradientto paint a soft circle, then each frame callsrenderer.drawImage(this.texture.canvas, ...)with the "lighter" blend mode.This works but has drawbacks:
(radiusX × 2) × (radiusY × 2) × 4bytes per light, sitting in GPU memory whether the light is on screen or notlight.resize(...)(or changingradiusX/Yafter construction) leaves the old gradient texture in placeProposed solution
A small custom
ShaderEffect-style shader that renders the light as a single textured quad with a procedural radial falloff. No texture upload, no offscreen canvas.Light2d.draw()becomes:Benefits
Considerations
"lighter"(additive). The shader returnsvec4(color, alpha)with the alpha encoding falloff; combined with additive blending, the colour accumulates correctly across overlapping lights.radiusXvsradiusY) — the quad's aspect ratio handles this naturally; the UV-space distance computation stays in[-1,1]per axis so the falloff stays elliptical.Pairs with
Light2ddata layout.Files (estimated)
packages/melonjs/src/renderable/light2d.js— replacecreateGradient+texturewith shader emission on WebGL; keep canvas fallbackpackages/melonjs/src/video/webgl/shaders/light2d.frag(new) — the falloff shadertests/light2d.spec.js(new) — unit tests for the rendering path selectionRisks
createGradientproduces a slightly soft edge from canvas's gradient interpolation; the shader's falloff curve has to match perceptually