Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions packages/examples/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ body:has(#screen canvas) {
* below the 41 px topbar. Without `pointer-events: none`, the
* empty `#screen` div (after an example unmounts) still sits on
* top of the index-page cards and silently swallows every click.
* The canvas inside opts back in with `pointer-events: auto`.
* Direct children opt back in with `pointer-events: auto` —
* not just the canvas, since examples may also append HTML
* controls (dropdowns, HUDs) to `#screen`. After unmount, no
* children remain → nothing blocks clicks.
*/
pointer-events: none;
}

#screen > * {
pointer-events: auto;
}

#screen canvas {
display: block;
pointer-events: auto;
}

/* Example top bar */
Expand Down
19 changes: 19 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [19.7.0] (melonJS 2) - _unreleased_

**Highlights:** foundational vertex-pipeline work toward the 19.7 Camera3d release. Every batched-rendering shader (`Quad`, `LitQuad`, `Primitive`, GPU TMX) now carries per-sprite depth as a true `vec3` vertex attribute — unblocking perspective projection for sprites, enabling depth-aware custom `ShaderEffect`s, and aligning the entire batched pipeline with the WebGPU vertex model. Backward compatible: existing custom `GLShader` / `ShaderEffect` code keeps working unchanged.

### Added
- **`renderer.setDepth(depth)`** — new public method on the base `Renderer`. Mirrors the existing `setTint` / `setColor` state-setter pattern. Sets `renderer.currentDepth`, which the batchers read at vertex-emit time and push as the z component of each vertex. `Renderable.preDraw` now forwards `this.depth` automatically, so user code typically never needs to call `setDepth` directly.
- Per-sprite depth on the GPU: all batched draw paths (`QuadBatcher`, `LitQuadBatcher`, `PrimitiveBatcher`, GPU TMX) now carry the renderable's `.depth` as the z component of their vertex stream. Default shaders consume it via `vec4(aVertex, 1.0)` in `gl_Position`. With an orthographic projection (the engine default), z has no visible effect — existing 2D apps render identically. With a perspective projection, sprites at different depths are correctly scaled and parallaxed by the projection matrix.

### Changed
- **Vertex attribute layout: `aVertex` widened from `vec2` to `vec3`** across `quad-multi.vert`, `quad-multi-lit.vert`, `primitive.vert`, `orthogonal-tmxlayer.vert`. `Mesh`'s shader already used `vec3 aVertex` — all batchers are now uniform. Per-vertex stride grows by 4 bytes (24 → 28 for the quad layout). Custom shaders binding attributes by name (`gl.getAttribLocation`) — the standard pattern, and what `GLShader` enforces — keep working unchanged; the byte-offset shift of `aRegion` / `aColor` / `aTextureId` is transparent because the batcher updates its own `vertexAttribPointer` offsets. Custom shaders declaring `attribute vec2 aVertex;` continue to work — WebGL silently drops the unused z component.
- **`VertexArrayBuffer.push()` signature gained a `z` parameter** between `y` and `u`: `push(x, y, z, u, v, tint, textureId?, normalTextureId?)`. `VertexArrayBuffer` is an internal class (not exported), but **subclasses of `QuadBatcher` / `PrimitiveBatcher` that reimplement `addQuad` / `drawVertices` from scratch** (not via `super.addQuad()`) and push to `vertexData` directly need to update their push call to the 7-arg form (insert `z` after `y`, default `0` works under ortho). Subclasses that delegate to `super.addQuad()` are unaffected.
- **`Camera2d` default near/far widened from `±1000` to `±1e6`.** With `aVertex.z` now participating in clip-space, the previous defaults would silently clip-cull any sprite with `depth` outside that range — common pitfalls being `Container.autoDepth = true` (default, assigns `pos.z = childCount`) with >1000 children, and Y-sort patterns (`sprite.depth = sprite.pos.y`) on tall maps. The new range covers every realistic 2D depth value while staying well within float32 precision. Override per-camera (`camera.near = -100; camera.far = 100`) for tighter z clipping when needed (e.g. perspective mode in Camera3d, PR B).

### Fixed
- None.

### Performance
- Stride grows ~16% per quad vertex. Real-world impact is below measurement noise on practical workloads — the extra 4 bytes per vertex sit inside the same upload + draw call.

## [19.6.0] (melonJS 2) - _2026-05-23_

