|
5 | 5 | ### Added |
6 | 6 | - **`ParticleEmitter` can measure its particles from somewhere other than itself**, through the new `referenceSpace` setting: `"local"` (the default, unchanged), `"world"`, or any `Container`. A particle stores a position, and this decides what that position is relative to. Until now it was always the emitter, so a moving emitter dragged its entire cloud along with it — correct for a flame or an aura, and impossible to opt out of for smoke, exhaust, sparks or footstep dust, where the effect should be emitted and then abandoned. With `"world"` the position names a place in the level instead, so only newly emitted particles appear at the emitter's new location and the rest stay put; passing a `Container` measures from that, for a frame of reference that is neither (snow drifting inside a moving carriage). `"world"` resolves to the emitter's parent container rather than the root, so a level that moves carries its own trails with it. Changing it at runtime — by assigning the property or through `reset()` — re-bases the particles already alive, so the cloud does not jump. An emitter using a non-local space is treated as always visible while it has live particles, since a trail would otherwise disappear the instant the emitter that made it scrolled off-screen (the particles themselves are still culled individually) (thanks @Vareniel) |
7 | 7 | - **`Renderable.getWorldTransform(out)`** — the matrix form of the existing `getAbsolutePosition()`, which sums positions up the ancestor chain and so cannot represent the rotation, scale or flip accumulated along the way. Returns the transform mapping a renderable's local space into world space, writing into a caller-supplied `Matrix3d` and storing nothing on the renderable. Note it answers a slightly different question than `getAbsolutePosition()`: it is the frame a renderable's content is drawn *in*, which for a `Container` includes its own position (it offsets its children) and for a leaf does not, since a leaf places itself from `pos` inside its own `draw()` |
8 | | -- **The six remaining CSS blend modes now work on both GPU backends** ([#1318](https://github.com/melonjs/melonJS/issues/1318)): `overlay`, `hard-light`, `color-dodge`, `color-burn`, `soft-light` and `difference`. All thirteen modes the engine names are now supported by all three renderers, so the Canvas fallback is no longer the most capable backend for blending. These six cannot be expressed as `src * sfactor + dst * dfactor` — each needs a per-pixel branch, a division or a `sqrt` on the *destination* — and neither WebGL 2 nor WebGPU can read the destination in a fragment shader, so each draw captures the destination, renders to an offscreen target and composites through a shader carrying both a GLSL and a WGSL body. Nothing changes in how you use them: set `sprite.blendMode = "overlay"` or call `renderer.setBlendMode("overlay")` as before, on any renderable — sprites, text, image layers, particles, Tiled layers — or on a direct shape fill. `setBlendMode` now reports these six as applied rather than falling back, so the capability probe pattern (comparing the return value against the request) reports them supported |
| 8 | +- **The six remaining CSS blend modes now work on both GPU backends** ([#1318](https://github.com/melonjs/melonJS/issues/1318)): `overlay`, `hard-light`, `color-dodge`, `color-burn`, `soft-light` and `difference`. All thirteen modes the engine names are now supported by all three renderers, so the Canvas fallback is no longer the most capable backend for blending. These six cannot be expressed as `src * sfactor + dst * dfactor` — each needs a per-pixel branch, a division or a `sqrt` on the *destination* — and neither WebGL 2 nor WebGPU can read the destination in a fragment shader, so each draw captures the destination, renders to an offscreen target and composites through a shader carrying both a GLSL and a WGSL body. Nothing changes in how you use them: set `sprite.blendMode = "overlay"` or call `renderer.setBlendMode("overlay")` as before, on any renderable — sprites, text, image layers, particles, Tiled layers — or on a direct shape fill. `setBlendMode` now reports these six as applied rather than falling back, so the capability probe pattern (comparing the return value against the request) reports them supported. Two things to know before reaching for them: each blended draw costs **one destination capture and one composite**, which is what per-draw blending against the live framebuffer requires without framebuffer-fetch hardware — right for accents (a glow, a light overlay, a coloured wash), expensive for hundreds of blended objects and unsuitable for something like a whole tilemap layer in `overlay`. And **3D meshes** (`drawMesh`) do not support them, falling back to `"normal"` with a one-time console warning rather than silently, because the offscreen's separate depth buffer would break subsequent depth testing |
9 | 9 |
|
10 | 10 | ### Fixed |
11 | 11 | - **An abandoned WebGPU frame could leave a batcher holding views into destroyed textures.** `abandonFrame()` drops the command buffer unsubmitted and then frees every texture retired during that frame, on the reasoning that the draws referencing them died with the buffer. But the batchers were not reset, so their segment entries kept `GPUTextureView`s into textures that had just been destroyed, and the next frame's bind group could be composed over freed resources. Reached whenever a frame is abandoned after a texture is replaced or unloaded mid-frame, a stage switch freeing the previous scene's assets being the ordinary case. Every registered batcher is now reset before the retired textures are freed, which also drops the dead frame's queued vertices rather than replaying them into the next one |
|
20 | 20 | - **A batcher could bind without adopting its own shader.** `Batcher.bind()` asked the renderer's program cache whether to call `useShader` — but that answers "what does GL have bound", not "has this batcher taken up its shader yet", and `useShader` is also what assigns `currentShader`, which `setProjection` and the uniform paths dereference. The two questions coincided only because the cache could be stale; making it truthful (above) pulled them apart and the renderer threw on construction. Latent until then, and the reason this and the fix above ship together |
21 | 21 | - **`CanvasRenderTarget.invalidate()` re-entered the renderer's batcher dispatch**, which the GPU backends use to bracket a draw for an advanced blend mode. Refreshing a texture therefore looked like a scene draw and opened a bracket of its own around the *invalidation*, so the following draw composited twice and blended the scene against itself. Only reachable with one of the six new modes active, so no released version is affected, but it is a real re-entrancy hole in the same class as the guards already covering `toFrameTexture` and `blitEffect` |
22 | 22 |
|
23 | | -### Changed |
24 | | -- **A game already setting one of those six modes on WebGL or WebGPU will look different.** They previously fell back to `"normal"` silently and rendered unblended; they now blend for real. This is the intended fix, but it is a visible change for any existing scene that set one — and for code branching on `setBlendMode`'s return value to detect support, which now reports them honoured |
25 | | -- The advanced modes cost **one destination capture and one composite per draw call**, which is what per-draw blending against the live framebuffer requires without framebuffer-fetch hardware. Right for accents (a glow, a light overlay, a coloured wash); expensive for hundreds of blended objects and unsuitable for something like an entire tilemap layer in `overlay` |
26 | | -- One path does not support them and falls back to `"normal"` with a one-time console warning rather than silently: **3D meshes** (`drawMesh`, where the offscreen's separate depth buffer would break subsequent depth testing). Gradient fills, shapes, sprites, text, image layers and particles all blend normally. `exclusion`, added in 20.0.0, is unaffected on every path since it needs no shader |
27 | | - |
28 | 23 | ## [20.1.1] (melonJS 2) - _2026-08-25_ |
29 | 24 |
|
30 | 25 | ### Fixed |
|
0 commit comments