Skip to content

v19.3.0

Choose a tag to compare

@obiot obiot released this 08 May 08:26
· 55 commits to master since this release
58a0050

What's New in melonJS 19.3.0

New Features

  • Normal-map sprite lighting (closes #1416) — Sprite.normalMap accepts a paired normal-map image (or auto-detected from TextureAtlas({ normalMap })). Sprites with a normal map render through a dedicated LitQuadBatcher whose fragment shader runs a Lambertian light loop over the active Light2d instances — up to 8 concurrent lights, configurable Stage.ambientLightingColor, quadratic attenuation. The lit batcher only kicks in when both normalMap and active lights are present, so unlit scenes pay zero overhead.
  • Light2d is now a first-class world Renderable — add via app.world.addChild(light) (or any container, so the light follows parent transforms). Auto-registers with the active stage's lighting set via onActivateEvent / onDeactivateEvent. The legacy Stage.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 public Stage.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 shared RadialGradientEffect shader; per-light color and intensity flow through the vertex tint attribute, so N lights = 1 program switch + 1 flush. Canvas caches a small Gradient config per light (rebuilt only on radii / color / intensity change) and shares one CanvasRenderTarget across every gradient. Light2d is now pure data — no canvas, no shader knowledge.
  • Light2d.illuminationOnly (default false) — when true, 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 (default max(radiusX, radiusY) * 0.075) — Z component of the light direction in the lit shader's dot(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 so getBounds() and getVisibleArea() track the new size. Fixes a latent bug where mutating radiusX/Y after 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 } plus setColor / setIntensity setters.
  • RenderState.peekScissor() — inspect the scissor box that the next restore() would install, without mutating state. Returns a live reference into the internal stack (read-only). Used by WebGLRenderer.restore() to flush only when the scissor actually changes.
  • Three new examplesNormal Map (three procedurally-generated 3D orbs reacting to a moving cursor light), SpriteIlluminator (port of CodeAndWeb's cocos2d-x dynamic-lighting demo), and Clipping (nested + animated Container.clipping).

Changed

  • Breaking: Light2d is now centered on its pos (anchorPoint = (0.5, 0.5)), matching Sprite and Ellipse(x, y, w, h) conventions. Constructor x/y and light.pos.x/y denote the light's center, not the bounding-box top-left. Transforms applied via light.scale(...) or light.rotate(...) now pivot around the visual center. Existing new Light2d(x, y, r) callers passing top-left coords need to add radius: new Light2d(x + r, y + r, r). Code using light.centerOn(x, y) is unaffected.
  • Bounds.addFrame now short-circuits the 4-corner walk when the matrix is identity (or omitted) — beneficial for WebGLRenderer.clipRect / enableScissor and 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 Light2d instances now render correctly under both Canvas and WebGL. Root cause was in the underlying setMask(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 an INCR-based stencil protocol so each shape adds independently.
  • Container clipping under nested transforms (closes #1349) — a clipping container nested inside a translated, scaled, or rotated parent was mis-positioned. Container.draw now applies its own translate before calling clipRect and passes container-local (0, 0, width, height); WebGL clipRect transforms the four input corners through currentTransform and uses the AABB as the screen-space scissor box (so scale and rotation are honored). Canvas's context.rect was already matrix-aware.
  • WebGL flush ordering on scissor change — pending PrimitiveBatcher vertices now drain when a save()/restore() pair changes the scissor box. Previously WebGLRenderer.restore() reverted the GL scissor without flushing, so vertices queued inside a deeper clip could survive past restore() and flush later under a more permissive scissor.
  • CanvasRenderer.setMask(shape) X/Y swap — with a Rect, Bounds, or RoundRect mask, args were being passed in the wrong order to context.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.drawLighting cutout 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, but drawLighting runs after the world container's translate(-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 inside drawLighting.
  • WebGL vertex attribute leak between batchers — each batcher owns its own attribute layout (e.g. LitQuadBatcher 5 attrs at stride 28 vs PrimitiveBatcher 3 at stride 20). On batcher switch the previous batcher's enabled attribute locations stayed live with their old stride/offset and could throw INVALID_OPERATION on the next draw. Batcher.unbind() now disables them on every switch.
  • WebGL gl.useProgram leak after setLightUniformsCamera2d.draw() called setLightUniforms(...) 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. setBatcher now always reconciles the active shader.
  • QuadBatcher.blitTexture texture-unit cache desyncblitTexture did not sync currentTextureUnit / boundTextures[0] with the GL state it mutated. After a blit ran with a non-zero unit, subsequent bindTexture2D calls 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 drawLight path auto-invalidates: the Canvas cache rebuilds when any of radiusX/radiusY/color/intensity differ; WebGL reads light.color / light.intensity live each call.

Install

npm install melonjs@19.3.0