**Highlights:** WebGL context-loss hardening release. Fixes a Windows + Chrome crash where a GPU switch lost the WebGL context and a partial `GLShader.destroy()` left the next frame's `setUniform("uTime", …)` reading from `null` uniforms. Beyond the crash, the renderer now also transparently recovers the rest of the pipeline (vertex buffer, default GL state, batchers, texture cache) across a `webglcontextlost` → `webglcontextrestored` cycle, and shaders replay their cached uniforms on restore — so the game keeps drawing across a GPU switch without any intervention from user code.
Expand Down
2 changes: 1 addition & 1 deletion packages/melonjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "melonjs",
"version": "19.6.0",
"version": "19.7.0",
"description": "melonJS Game Engine",
"homepage": "http://www.melonjs.org/",
"type": "module",
Expand Down
19 changes: 13 additions & 6 deletions packages/melonjs/src/camera/camera2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,21 @@ export default class Camera2d extends Renderable {
damping: number;

/**
* the closest point relative to the camera
* @default -1000
* the closest point relative to the camera. Widened from `-1000` in
* 19.7 to accommodate sprites at large `depth` values now that
* `aVertex.z` participates in clip-space (PR A): the default
* `Container.autoDepth = true` assigns `pos.z = childCount` so any
* container with >1000 children would otherwise clip-cull, and the
* common Y-sort pattern `sprite.depth = sprite.pos.y` exceeds 1000
* on tall maps. Override per-camera if you need tighter z clipping.
* @default -1e6
*/
near: number;

/**
* the furthest point relative to the camera.
* @default 1000
* the furthest point relative to the camera. Widened from `1000` in
* 19.7 — see {@link Camera2d#near}.
* @default 1e6
*/
far: number;

Expand Down Expand Up @@ -206,9 +213,9 @@ export default class Camera2d extends Renderable {

this.damping = 1.0;

this.near = -1000;
this.near = -1e6;

this.far = 1000;
this.far = 1e6;

this.projectionMatrix = new Matrix3d();

Expand Down
6 changes: 6 additions & 0 deletions packages/melonjs/src/renderable/renderable.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,12 @@ export default class Renderable extends Rect {
// apply the current tint and opacity
renderer.setTint(this.tint, this.getOpacity());

// forward depth to the renderer — pushed into the WebGL vertex
// stream as the `z` component by the batchers. No visible effect
// under the default ortho projection; required for perspective
// (Camera3d) to scale and parallax this renderable by distance.
renderer.setDepth(this.depth);

// apply blending if different from "normal"
if (this.blendMode !== renderer.getBlendMode()) {
renderer.setBlendMode(this.blendMode);
Expand Down
22 changes: 12 additions & 10 deletions packages/melonjs/src/video/buffer/vertex.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,33 @@ export default class VertexArrayBuffer {
* push a new vertex to the buffer
* @param {number} x - x position
* @param {number} y - y position
* @param {number} u - texture U coordinate
* @param {number} v - texture V coordinate
* @param {number} z - z position (per-renderable depth; 0 under ortho)
* @param {number} u - texture U coordinate (or aNormal.x for primitives)
* @param {number} v - texture V coordinate (or aNormal.y for primitives)
* @param {number} tint - tint color in UINT32 (argb) format
* @param {number} [textureId] - texture unit index for multi-texture batching
* @param {number} [normalTextureId] - paired normal-map texture unit index, or `-1` for unlit quads
* @ignore
*/
push(x, y, u, v, tint, textureId, normalTextureId) {
push(x, y, z, u, v, tint, textureId, normalTextureId) {
const offset = this.vertexCount * this.vertexSize;

this.bufferF32[offset] = x;
this.bufferF32[offset + 1] = y;
this.bufferF32[offset + 2] = u;
this.bufferF32[offset + 3] = v;
this.bufferU32[offset + 4] = tint;
if (this.vertexSize > 5) {
this.bufferF32[offset + 5] = textureId || 0;
if (this.vertexSize > 6) {
this.bufferF32[offset + 2] = z;
this.bufferF32[offset + 3] = u;
this.bufferF32[offset + 4] = v;
this.bufferU32[offset + 5] = tint;
if (this.vertexSize > 6) {
this.bufferF32[offset + 6] = textureId || 0;
if (this.vertexSize > 7) {
// `aNormalTextureId`: -1 (sentinel for unlit) is the safe
// default when the caller doesn't supply one. Writing 0
// here would let the fragment shader's lit path activate
// on unlit quads with whatever normal-map happened to be
// bound at unit 0 — visible as garbage hemispheric
// shading on every sprite.
this.bufferF32[offset + 6] =
this.bufferF32[offset + 7] =
typeof normalTextureId === "number" ? normalTextureId : -1;
}
}
Expand Down
36 changes: 36 additions & 0 deletions packages/melonjs/src/video/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ export default class Renderer {
this.renderState.currentBlendMode = value;
}

/**
* Current per-renderable depth value. WebGL batchers push it into the
* vertex stream as the `z` component of each vertex — a no-op under the
* default orthographic projection, used by perspective (Camera3d) to
* scale and parallax sprites by distance. Mirrors `renderable.depth`,
* set automatically by `Renderable.preDraw` via {@link Renderer#setDepth}.
* @type {number}
* @default 0
*/
get currentDepth() {
return this.renderState.currentDepth;
}

set currentDepth(value) {
this.renderState.currentDepth = value;
}

/**
* return the height of the canvas which this renderer draws to
* @returns {number} height of the system Canvas
Expand Down Expand Up @@ -939,6 +956,25 @@ export default class Renderer {
this.currentTint.setFloat(1.0, 1.0, 1.0, 1.0);
}

/**
* Set the current per-renderable depth value. WebGL batchers push it
* into the vertex stream as the `z` component of each vertex — a no-op
* under the default orthographic projection, used by perspective
* (Camera3d) to scale and parallax sprites by distance.
*
* Typically called automatically by `Renderable.preDraw` from the
* renderable's `depth` property. User code only needs to call this
* directly when emitting draw calls outside of a `Renderable.draw()`
* — e.g. from a custom `Container.draw()` override.
*
* Honored by the save/restore stack like `setTint` / `setColor`.
* @param {number} depth - the depth value to set
* @see Renderable#depth
*/
setDepth(depth) {
this.currentDepth = depth;
}

/**
* creates a Blob object representing the last rendered frame
* @param {string} [type="image/png"] - A string indicating the image format
Expand Down
20 changes: 20 additions & 0 deletions packages/melonjs/src/video/renderstate.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ export default class RenderState {
*/
this.currentShader = undefined;

/**
* current per-renderable depth value, pushed into the vertex stream
* by the WebGL batchers as the `z` component of each vertex. Has no
* visible effect under the engine's default orthographic projection;
* required for perspective (Camera3d) to scale sprites by distance.
* Mirrors `renderable.depth`, set by `Renderable.preDraw` via
* {@link Renderer#setDepth}.
* @type {number}
*/
this.currentDepth = 0;

// ---- pre-allocated save/restore stack ----

/**
Expand Down Expand Up @@ -110,6 +121,9 @@ export default class RenderState {

/** @ignore */
this._shaderStack = new Array(this._stackCapacity);

/** @ignore */
this._depthStack = new Float32Array(this._stackCapacity);
}

/**
Expand All @@ -130,6 +144,7 @@ export default class RenderState {
this._lineDashStack[depth] = this.lineDash;
this._blendStack[depth] = this.currentBlendMode;
this._shaderStack[depth] = this.currentShader;
this._depthStack[depth] = this.currentDepth;

if (scissorTestActive) {
this._scissorStack[depth].set(this.currentScissor);
Expand Down Expand Up @@ -187,6 +202,7 @@ export default class RenderState {
this.currentGradient = this._gradientStack[depth];
this.lineDash = this._lineDashStack[depth];
this.currentShader = this._shaderStack[depth];
this.currentDepth = this._depthStack[depth];
const scissorActive = !!this._scissorActive[depth];
if (scissorActive) {
this.currentScissor.set(this._scissorStack[depth]);
Expand Down Expand Up @@ -215,6 +231,7 @@ export default class RenderState {
this.currentScissor[2] = width;
this.currentScissor[3] = height;
this.currentShader = undefined;
this.currentDepth = 0;
}

/** @private — doubles stack capacity when exceeded */
Expand All @@ -234,6 +251,9 @@ export default class RenderState {
const newScissorActive = new Uint8Array(newCap);
newScissorActive.set(this._scissorActive);
this._scissorActive = newScissorActive;
const newDepthStack = new Float32Array(newCap);
newDepthStack.set(this._depthStack);
this._depthStack = newDepthStack;
this._stackCapacity = newCap;
}
}
68 changes: 55 additions & 13 deletions packages/melonjs/src/video/webgl/batchers/lit_quad_batcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ export default class LitQuadBatcher extends QuadBatcher {
Object.getPrototypeOf(QuadBatcher.prototype).init.call(this, renderer, {
attributes: [
{
// vec3: (x, y, z). z carries `renderable.depth` for
// perspective projection (Camera3d). Stride = 32 bytes.
name: "aVertex",
size: 2,
size: 3,
type: renderer.gl.FLOAT,
normalized: false,
offset: 0 * Float32Array.BYTES_PER_ELEMENT,
Expand All @@ -54,28 +56,28 @@ export default class LitQuadBatcher extends QuadBatcher {
size: 2,
type: renderer.gl.FLOAT,
normalized: false,
offset: 2 * Float32Array.BYTES_PER_ELEMENT,
offset: 3 * Float32Array.BYTES_PER_ELEMENT,
},
{
name: "aColor",
size: 4,
type: renderer.gl.UNSIGNED_BYTE,
normalized: true,
offset: 4 * Float32Array.BYTES_PER_ELEMENT,
offset: 5 * Float32Array.BYTES_PER_ELEMENT,
},
{
name: "aTextureId",
size: 1,
type: renderer.gl.FLOAT,
normalized: false,
offset: 5 * Float32Array.BYTES_PER_ELEMENT,
offset: 6 * Float32Array.BYTES_PER_ELEMENT,
},
{
name: "aNormalTextureId",
size: 1,
type: renderer.gl.FLOAT,
normalized: false,
offset: 6 * Float32Array.BYTES_PER_ELEMENT,
offset: 7 * Float32Array.BYTES_PER_ELEMENT,
},
],
shader: {
Expand Down Expand Up @@ -315,10 +317,49 @@ export default class LitQuadBatcher extends QuadBatcher {
}

const textureId = this.useMultiTexture ? unit : 0;
vertexData.push(vec0.x, vec0.y, u0, v0, tint, textureId, normalTextureId);
vertexData.push(vec1.x, vec1.y, u1, v0, tint, textureId, normalTextureId);
vertexData.push(vec2.x, vec2.y, u0, v1, tint, textureId, normalTextureId);
vertexData.push(vec3.x, vec3.y, u1, v1, tint, textureId, normalTextureId);
// z = current renderer depth (Renderable.preDraw); a no-op under ortho,
// consumed by perspective (Camera3d) — matches QuadBatcher.addQuad.
const z = this.renderer.currentDepth;
vertexData.push(
vec0.x,
vec0.y,
z,
u0,
v0,
tint,
textureId,
normalTextureId,
);
vertexData.push(
vec1.x,
vec1.y,
z,
u1,
v0,
tint,
textureId,
normalTextureId,
);
vertexData.push(
vec2.x,
vec2.y,
z,
u0,
v1,
tint,
textureId,
normalTextureId,
);
vertexData.push(
vec3.x,
vec3.y,
z,
u1,
v1,
tint,
textureId,
normalTextureId,
);
}

/**
Expand Down Expand Up @@ -360,11 +401,12 @@ export default class LitQuadBatcher extends QuadBatcher {
m.apply(vec3);
}

// blits are always rendered at z = 0 (screen-space, ortho)
const tint = 0xffffffff;
this.vertexData.push(vec0.x, vec0.y, 0, 1, tint, 0, -1);
this.vertexData.push(vec1.x, vec1.y, 1, 1, tint, 0, -1);
this.vertexData.push(vec2.x, vec2.y, 0, 0, tint, 0, -1);
this.vertexData.push(vec3.x, vec3.y, 1, 0, tint, 0, -1);
this.vertexData.push(vec0.x, vec0.y, 0, 0, 1, tint, 0, -1);
this.vertexData.push(vec1.x, vec1.y, 0, 1, 1, tint, 0, -1);
this.vertexData.push(vec2.x, vec2.y, 0, 0, 0, tint, 0, -1);
this.vertexData.push(vec3.x, vec3.y, 0, 1, 0, tint, 0, -1);

this.flush();

Expand Down
Loading
Loading