Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ab309b7
feat(texture): generalize the Texture2d contract + setTexture accepts…
obiot Jul 8, 2026
472caec
test: Canvas-stub coverage for setTexture(Texture2d)
obiot Jul 8, 2026
9923a33
fix(shader): setUniform/setTexture no longer drop values while disabled
obiot Jul 8, 2026
0baac96
fix(shader): keep the destroy() no-op guard on the ungated setters
obiot Jul 8, 2026
912cd3b
refactor(texture): address PR #1543 review — opaque type, unit reuse,…
obiot Jul 8, 2026
7c764c6
feat(renderer): toFrameTexture() — GPU frame capture as a Texture2d (…
obiot Jul 8, 2026
cc00415
feat(examples): Aquarium — toFrameTexture screen-space refraction; fi…
obiot Jul 8, 2026
79af16b
feat(examples): port the contributor's aquarium to toFrameTexture (re…
obiot Jul 8, 2026
238b00d
refactor(toFrameTexture): address PR #1543 review round 2
obiot Jul 8, 2026
ce66be3
refactor(toFrameTexture): address PR #1543 review round 3
obiot Jul 8, 2026
c3b71c1
fix(examples): smooth fish movement in aquarium (subPixel rendering)
obiot Jul 8, 2026
36d70e5
fix(examples): soften the default water ripple strength in aquarium
obiot Jul 8, 2026
0f190bd
refactor(renderer): declare toFrameTexture on the base Renderer
obiot Jul 8, 2026
4e9a2c2
fix(webgl): toFrameTexture must not corrupt lit-batcher normal-map units
obiot Jul 9, 2026
01da107
fix(webgl): invalidate ALL directly-bound top units, not just the cap…
obiot Jul 9, 2026
d8ff819
refactor(toFrameTexture): PR review — target/region validation, docs,…
obiot Jul 9, 2026
abecc20
test(toFrameTexture): harden the added tests to be genuinely adversarial
obiot Jul 9, 2026
e6574b5
refactor(toFrameTexture): region accepts a Bounds, not a Rect
obiot Jul 9, 2026
e6838f7
test(toFrameTexture): dedicated adversarial rect.getBounds() region test
obiot Jul 9, 2026
f50a26e
chore(toFrameTexture): final PR review — CHANGELOG, RGB comment, inva…
obiot Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
### Added
- **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path.
- **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example.
- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a drawable source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing canvas/image — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy.
- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a drawable source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing canvas/image — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (a renderer texture resource that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow.
Comment thread
obiot marked this conversation as resolved.
Outdated
Comment thread
obiot marked this conversation as resolved.
Outdated
Comment thread
obiot marked this conversation as resolved.
Outdated
- **Procedural noise: `Noise` + `NoiseTexture2d`** — `me.Noise` is a renderer-free coherent-noise generator (simplex / perlin / value / valueCubic / cellular, with fBm, ridged and ping-pong fractal layering, plus optional domain warp) that you can sample on the CPU via `getNoise2d` / `getNoise3d` for gameplay (heightmaps, terrain, spawn jitter) — fully deterministic per `seed`. `me.NoiseTexture2d` is a `Texture2d` that bakes a `Noise` field into a drawable canvas, with three output modes: grayscale, a `Gradient` **color ramp**, or a tangent-space **normal map** (`asNormalMap` + `bumpStrength`) for per-pixel lighting; optional `seamless` tiling (`seamlessBlendSkirt`); and **live animation** (`animated` + `speed` — sampled in 3D, advanced by `update(dt)`). Each re-bake bumps a content `version` stamped on the canvas; the WebGL lit pipeline re-uploads the normal-map texture only when that version changes (the three.js `Texture.needsUpdate` model — no renderer handle needed). Both work under the Canvas and WebGL backends.
- **`shared` flag on `ShaderEffect` / `GLShader`** — set `effect.shared = true` on a post-processing shader that is reused across several renderables, so one renderable's cleanup (the `shader` setter, `removePostEffect`, `clearPostEffects`, or `destroy()`) doesn't free the GL program the others still use. Defaults to `false` (unchanged auto-destroy behavior); when set, you own the shader's lifecycle and call `destroy()` yourself.
- **`ShaderEffect.setTime(seconds)`** — a convenience for the `uTime` shader-animation convention, now on the base class: call it once per frame to write a shader's `uniform float uTime` (e.g. to scroll a static texture's UVs, or pulse / wave), driven by whatever clock you choose. A no-op when the shader doesn't declare `uTime`, so it's safe on any effect. Manual by design — the engine never calls it, so animation stays opt-in. The built-in `Hologram` / `Shine` effects now inherit it (their identical `setTime` overrides were removed).
- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D <name>;` in your fragment and pass any engine texture (e.g. `noiseTexture.getTexture()`); the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the new **Water Refraction** example (a perspective pool floor refracted through a GPU-scrolled seamless `NoiseTexture2d`).
- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D <name>;` in your fragment and pass any engine texture — a `Texture2d` asset (`NoiseTexture2d`, `TextureAtlas`, …) directly, or a raw drawable source; the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the new **Water Refraction** example (a perspective pool floor refracted through a GPU-scrolled seamless `NoiseTexture2d`).
Comment thread
obiot marked this conversation as resolved.
Outdated
Comment thread
obiot marked this conversation as resolved.
Outdated
Comment thread
obiot marked this conversation as resolved.
Outdated
- **Anchor-point presets + `Sprite3d` anchor support** (#1514) — `settings.anchorPoint` now accepts the named presets `"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"` in addition to an `{x, y}` object, uniformly across `Sprite`, `Entity`, `Collectable`, `ImageLayer`, `Text`, `BitmapText` (and their subclasses), including as a plain-string Tiled property. `Sprite3d` gains anchoring through the same `settings.anchorPoint` key (centered default, same convention as `Sprite`): the anchor is baked into the quad's local vertices so it composes with billboarding, flips and trimmed/rotated atlas frames, never leaks into the renderer transform on either camera path, is mutable at runtime via `sprite3d.anchorPoint.set(...)` (automatic re-bake + cull-bounds update), and grows the frustum-cull bounds exactly so an anchored sprite can't pop out while still on screen. On the 2D classes invalid values (unknown preset, malformed object) keep their historical silent `(0, 0)` outcome for full backward compatibility, but now log a console warning; `Sprite3d` (a new surface) throws. See the updated billboard example (`anchorPoint: "bottom"` — feet on the ground plane).
- **Shader preloading: `"shader"` asset type + `loader.getShader()` + `ShaderEffect.clone()`** — custom shaders can now be preloaded like any other asset and are **compiled at load time** (the GLSL compile cost lands in the loading screen, and a compile error fails the load — `loader.load()` rejects with an error carrying the asset's name). The source is a GLSL fragment body following the `ShaderEffect` convention (`uniform` declarations + `vec4 apply(vec4 color, vec2 uv)`), loaded from a `src` URL / data: URI or inline via the `data` field (the inline-TMX convention): `{ name: "waterRipple", type: "shader", src: "shaders/waterRipple.frag" }`. `loader.getShader(name)` returns the **shared, loader-owned instance** — the *same* object on every call, with `shared = true` so renderable cleanup never auto-destroys it; everything it is assigned to shares one set of uniform values, and it is freed only by `loader.unload()` / `loader.unloadAll()` (which destroy the GL program). For per-renderable uniform values, the new `ShaderEffect.clone()` compiles an independent, caller-owned copy: it copies the *recipe* (fragment source, precision, uniform values, `setTexture` bindings — with its own GL uploads) but never the *ownership* — the clone's `shared` flag is **always reset to `false`**, so it is auto-destroyed with the renderable it is assigned to unless explicitly re-shared. `GLShader.clone()` exists likewise for raw full-program (vertex + fragment) shaders, with the same recipe-not-ownership semantics. The **Water Refraction** example now ships its fragment as a preloaded shader asset.
Comment thread
obiot marked this conversation as resolved.
Outdated
Comment thread
obiot marked this conversation as resolved.
Outdated

Expand All @@ -36,6 +36,7 @@
- **`Rect.right`/`Rect.bottom` returned the width/height when the edge sat exactly at coordinate 0** — the getters computed `this.left + w || w`, so an edge landing precisely on 0 (falsy) fell back to the size. Bounds and collision math around the origin came out wrong for such rectangles.
- **`Rect.toPolygon()` handed out the rectangle's live vertices** — `Polygon.setVertices` stores a `Vector2d[]` by reference, so the "new" polygon shared the rect's actual points array: transforming or mutating the polygon silently corrupted the rectangle. The vertices are now cloned.
- **`loader.load()` mutated the caller's asset descriptor, so retries double-prepended `baseURL`** — the resolved URL (fontface `url()` unwrap + `baseURL` prefix) was written back into `asset.src`; `loader.reload()` (which re-loads the same stored object) — or simply `load()`ing the same manifest entry twice — then fetched `base + base + src`. The URL is now resolved into a copy handed to the parser; the caller's object is never touched.
- **`ShaderEffect.setUniform()` silently dropped values while the effect was disabled** — including during a context-loss window, where the auto-disable gate defeated the very suspend-cache replay built for it (a uniform set mid-loss reverted to its pre-loss value after restore); a user-disabled effect lost values the same way. `setUniform` now always forwards to the inner shader (which correctly defers while suspended); `setTexture` likewise stores its binding regardless of the enabled state for the lazy upload on the next enabled draw, and `setTime` skips only while genuinely suspended. Canvas-mode stubs keep no-oping.
- **`loader.unload()` of a never-loaded fontface threw a TypeError** — the fontface case was the only one without a membership guard, and `FontFaceSet.delete(undefined)` is a WebIDL type error rather than a `false` return. It now returns `false` like every other asset type.
- **Re-preloading an audio manifest silently replaced each Howl and leaked the old one** — the audio parser was the only asset parser without an already-loaded guard, so returning to a stage that preloads (a common pattern) churned every clip's decoded buffers / HTML5 nodes. An already-loaded clip is now reported as cached (like every other asset type); `me.audio.unload()` first to genuinely reload one.
- **Parallel audio loads shared a single retry counter** — three failures of one flaky file pushed *another* sound's first failure straight over the give-up threshold, throwing a spurious fatal load error (the default `stopOnAudioError`). Retries are now budgeted per sound.
Expand Down
57 changes: 39 additions & 18 deletions packages/melonjs/src/video/texture/texture2d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
import type { TextureResource } from "./resource.js";

/**
* The backing source a {@link Texture2d} resolves to through
* {@link Texture2d#getTexture}: a drawable (canvas/image) for CPU-backed
* assets, or a renderer texture resource for GPU-resident ones — a resource
* uploads/binds itself through the texture cache instead of being read back
* to the CPU.
*/
export type Texture2dSource =
| HTMLCanvasElement
| HTMLImageElement
| OffscreenCanvas
| ImageBitmap
| TextureResource;

Comment thread
obiot marked this conversation as resolved.
Outdated
/**
* Abstract base for a user-constructed 2D texture asset — an object that owns
* a drawable image source and can be used anywhere the engine expects an
* image: {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer},
* or bound as a sampler uniform in a custom {@link GLShader}.
* Abstract base for a 2D texture asset — an object that owns a texture
* source and can be used anywhere the engine expects an image:
* {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer}, or
* bound as a sampler uniform in a custom shader (see
* {@link ShaderEffect#setTexture}).
Comment thread
obiot marked this conversation as resolved.
*
* A `Texture2d` is recognized via `instanceof` and resolved to its backing
* source through {@link Texture2d#getTexture} — so passing the asset object
* directly (`{ image: myTexture }`) works the same as passing a raw
* `HTMLCanvasElement`. Raw DOM image/canvas sources and the loader's decoded
* `CompressedImage` data are accepted too, but are not part of this class
* hierarchy.
*
* A `Texture2d` is recognized by the renderables via `instanceof` and resolved
* to its backing canvas/image through {@link Texture2d#getTexture} — so passing
* the asset object directly (`{ image: myTexture }`) works the same as passing a
* raw `HTMLCanvasElement`. Raw DOM image/canvas sources and the loader's
* decoded `CompressedImage` data are accepted too, but are not part of this
* class hierarchy.
* Most assets are CPU-backed and resolve to a drawable canvas/image.
* Subclasses may also be **GPU-resident**, resolving to a renderer texture
* resource that never leaves the GPU — the contract deliberately admits both
* (a future WebGPU backend follows the same shape with a `GPUTexture`-backed
* resource).
*
* Concrete implementations:
* - {@link TextureAtlas} — packed multi-region sprite sheet
*
* A future WebGPU implementation would back {@link Texture2d#getTexture} with a
* `GPUTexture`-wrapping surface rather than an `HTMLCanvasElement`.
* @category Game Objects
*/
export default abstract class Texture2d {
/**
* Return the drawable source for this texture — assignable to
* {@link Sprite#image}, {@link Sprite#normalMap}, an {@link ImageLayer}, or
* bound as a sampler uniform in a custom shader.
* @returns the backing canvas/image
* Return the backing source for this texture — a drawable canvas/image
* for CPU-backed assets, or a renderer texture resource for GPU-resident
* ones. Assignable to {@link Sprite#image}, {@link Sprite#normalMap}, an
* {@link ImageLayer}, or bound as a sampler uniform in a custom shader.
* @returns the backing source
Comment thread
obiot marked this conversation as resolved.
Outdated
*/
abstract getTexture(): HTMLCanvasElement | HTMLImageElement;
abstract getTexture(): Texture2dSource;

/**
* Release any GPU/CPU resources held by this texture. The texture must not
Expand Down
Loading
Loading