From 9963354d817bffd56bc1fc689ff3fc6bde55149a Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sun, 13 Sep 2026 21:46:45 +0800 Subject: [PATCH 1/3] Typings: glTF geometry, Texture2d meshes, 32-bit indices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four are things the engine already does and the types denied. `GLTFData.nodes` was `object[]` with every field named in prose, so reading `node.vertices` to feed a `Mesh` — which is why the descriptor is exposed at all — did not compile. Typed as `GLTFNode[]`. `Mesh`'s `texture` resolves a `Texture2d` at mesh.js:101, deliberately, so a procedural `NoiseTexture2d` can skin geometry; the type listed only images, canvases, atlases and names. `Mesh`'s `indices` preserves a `Uint32Array` on purpose — there is a comment saying truncating one would silently corrupt any mesh past 65535 vertices, and that the glTF parser emits them for exactly that case — while the type said `Uint16Array|number[]`. A shader `Asset` may carry `{glsl, wgsl}` so one asset serves both backends, which the parser has handled since the dual-language work; `data` was `string`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t --- packages/melonjs/CHANGELOG.md | 2 ++ packages/melonjs/src/loader/loader.js | 29 +++++++++++++++++++++++-- packages/melonjs/src/renderable/mesh.js | 4 ++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 9448923f5..b7986a102 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -11,6 +11,8 @@ - `Light3d`: `direction` and `position` accept a `Vector3d` as well as an `[x, y, z]` array, so a value the game already holds goes straight in — `color` already took a `Color`, a string or an array. The vector is read, not retained. Passing one used to be read by index and silently produce a `NaN` direction, so the light contributed nothing ([#1661](https://github.com/melonjs/melonJS/issues/1661)) +- Typings: a parsed glTF node's geometry is readable from TypeScript. `GLTFData.nodes` was `object[]` with its fields listed in prose, so `node.vertices` did not compile — the array is `GLTFNode[]` now. `Mesh`'s `texture` accepts a `Texture2d` and its `indices` a `Uint32Array`, both of which it already handled; a shader `Asset` may carry the `{glsl, wgsl}` pair the loader has always parsed + ### Fixed - Renderable: opacity now cascades — `alpha` multiplies with the alpha already on the renderer instead of replacing it, so a child at 0.5 inside a parent at 0.5 draws at 0.25. **A nested renderable that was visibly opaque under a faded ancestor will now fade with it** - GLTFModel: a loaded model reported no bounds at all, so a model with a `Body` was filed in the broadphase away from where it stood and collided with nothing. Its extent now comes from the glTF bounding box, measured once at load diff --git a/packages/melonjs/src/loader/loader.js b/packages/melonjs/src/loader/loader.js index d241dd311..701990214 100644 --- a/packages/melonjs/src/loader/loader.js +++ b/packages/melonjs/src/loader/loader.js @@ -270,7 +270,7 @@ function onLoadingError(res) { * @property {string|string[]} [src] - path and/or file name of the resource (for audio assets only the path is required). * For image assets, an array of sources can be provided as a fallback chain (e.g. compressed texture formats by priority, with a PNG fallback). * The loader will try each source in order and use the first one that loads successfully. - * @property {string} [data] - inline content if not provided through a src url: TMX data for "tmx" assets, GLSL source (the ShaderEffect fragment-body convention) for "shader" assets + * @property {string|{glsl?: string, wgsl?: string}} [data] - inline content if not provided through a src url: TMX data for "tmx" assets, GLSL source (the ShaderEffect fragment-body convention) for "shader" assets. A shader may instead carry a `{glsl, wgsl}` pair, so one asset serves both backends; either half may be omitted * @property {boolean} [stream=false] - Set to true to not to wait for large audio or video file to be downloaded before playing. * @property {boolean} [autoplay=false] - Set to true to automatically start playing audio or video when loaded or added to a scene (using autoplay might require user iteraction to enable it) * @property {boolean} [loop=false] - Set to true to automatically loop the audio or video when playing @@ -931,10 +931,35 @@ export function getOBJ(elt) { return null; } +/** + * One mesh primitive out of a parsed glTF/GLB scene. + * + * Spelled out rather than left as `object` so the geometry can be read from + * TypeScript — feeding `vertices`/`uvs`/`normals`/`indices` straight into a + * {@link Mesh} or {@link InstancedMesh} is the whole point of exposing it. + * @typedef {object} GLTFNode + * @property {number[]} world - accumulated world transform, 16 floats, column-major + * @property {Float32Array} vertices - positions, x,y,z triplets + * @property {Float32Array} normals - per-vertex normals + * @property {Float32Array} uvs - texture coordinates, u,v pairs + * @property {Uint16Array|Uint32Array} indices - triangle vertex indices + * @property {number} vertexCount - number of vertices + * @property {HTMLImageElement|null} image - decoded baseColor texture, or `null` + * @property {number[]} [baseColorFactor] - material baseColor factor, `[r, g, b, a]` + * @property {Uint32Array} [colors] - per-vertex colour, packed RGBA8 + * @property {string} [textureRepeat] - wrap mode derived from the glTF sampler + * @property {string} [textureFilter] - magnification filter derived from the glTF sampler + * @property {number} [alphaCutoff] - cutout threshold from `alphaMode: "MASK"` + * @property {number[]} [emissive] - emissive factor, `[r, g, b]` + * @property {boolean} [unlit] - the material carried `KHR_materials_unlit` + * @property {boolean} [doubleSided] - the material is double-sided + * @property {string} [name] - the source node's name + */ + /** * a parsed glTF/GLB scene descriptor, as returned by {@link loader.getGLTF} * @typedef {object} GLTFData - * @property {object[]} nodes - one entry per mesh primitive: accumulated `world` transform, `vertices`, `normals`, `uvs`, `indices`, `vertexCount`, decoded baseColor `image` (or `null`), `baseColorFactor`, per-vertex `colors`, sampler-derived `textureRepeat`/`textureFilter`, `alphaCutoff`, `emissive`, `unlit` (KHR_materials_unlit), `doubleSided`, and the source node `name` + * @property {GLTFNode[]} nodes - one entry per mesh primitive * @property {Array<{world: number[], type?: string, perspective?: {yfov?: number, aspectRatio?: number, znear?: number, zfar?: number}, orthographic?: object}>} cameras - glTF cameras, each with its `world` transform + the glTF camera parameters (`perspective` for perspective cameras, `orthographic` otherwise) * @property {object[]} lights - parsed `KHR_lights_punctual` lights (`type`, `color`, `intensity`, `range`, `innerConeAngle`/`outerConeAngle` for spots, world-space `direction`/`position`, `name`) * @property {{min: number[], max: number[]}} bounds - world-space scene bounds in glTF units diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index 796257ada..82d96fd46 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -313,8 +313,8 @@ function buildTextureGroups( * @property {string} [model] - name of a preloaded OBJ model (via loader.preload with type "obj"). Vertex normals come with it — authored `vn` when the file has them, generated from face geometry when it does not — so an OBJ model can be `lit`. * @property {Float32Array|number[]} [vertices] - vertex positions as x,y,z triplets (alternative to `model`) * @property {Float32Array|number[]} [uvs] - texture coordinates as u,v pairs (alternative to `model`) - * @property {Uint16Array|number[]} [indices] - triangle vertex indices (alternative to `model`) - * @property {HTMLImageElement|HTMLCanvasElement|TextureAtlas|string} [texture] - the texture to apply (image name, HTMLImageElement, or TextureAtlas). If omitted and settings.material is provided, the texture is resolved from the MTL material's map_Kd. Passing this pins ONE binding over the whole model, which on a multi-material model suppresses the per-material texture split — see {@link Mesh#textureGroups}. + * @property {Uint16Array|Uint32Array|number[]} [indices] - triangle vertex indices (alternative to `model`). A `Uint32Array` is preserved as-is for meshes past 65535 vertices — which is what the glTF parser emits for them — while a plain array is materialized as `Uint16Array` + * @property {HTMLImageElement|HTMLCanvasElement|Texture2d|TextureAtlas|string} [texture] - the texture to apply (image name, HTMLImageElement, or TextureAtlas). If omitted and settings.material is provided, the texture is resolved from the MTL material's map_Kd. Passing this pins ONE binding over the whole model, which on a multi-material model suppresses the per-material texture split — see {@link Mesh#textureGroups}. * @property {string} [material] - name of a preloaded MTL material (via loader.preload with type "mtl"). When provided, the diffuse texture (map_Kd), tint color (Kd), and opacity (d) are automatically applied. On a multi-material model each material's own `map_Kd` is bound for its own slice of the geometry (#1573) and each `Kd` is baked per-vertex, so one `Mesh` renders the whole model. * @property {number} width - display width in pixels. With normalization on (the default) the model is scaled to fit this size; with `normalize: false` this is the uniform pixels-per-unit scale applied to the raw geometry. With `normalize: false` and an explicit `scale`, an omitted `width`/`height` is derived from the GEOMETRY's own extent, the way a Sprite takes its size from its frame — a mesh that reports no extent misleads frustum culling, pointer picking and the physics broadphase alike. * @property {number} [height] - display height in pixels (normalized models only; ignored when `normalize: false`) From cb7463e4dd01cf2ff59158bde3ce3b73f1fba512 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Mon, 14 Sep 2026 15:36:30 +0800 Subject: [PATCH 2/3] Typings: setUniform accepts a scalar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `setUniform` was typed `object|Float32Array`, so passing a number — the commonest case by far — did not compile from TypeScript. The type was wrong rather than deliberately narrow: the method's own JSDoc examples pass numbers (`setUniform("uStrength", 0.5)`), the engine passes them internally (`uUVYDir`, `uTime`), and the implementation has an explicit scalar branch that exists to handle them. Widened to `number|boolean|number[]|Float32Array|object` on both `ShaderEffect` and `GLShader`, where the same wrong type had been copy-pasted, and documented which form goes with which GLSL type. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t --- packages/melonjs/CHANGELOG.md | 1 + packages/melonjs/src/video/effects/shadereffect.js | 12 +++++++++++- packages/melonjs/src/video/webgl/glshader.js | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index b7986a102..65eaa5e3d 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -24,6 +24,7 @@ - Mesh: a mesh with `normalize: false` and an explicit `scale` now sizes itself from its geometry when no `width`/`height` is given. It reported a zero-size box while drawing at full size, misleading frustum culling, pointer picking and the broadphase alike - Mesh: a `ShaderEffect` attached to a mesh drew the geometry unplaced and without the camera on WebGL, and was refused on WebGPU with a message naming the wrong reason ([#1658](https://github.com/melonjs/melonJS/issues/1658)) - `Sprite3d`: `fog: false` reaches the mesh — a sprite builds its own settings for `Mesh` and did not copy it, so a sun could not be kept out of the haze. `transparent` and `castGroundShadow` are documented on `Sprite3d` too +- Typings: `setUniform` accepts a number. It was typed `object|Float32Array`, so setting a `float` uniform — the commonest case, and the one the method's own examples show — did not compile from TypeScript. Scalars, arrays, `Float32Array` and any `toArray()`-bearing object are now all in the signature - Typings: documented options no longer fail to compile — the `Noise` settings `NoiseTexture2d` forwards, the `Mesh` settings `InstancedMesh` forwards, `setCurrentAnimation(name, { loop: true })`, non-object values for `Container#setChildsProperty`, an `HTMLCanvasElement` as `image`/`texture`, and `super.update(dt)` in a custom `Stage`. Settings shapes are exported as `MeshSettings`, `InstancedMeshSettings`, `NoiseTexture2dSettings` and `AnimationOptionsInput` ## [20.4.0] (melonJS 2) - _2026-09-09_ diff --git a/packages/melonjs/src/video/effects/shadereffect.js b/packages/melonjs/src/video/effects/shadereffect.js index 116704d39..0cb1d0e5f 100644 --- a/packages/melonjs/src/video/effects/shadereffect.js +++ b/packages/melonjs/src/video/effects/shadereffect.js @@ -396,7 +396,17 @@ export default class ShaderEffect { /** * Set the uniform to the given value * @param {string} name - the uniform name - * @param {object|Float32Array} value - the value to assign to that uniform + * @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that + * uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean; + * vectors and matrices take an array, a `Float32Array`, or any object + * exposing `toArray()` — which is every {@link Vector2d}, + * {@link Vector3d}, {@link Color} and {@link Matrix3d}. + * @example + * // a scalar the body declares as `uniform float uStrength;` + * fx.setUniform("uStrength", 0.5); + * // a vec3 — an array, or anything with toArray() + * fx.setUniform("uTint", [1.0, 0.82, 0.55]); + * fx.setUniform("uOrigin", new me.Vector2d(0.5, 0.5)); */ setUniform(name, value) { // forward whenever a live shader exists (WebGL mode): GLShader handles diff --git a/packages/melonjs/src/video/webgl/glshader.js b/packages/melonjs/src/video/webgl/glshader.js index cf00737af..fbced3011 100644 --- a/packages/melonjs/src/video/webgl/glshader.js +++ b/packages/melonjs/src/video/webgl/glshader.js @@ -366,9 +366,14 @@ export default class GLShader { /** * Set the uniform to the given value * @param {string} name - the uniform name - * @param {object|Float32Array} value - the value to assign to that uniform + * @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that + * uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean; + * vectors and matrices take an array, a `Float32Array`, or any object + * exposing `toArray()` — which is every {@link Vector2d}, + * {@link Vector3d}, {@link Color} and {@link Matrix3d}. * @example * myShader.setUniform("uProjectionMatrix", this.projectionMatrix); + * myShader.setUniform("uStrength", 0.5); // a scalar is a plain number */ setUniform(name, value) { if (this.destroyed) { From 7bd2fd4af446e1f48fee122e956de8f17a2e0ac2 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Mon, 14 Sep 2026 15:43:00 +0800 Subject: [PATCH 3/3] Skill: a camera post-effect covers the HUD too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The effects skill said an effect attaches to a renderable or to a camera "for full-screen grading", without saying that a camera's post-effect brackets the entire world draw — floating children included. Written the obvious way, a full-screen pass washes over every HUD label in that world. Documents the alternative (host the effect on a screen-filling floating renderable ordered below the HUD's z), why such a quad needs none of the screen_uv / screen_texture builtins, and that a higher z draws later. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t --- .../melonjs-effects-and-shaders/SKILL.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md b/packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md index aa5d1a9bf..83486afae 100644 --- a/packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md +++ b/packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md @@ -25,6 +25,34 @@ app.viewport.addPostEffect(new VignetteEffect(app.renderer)); `renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys whatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`. +### A camera effect covers the HUD too + +A camera's post-effect brackets the **entire** world draw — floating children +included. So the obvious way to write a full-screen pass also washes over every +HUD label in that world, which is rarely what you want. + +To land a pass *after* the world but *before* the HUD, host the effect on a +screen-filling floating renderable ordered below the HUD's z instead: + +```js +const quad = new Sprite(0, 0, { image: anyImage }); +quad.anchorPoint.set(0, 0); +quad.scale(viewW / anyImage.width, viewH / anyImage.height); +quad.floating = true; // its own uv is now screen space +quad.blendMode = "additive"; +quad.addPostEffect(myEffect); +world.addChild(quad, HUD_Z - 10); // BELOW the labels, not above +``` + +Two things follow from the quad filling the frame: its own `uv` is screen space +(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins), +and the incoming `color` is the quad's own texture, which a body that paints +from scratch can ignore entirely. + +Depth here is easy to get backwards — a higher z draws **later**, i.e. on top. +Verify it by returning a flat colour from the body for one frame: whatever it +tints is what the pass covers. + ## Toggle with `enabled`, do not remove **`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and