v19.3.0
What's New in melonJS 19.3.0
New Features
- Normal-map sprite lighting (closes #1416) —
Sprite.normalMapaccepts a paired normal-map image (or auto-detected fromTextureAtlas({ normalMap })). Sprites with a normal map render through a dedicatedLitQuadBatcherwhose fragment shader runs a Lambertian light loop over the activeLight2dinstances — up to 8 concurrent lights, configurableStage.ambientLightingColor, quadratic attenuation. The lit batcher only kicks in when bothnormalMapand active lights are present, so unlit scenes pay zero overhead. Light2dis now a first-class worldRenderable— add viaapp.world.addChild(light)(or any container, so the light follows parent transforms). Auto-registers with the active stage's lighting set viaonActivateEvent/onDeactivateEvent. The legacyStage.lights.set()API still works (entries are auto-adopted into the world tree on stage reset).- Lights now render inside the camera's post-effect FBO bracket (closes #1398) — vignette, scanlines, ColorMatrix, and any other camera shader effect now wrap the lighting output. The
Stage.draw()lighting block has been removed; rendering happens via the world tree walk and a publicStage.drawLighting(renderer, camera)pass invoked by each camera (subclassable for custom lighting). - Procedural Light2d (closes #1430) — new
Renderer.drawLight(light)API replaces the per-light offscreen-canvas pipeline. WebGL renders lights as quads through a sharedRadialGradientEffectshader; per-light color and intensity flow through the vertextintattribute, so N lights = 1 program switch + 1 flush. Canvas caches a smallGradientconfig per light (rebuilt only on radii / color / intensity change) and shares oneCanvasRenderTargetacross every gradient.Light2dis now pure data — no canvas, no shader knowledge. Light2d.illuminationOnly(defaultfalse) — whentrue, the light's own gradient isn't drawn but it still feeds the cutout pass and the lit-sprite shader. Useful for SpriteIlluminator-style demos where the light is a logical source, not a visible glow.Light2d.lightHeight(defaultmax(radiusX, radiusY) * 0.075) — Z component of the light direction in the lit shader'sdot(normal, lightDir). Low values graze across the surface (dramatic detail); high values produce more uniform brightness.Light2d.setRadii(rx, ry)— updates both radii and the underlying bbox sogetBounds()andgetVisibleArea()track the new size. Fixes a latent bug where mutatingradiusX/Yafter construction left the rendered light stale while the cutout pass moved.RadialGradientEffect— generic procedural radial gradient shader (video/webgl/effects/radialGradient.js). Solid color at center fading linearly to transparent at the host quad's edge. Accepts{ color, intensity }plussetColor/setIntensitysetters.RenderState.peekScissor()— inspect the scissor box that the nextrestore()would install, without mutating state. Returns a live reference into the internal stack (read-only). Used byWebGLRenderer.restore()to flush only when the scissor actually changes.- Three new examples —
Normal Map(three procedurally-generated 3D orbs reacting to a moving cursor light),SpriteIlluminator(port of CodeAndWeb's cocos2d-x dynamic-lighting demo), andClipping(nested + animatedContainer.clipping).
Changed
- Breaking:
Light2dis now centered on itspos(anchorPoint = (0.5, 0.5)), matchingSpriteandEllipse(x, y, w, h)conventions. Constructorx/yandlight.pos.x/ydenote the light's center, not the bounding-box top-left. Transforms applied vialight.scale(...)orlight.rotate(...)now pivot around the visual center. Existingnew Light2d(x, y, r)callers passing top-left coords need to add radius:new Light2d(x + r, y + r, r). Code usinglight.centerOn(x, y)is unaffected. Bounds.addFramenow short-circuits the 4-corner walk when the matrix is identity (or omitted) — beneficial forWebGLRenderer.clipRect/enableScissorand any other call site that doesn't pre-filter.
Bug Fixes
- Multi-light support — lifted the historical "Canvas mode only supports one light per stage" limitation. Multiple
Light2dinstances now render correctly under both Canvas and WebGL. Root cause was in the underlyingsetMask(shape, true)implementation on both renderers: chained calls did not accumulate cutouts. Canvas now adds the outer rect once per mask sequence (made tractable by the evenodd groundwork from #1369); WebGL switched to anINCR-based stencil protocol so each shape adds independently. Containerclipping under nested transforms (closes #1349) — a clipping container nested inside a translated, scaled, or rotated parent was mis-positioned.Container.drawnow applies its own translate before callingclipRectand passes container-local(0, 0, width, height); WebGLclipRecttransforms the four input corners throughcurrentTransformand uses the AABB as the screen-space scissor box (so scale and rotation are honored). Canvas'scontext.rectwas already matrix-aware.- WebGL flush ordering on scissor change — pending
PrimitiveBatchervertices now drain when asave()/restore()pair changes the scissor box. PreviouslyWebGLRenderer.restore()reverted the GL scissor without flushing, so vertices queued inside a deeper clip could survive pastrestore()and flush later under a more permissive scissor. CanvasRenderer.setMask(shape)X/Y swap — with aRect,Bounds, orRoundRectmask, args were being passed in the wrong order tocontext.rect/context.roundRect. Masks at off-diagonal positions clipped at the wrong location. Latent because nothing in core or examples used those shape types as masks.Stage.drawLightingcutout alignment — ambient-overlay cutouts now align with each light's rendered gradient when the camera is scrolled or the light is parented to a translated container.getVisibleArea()returns world-space coords, butdrawLightingruns after the world container'stranslate(-cameraPos)has been popped — so cutouts were landing at world coords inside a camera-local FBO. Fix re-applies the camera's world-to-screen translate insidedrawLighting.- WebGL vertex attribute leak between batchers — each batcher owns its own attribute layout (e.g.
LitQuadBatcher5 attrs at stride 28 vsPrimitiveBatcher3 at stride 20). On batcher switch the previous batcher's enabled attribute locations stayed live with their old stride/offset and could throwINVALID_OPERATIONon the next draw.Batcher.unbind()now disables them on every switch. - WebGL
gl.useProgramleak aftersetLightUniforms—Camera2d.draw()calledsetLightUniforms(...)every frame even when the scene had zero lights, leaving the GL program pointed at the lit shader. The next sprite draw (4-attribute vertex data) was being fed to the lit shader (5 attributes), rendering as garbage. Fixed by restoring the active batcher's program after the upload. - WebGL stale custom shader past
setBatcher— the previous fast path returned early when the active batcher matched and no shader was provided, so a custom shader bound by a prior call could keep rendering subsequent batches through the wrong program.setBatchernow always reconciles the active shader. QuadBatcher.blitTexturetexture-unit cache desync —blitTexturedid not synccurrentTextureUnit/boundTextures[0]with the GL state it mutated. After a blit ran with a non-zero unit, subsequentbindTexture2Dcalls could short-circuit on the stale cached unit and bind the new texture on the wrong unit, corrupting the next sprite batch.- Stale Light2d gradient on radius / color / intensity change — pre-#1430 the gradient was baked once at construction. The new
drawLightpath auto-invalidates: the Canvas cache rebuilds when any ofradiusX/radiusY/color/intensitydiffer; WebGL readslight.color/light.intensitylive each call.
Install
npm install melonjs@19.3.0