diff --git a/packages/examples/src/examples/platformer/createGame.ts b/packages/examples/src/examples/platformer/createGame.ts index 2d77899a0..a13ff2e63 100644 --- a/packages/examples/src/examples/platformer/createGame.ts +++ b/packages/examples/src/examples/platformer/createGame.ts @@ -26,7 +26,6 @@ export const createGame = () => { scaleMethod: "flex-width", renderer: AUTO, preferWebGL1: false, - depthTest: "z-buffer", subPixel: false, }); diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index c9252167b..b57879a92 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -30,13 +30,17 @@ - `Renderable.rotate(angle, v)` defaults to Z axis when no axis given (same 2D behavior) - `Renderable.scale(x, y, z)` defaults z to 1 (preserves Z dimension) - `QuadBatcher` and `MeshBatcher` now extend `MaterialBatcher` (was `Batcher`) — eliminates ~180 lines of duplicated texture management code -- WebGL context always created with `depth: true` for hardware depth buffer support -- WebGL `clear()` now always clears depth + color + stencil buffers +- WebGL context always created with `depth: true` for hardware depth buffer support (used by 3D mesh rendering) - WebGL renderer `setBatcher()` now syncs the projection matrix to the new batcher - **BREAKING**: `Text.draw()` and `BitmapText.draw()` no longer accept `text`, `x`, `y` parameters — standalone draw without a parent container is removed (deprecated since 10.6.0) - **BREAKING**: `Text.measureText()` no longer takes a `renderer` parameter (was unused) - **BREAKING**: `UITextButton` settings `backgroundColor` and `hoverColor` removed — use `hoverOffColor` and `hoverOnColor` instead - **BREAKING**: `Tween` no longer adds itself to `game.world` — uses event-based lifecycle (`TICK`, `GAME_AFTER_UPDATE`, `STATE_PAUSE`, `STATE_RESUME`, `GAME_RESET`) instead. Public API unchanged. `isPersistent` and `updateWhenPaused` properties still supported. +- **BREAKING**: removed `depthTest` application setting and `DepthTest` type — GPU depth sorting is incompatible with 2D alpha blending and the painter's algorithm. The `"z-buffer"` option never worked correctly for 2D sprites. Depth testing remains available internally for 3D mesh rendering only (`drawMesh`). +- Container: `sortOn` is now a getter/setter that caches the comparator function — avoids string lookup on each sort call +- Container: sort comparators simplified — removed legacy null guards (children always have `pos`) +- Renderer: `customShader` property moved to base `Renderer` class — `Renderable.preDraw`/`postDraw` no longer check renderer type via `renderer.gl` +- WebGL: `clear()` no longer clears the depth buffer (only used by `drawMesh` which clears it locally) ### Fixed - Geometry: `Rect.setSize()` now calls `updateBounds()` — fixes a regression from July 2024 (`4d185c902`) where replacing `Rect.setShape()` with `pos.set()` + `setSize()` during the TypeScript conversion left bounds stale, causing pointer event broadphase lookups to use `(0,0)` instead of the actual pointer position (see #817) diff --git a/packages/melonjs/src/application/application.ts b/packages/melonjs/src/application/application.ts index 273780381..c33285a11 100644 --- a/packages/melonjs/src/application/application.ts +++ b/packages/melonjs/src/application/application.ts @@ -244,7 +244,6 @@ export default class Application { scale: autoScale ? 1.0 : +merged.scale || 1.0, zoomX: 0, zoomY: 0, - depthTest: merged.depthTest === "z-buffer" ? "z-buffer" : "sorting", scaleMethod: /^(fill-(min|max)|fit|flex(-(width|height))?|stretch)$/.test( merged.scaleMethod, ) @@ -383,11 +382,6 @@ export default class Application { // app starting time this.lastUpdate = globalThis.performance.now(); - // manually sort child if depthTest setting is "sorting" - this.world.autoSort = !( - this.renderer.type === "WEBGL" && this.settings.depthTest === "z-buffer" - ); - // only register event listeners once per instance if (!this.isInitialized) { /* eslint-disable @typescript-eslint/unbound-method */ diff --git a/packages/melonjs/src/application/defaultApplicationSettings.ts b/packages/melonjs/src/application/defaultApplicationSettings.ts index 28a732906..d1b55b885 100644 --- a/packages/melonjs/src/application/defaultApplicationSettings.ts +++ b/packages/melonjs/src/application/defaultApplicationSettings.ts @@ -7,7 +7,6 @@ export const defaultApplicationSettings = { scale: 1.0, scaleMethod: ScaleMethods.Manual, preferWebGL1: false, - depthTest: "sorting", powerPreference: "default", transparent: false, antiAlias: false, diff --git a/packages/melonjs/src/application/header.ts b/packages/melonjs/src/application/header.ts index c5dfe598f..98d56e95e 100644 --- a/packages/melonjs/src/application/header.ts +++ b/packages/melonjs/src/application/header.ts @@ -11,15 +11,11 @@ export function consoleHeader(app: Application): void { typeof app.renderer.GPURenderer === "string" ? ` (${app.renderer.GPURenderer})` : ""; - const depthTesting = - renderType.includes("WebGL") && app.renderer.depthTest === "z-buffer" - ? "Depth Test | " - : ""; const audioType = device.hasWebAudio ? "Web Audio" : "HTML5 Audio"; // output video information in the console console.log( - `${renderType} renderer${gpu_renderer} | ${depthTesting}${audioType} | ` + + `${renderType} renderer${gpu_renderer} | ${audioType} | ` + `pixel ratio ${device.devicePixelRatio} | ${ device.platform.nodeJS ? "node.js" diff --git a/packages/melonjs/src/application/settings.ts b/packages/melonjs/src/application/settings.ts index 37c5e0059..a0ff4662c 100644 --- a/packages/melonjs/src/application/settings.ts +++ b/packages/melonjs/src/application/settings.ts @@ -14,8 +14,6 @@ type PhysicsType = "builtin" | "none"; type PowerPreference = "default" | "low-power"; -type DepthTest = "sorting" | "z-buffer"; - export type ApplicationSettings = { /** * renderer to use (CANVAS, WEBGL, AUTO), or a custom renderer class @@ -46,12 +44,6 @@ export type ApplicationSettings = { */ preferWebGL1: boolean; - /** - * ~Experimental~ the default method to sort object on the z axis in WebGL - * @default sorting - */ - depthTest: DepthTest; - /** * a hint to the user agent indicating what configuration of GPU is suitable for the WebGL context. To be noted that Safari and Chrome (since version 80) both default to "low-power" to save battery life and improve the user experience on these dual-GPU machines. * @default default diff --git a/packages/melonjs/src/camera/camera2d.ts b/packages/melonjs/src/camera/camera2d.ts index 196e7af36..50a342a80 100644 --- a/packages/melonjs/src/camera/camera2d.ts +++ b/packages/melonjs/src/camera/camera2d.ts @@ -760,7 +760,7 @@ export default class Camera2d extends Renderable { * @param target - the renderable to focus the camera on */ focusOn(target: Renderable): void { - const bounds = target.getBounds(); + const bounds = target.getBounds() as Bounds; this.moveTo( bounds.left + bounds.width / 2 - this.width / 2, bounds.top + bounds.height / 2 - this.height / 2, diff --git a/packages/melonjs/src/math/matrix2d.ts b/packages/melonjs/src/math/matrix2d.ts index 7bfea1c27..636898f54 100644 --- a/packages/melonjs/src/math/matrix2d.ts +++ b/packages/melonjs/src/math/matrix2d.ts @@ -403,16 +403,19 @@ export class Matrix2d { * @returns true if the matrix is an identity matrix */ isIdentity() { + const a = this.val; + + // check translation first (most commonly non-zero), then diagonal, then rest return ( - this.val[0] === 1 && - this.val[1] === 0 && - this.val[2] === 0 && - this.val[3] === 0 && - this.val[4] === 1 && - this.val[5] === 0 && - this.val[6] === 0 && - this.val[7] === 0 && - this.val[8] === 1 + a[6] === 0 && + a[7] === 0 && + a[0] === 1 && + a[4] === 1 && + a[8] === 1 && + a[1] === 0 && + a[2] === 0 && + a[3] === 0 && + a[5] === 0 ); } diff --git a/packages/melonjs/src/math/matrix3d.ts b/packages/melonjs/src/math/matrix3d.ts index 863cb0911..8a38936be 100644 --- a/packages/melonjs/src/math/matrix3d.ts +++ b/packages/melonjs/src/math/matrix3d.ts @@ -633,23 +633,24 @@ export class Matrix3d { isIdentity() { const a = this.val; + // check translation first (most commonly non-zero), then diagonal, then rest return ( + a[12] === 0 && + a[13] === 0 && a[0] === 1 && + a[5] === 1 && + a[10] === 1 && + a[15] === 1 && a[1] === 0 && a[2] === 0 && a[3] === 0 && a[4] === 0 && - a[5] === 1 && a[6] === 0 && a[7] === 0 && a[8] === 0 && a[9] === 0 && - a[10] === 1 && a[11] === 0 && - a[12] === 0 && - a[13] === 0 && - a[14] === 0 && - a[15] === 1 + a[14] === 0 ); } diff --git a/packages/melonjs/src/renderable/container.js b/packages/melonjs/src/renderable/container.js index 7db5d191e..0b8c72eb2 100644 --- a/packages/melonjs/src/renderable/container.js +++ b/packages/melonjs/src/renderable/container.js @@ -88,7 +88,8 @@ export default class Container extends Renderable { * @type {string} * @default "z" */ - this.sortOn = "z"; + this._sortOn = "z"; + this._comparator = this._sortZ; /** * Specify if the children list should be automatically sorted when adding a new child @@ -169,6 +170,26 @@ export default class Container extends Renderable { } } + /** + * The property of the child object that should be used to sort on this container. + * Accepted values: "x", "y", "z" + * @type {string} + * @default "z" + */ + get sortOn() { + return this._sortOn; + } + set sortOn(value) { + const v = value.toLowerCase(); + if (v !== "x" && v !== "y" && v !== "z") { + throw new Error( + `Invalid sortOn value: "${value}" (expected "x", "y", or "z")`, + ); + } + this._sortOn = v; + this._comparator = this["_sort" + v.toUpperCase()]; + } + /** * reset the container, removing all children, and resetting transforms. */ @@ -188,10 +209,7 @@ export default class Container extends Renderable { } } - if (typeof this.currentTransform !== "undefined") { - // just reset some variables - this.currentTransform.identity(); - } + this.currentTransform.identity(); this.backgroundColor.setColor(0, 0, 0, 0.0); } @@ -206,20 +224,20 @@ export default class Container extends Renderable { * will not be in any container.
* if the given child implements an onActivateEvent method, that method will be called * once the child is added to this container. - * @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText|Tween} child - Child to be added + * @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be added * @param {number} [z] - forces the z index of the child to the specified value * @returns {Renderable} the added child */ addChild(child, z) { + if (!(child instanceof Renderable)) { + throw new Error(`${String(child)} is not an instance of Renderable`); + } if (child.ancestor instanceof Container) { child.ancestor.removeChildNow(child); } else { // only allocate a GUID if the object has no previous ancestor // (e.g. move one child from one container to another) - if (child.isRenderable) { - // allocated a GUID value (use child.id as based index if defined) - child.GUID = createGUID(child.id); - } + child.GUID = createGUID(child.id); } // add the new child @@ -227,21 +245,17 @@ export default class Container extends Renderable { this.getChildren().push(child); // update child bounds to reflect the new ancestor - if (typeof child.updateBounds === "function") { - if (this.isFloating === true) { - // only parent container can be floating - child.floating = false; - } - child.updateBounds(); + if (this.isFloating === true) { + // only parent container can be floating + child.floating = false; } + child.updateBounds(); // set the child z value if required - if (typeof child.pos !== "undefined") { - if (typeof z === "number") { - child.pos.z = z; - } else if (this.autoDepth === true) { - child.pos.z = this.getChildren().length; - } + if (typeof z === "number") { + child.pos.z = z; + } else if (this.autoDepth === true) { + child.pos.z = this.getChildren().length; } if (this.autoSort === true) { @@ -294,16 +308,16 @@ export default class Container extends Renderable { * @returns {Renderable} the added child */ addChildAt(child, index) { + if (!(child instanceof Renderable)) { + throw new Error(`${String(child)} is not an instance of Renderable`); + } if (index >= 0 && index <= this.getChildren().length) { if (child.ancestor instanceof Container) { child.ancestor.removeChildNow(child); } else { // only allocate a GUID if the object has no previous ancestor // (e.g. move one child from one container to another) - if (child.isRenderable) { - // allocated a GUID value - child.GUID = createGUID(); - } + child.GUID = createGUID(); } // add the new child @@ -311,13 +325,11 @@ export default class Container extends Renderable { this.getChildren().splice(index, 0, child); // update child bounds to reflect the new ancestor - if (typeof child.updateBounds === "function") { - if (this.isFloating === true) { - // only parent container can be floating - child.floating = false; - } - child.updateBounds(); + if (this.isFloating === true) { + // only parent container can be floating + child.floating = false; } + child.updateBounds(); if ( typeof child.onActivateEvent === "function" && @@ -577,11 +589,9 @@ export default class Container extends Renderable { if (this.enableChildBoundsUpdate === true) { this.forEach((child) => { - if (child.isRenderable) { - const childBounds = child.updateBounds(absolute); - if (childBounds.isFinite()) { - bounds.addBounds(childBounds); - } + const childBounds = child.updateBounds(absolute); + if (childBounds.isFinite()) { + bounds.addBounds(childBounds); } }); } @@ -656,7 +666,7 @@ export default class Container extends Renderable { * Removes (and optionally destroys) a child from the container.
* (removal is immediate and unconditional)
* Never use keepalive=true with objects from {@link pool}. Doing so will create a memory leak. - * @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText|Tween} child - Child to be removed + * @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be removed * @param {boolean} [keepalive=False] - True to prevent calling child.destroy() */ removeChildNow(child, keepalive) { @@ -804,7 +814,7 @@ export default class Container extends Renderable { } this.pendingSort = defer(function () { // sort everything in this container - this.getChildren().sort(this["_sort" + this.sortOn.toUpperCase()]); + this.getChildren().sort(this._comparator); // clear the defer id this.pendingSort = null; // make sure we redraw everything @@ -829,7 +839,7 @@ export default class Container extends Renderable { * @ignore */ _sortZ(a, b) { - return b.pos && a.pos ? b.pos.z - a.pos.z : a.pos ? -Infinity : Infinity; + return b.pos.z - a.pos.z; } /** @@ -837,7 +847,7 @@ export default class Container extends Renderable { * @ignore */ _sortReverseZ(a, b) { - return a.pos && b.pos ? a.pos.z - b.pos.z : a.pos ? Infinity : -Infinity; + return a.pos.z - b.pos.z; } /** @@ -845,11 +855,7 @@ export default class Container extends Renderable { * @ignore */ _sortX(a, b) { - if (!b.pos || !a.pos) { - return a.pos ? -Infinity : Infinity; - } - const result = b.pos.z - a.pos.z; - return result ? result : b.pos.x - a.pos.x; + return b.pos.z - a.pos.z || b.pos.x - a.pos.x; } /** @@ -857,11 +863,7 @@ export default class Container extends Renderable { * @ignore */ _sortY(a, b) { - if (!b.pos || !a.pos) { - return a.pos ? -Infinity : Infinity; - } - const result = b.pos.z - a.pos.z; - return result ? result : b.pos.y - a.pos.y; + return b.pos.z - a.pos.z || b.pos.y - a.pos.y; } /** @@ -897,30 +899,25 @@ export default class Container extends Renderable { continue; } - if (obj.isRenderable) { - isFloating = globalFloatingCounter > 0 || obj.floating; - if (isFloating) { - globalFloatingCounter++; - } + isFloating = globalFloatingCounter > 0 || obj.floating; + if (isFloating) { + globalFloatingCounter++; + } - // check if object is in any active cameras - obj.inViewport = false; - // iterate through all cameras - cameras.forEach((camera) => { - if (camera.isVisible(obj, isFloating)) { - obj.inViewport = true; - } - }); + // check if object is in any active cameras + obj.inViewport = false; + // iterate through all cameras + cameras.forEach((camera) => { + if (camera.isVisible(obj, isFloating)) { + obj.inViewport = true; + } + }); - // update our object - this.isDirty |= (obj.inViewport || obj.alwaysUpdate) && obj.update(dt); + // update our object + this.isDirty |= (obj.inViewport || obj.alwaysUpdate) && obj.update(dt); - if (globalFloatingCounter > 0) { - globalFloatingCounter--; - } - } else { - // just directly call update() for non renderable object - this.isDirty |= obj.update(dt); + if (globalFloatingCounter > 0) { + globalFloatingCounter--; } } @@ -959,36 +956,34 @@ export default class Container extends Renderable { const children = this.getChildren(); for (let i = children.length, obj; i--, (obj = children[i]); ) { - if (obj.isRenderable) { - const isFloating = obj.floating === true; + const isFloating = obj.floating === true; - if (obj.inViewport || isFloating) { - // skip UI-only floating elements on non-default cameras - if (isFloating && isNonDefaultCamera && !obj.visibleInAllCameras) { - continue; - } + if (obj.inViewport || isFloating) { + // skip UI-only floating elements on non-default cameras + if (isFloating && isNonDefaultCamera && !obj.visibleInAllCameras) { + continue; + } - if (isFloating) { - renderer.save(); - renderer.resetTransform(); - if (isNonDefaultCamera) { - renderer.setProjection(viewport.screenProjection); - } + if (isFloating) { + renderer.save(); + renderer.resetTransform(); + if (isNonDefaultCamera) { + renderer.setProjection(viewport.screenProjection); } + } - obj.preDraw(renderer); - obj.draw(renderer, viewport); - obj.postDraw(renderer); + obj.preDraw(renderer); + obj.draw(renderer, viewport); + obj.postDraw(renderer); - if (isFloating) { - if (isNonDefaultCamera) { - renderer.setProjection(viewport.worldProjection); - } - renderer.restore(); + if (isFloating) { + if (isNonDefaultCamera) { + renderer.setProjection(viewport.worldProjection); } - - this.drawCount++; + renderer.restore(); } + + this.drawCount++; } } } diff --git a/packages/melonjs/src/renderable/renderable.js b/packages/melonjs/src/renderable/renderable.js index bd32b60a8..335971d8c 100644 --- a/packages/melonjs/src/renderable/renderable.js +++ b/packages/melonjs/src/renderable/renderable.js @@ -1,13 +1,13 @@ import { ObservablePoint } from "../geometries/observablePoint.ts"; import { Rect } from "./../geometries/rectangle.ts"; import { releaseAllPointerEvents } from "./../input/input.ts"; -import { Color, colorPool } from "./../math/color.ts"; +import { colorPool } from "./../math/color.ts"; import { clamp } from "./../math/math.ts"; import { Matrix3d } from "../math/matrix3d.ts"; import { ObservableVector3d } from "../math/observableVector3d.ts"; import { vector2dPool } from "../math/vector2d.ts"; import Body from "./../physics/body.js"; -import { Bounds, boundsPool } from "./../physics/bounds.ts"; +import { boundsPool } from "./../physics/bounds.ts"; import pool from "../system/legacy_pool.js"; /** @@ -347,14 +347,9 @@ export default class Renderable extends Rect { * @return {Application} the parent application or undefined if not attached to any container/app */ get parentApp() { - if (typeof this._parentApp === "undefined") { - if ( - typeof this.ancestor !== "undefined" && - typeof this.ancestor.getRootAncestor === "function" - ) { - // the `app` property is only defined in the world "root" container - this._parentApp = this.ancestor.getRootAncestor().app; - } + if (!this._parentApp && this.ancestor) { + // the `app` property is only defined in the world "root" container + this._parentApp = this.ancestor.getRootAncestor().app; } return this._parentApp; } @@ -367,8 +362,7 @@ export default class Renderable extends Rect { get isFloating() { return ( this.floating === true || - (typeof this.ancestor !== "undefined" && - this.ancestor.isFloating === true) + (this.ancestor && this.ancestor.isFloating === true) ); } @@ -707,11 +701,7 @@ export default class Renderable extends Rect { } // TODO: cache the absolute position and invalidate when pos or ancestor changes this._absPos.set(this.pos.x, this.pos.y); - if ( - typeof this.ancestor !== "undefined" && - typeof this.ancestor.getAbsolutePosition === "function" && - this.floating !== true - ) { + if (this.ancestor && this.floating !== true) { this._absPos.add(this.ancestor.getAbsolutePosition()); } return this._absPos; @@ -745,14 +735,14 @@ export default class Renderable extends Rect { } // apply stencil mask if defined - if (typeof this.mask !== "undefined") { + if (this.mask) { renderer.translate(this.pos.x, this.pos.y); renderer.setMask(this.mask); renderer.translate(-this.pos.x, -this.pos.y); } // use this renderable shader if defined - if (this.shader && typeof renderer.gl !== "undefined") { + if (this.shader) { renderer.customShader = this.shader; } @@ -804,12 +794,12 @@ export default class Renderable extends Rect { renderer.clearTint(); // clear the mask if set - if (typeof this.mask !== "undefined") { + if (this.mask) { renderer.clearMask(); } // revert to the default shader if defined - if (this.shader && typeof renderer.gl !== "undefined") { + if (this.shader) { renderer.customShader = undefined; } @@ -857,24 +847,24 @@ export default class Renderable extends Rect { this.pos = undefined; - if (typeof this._absPos !== "undefined") { + if (this._absPos) { vector2dPool.release(this._absPos); this._absPos = undefined; } - if (this._bounds instanceof Bounds) { + if (this._bounds) { boundsPool.release(this._bounds); this._bounds = undefined; } this.onVisibilityChange = undefined; - if (typeof this.mask !== "undefined") { + if (this.mask) { pool.push(this.mask); this.mask = undefined; } - if (this._tint instanceof Color) { + if (this._tint) { colorPool.release(this._tint); this._tint = undefined; } diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 9fd91b8d7..baa4cdce2 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -67,19 +67,20 @@ export default class Renderer { */ this.isContextValid = true; - /** - * the default method to sort object ("sorting", "z-buffer") - * @type {string} - * @default "sorting" - */ - this.depthTest = "sorting"; - /** * The GPU renderer string (WebGL only, undefined for Canvas) * @type {string|undefined} */ this.GPURenderer = undefined; + /** + * an optional custom shader to use instead of the default one. + * Set by a renderable's preDraw when a shader is assigned. + * (WebGL only, ignored by Canvas renderer) + * @type {GLShader|ShaderEffect|undefined} + */ + this.customShader = undefined; + /** * The Path2D instance used by the renderer to draw primitives * @type {Path2D} diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 6643d4d2a..8547f5a27 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -151,19 +151,11 @@ export default class WebGLRenderer extends Renderer { this.addBatcher(new (CustomBatcher || PrimitiveBatcher)(this), "primitive"); this.addBatcher(new MeshBatcher(this), "mesh"); - // depth Test settings - this.depthTest = options.depthTest; - // default WebGL state(s) - if (this.depthTest === "z-buffer") { - this.gl.enable(this.gl.DEPTH_TEST); - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/depthFunc - this.gl.depthFunc(this.gl.LEQUAL); - this.gl.depthMask(true); - } else { - this.gl.disable(this.gl.DEPTH_TEST); - this.gl.depthMask(false); - } + // depth testing disabled for 2D (painter's algorithm handles z-ordering). + // drawMesh() enables it temporarily for 3D mesh rendering. + this.gl.disable(this.gl.DEPTH_TEST); + this.gl.depthMask(false); this.gl.disable(this.gl.SCISSOR_TEST); this.gl.enable(this.gl.BLEND); @@ -180,8 +172,7 @@ export default class WebGLRenderer extends Renderer { ); } - // an optional custom shader set by a renderable's preDraw - this.customShader = undefined; + // customShader is declared on the base Renderer class // Create a texture cache this.cache = new TextureCache(this, this.maxTextures); @@ -485,8 +476,8 @@ export default class WebGLRenderer extends Renderer { const clearColor = this.backgroundColor.toArray(); gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); this.lineWidth = 1; - // always clear depth + color + stencil (depth buffer is always available) - gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + // clear color + stencil (depth buffer is only used by drawMesh, which clears it locally) + gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); } /** @@ -649,9 +640,7 @@ export default class WebGLRenderer extends Renderer { this.currentBatcher.useShader(this.customShader); } - // save current depth state and force depth testing for 3D mesh - const depthWasEnabled = this.depthTest === "z-buffer"; - + // enable depth testing for 3D mesh rendering gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); gl.depthMask(true); @@ -682,11 +671,8 @@ export default class WebGLRenderer extends Renderer { // restore blending and depth state gl.enable(gl.BLEND); - - if (!depthWasEnabled) { - gl.disable(gl.DEPTH_TEST); - gl.depthMask(false); - } + gl.disable(gl.DEPTH_TEST); + gl.depthMask(false); // revert to default shader if custom was applied if (typeof this.customShader === "object") { diff --git a/packages/melonjs/tests/container.spec.js b/packages/melonjs/tests/container.spec.js index a8059c267..5284a4a48 100644 --- a/packages/melonjs/tests/container.spec.js +++ b/packages/melonjs/tests/container.spec.js @@ -654,6 +654,67 @@ describe("Container", () => { }); }); + describe("sort comparators", () => { + it("_sortZ should sort descending by z", () => { + const a = new Renderable(0, 0, 1, 1); + const b = new Renderable(0, 0, 1, 1); + a.pos.z = 5; + b.pos.z = 10; + expect(container._sortZ(a, b)).toBeGreaterThan(0); // b first + expect(container._sortZ(b, a)).toBeLessThan(0); // b still first + }); + + it("_sortZ should return 0 for equal z", () => { + const a = new Renderable(0, 0, 1, 1); + const b = new Renderable(0, 0, 1, 1); + a.pos.z = 5; + b.pos.z = 5; + expect(container._sortZ(a, b)).toBe(0); + }); + + it("_sortReverseZ should sort ascending by z", () => { + const a = new Renderable(0, 0, 1, 1); + const b = new Renderable(0, 0, 1, 1); + a.pos.z = 5; + b.pos.z = 10; + expect(container._sortReverseZ(a, b)).toBeLessThan(0); // a first + expect(container._sortReverseZ(b, a)).toBeGreaterThan(0); // a still first + }); + + it("_sortX should sort by z first, then by x", () => { + const a = new Renderable(100, 0, 1, 1); + const b = new Renderable(200, 0, 1, 1); + a.pos.z = 5; + b.pos.z = 5; + // same z, should sort by x + expect(container._sortX(a, b)).toBeGreaterThan(0); // b.x > a.x + // different z, x ignored + b.pos.z = 10; + expect(container._sortX(a, b)).toBeGreaterThan(0); // b.z > a.z + }); + + it("_sortY should sort by z first, then by y", () => { + const a = new Renderable(0, 100, 1, 1); + const b = new Renderable(0, 200, 1, 1); + a.pos.z = 5; + b.pos.z = 5; + // same z, should sort by y + expect(container._sortY(a, b)).toBeGreaterThan(0); // b.y > a.y + // different z, y ignored + b.pos.z = 10; + expect(container._sortY(a, b)).toBeGreaterThan(0); // b.z > a.z + }); + + it("sortOn setter should update the cached comparator", () => { + container.sortOn = "y"; + expect(container._comparator).toBe(container._sortY); + container.sortOn = "x"; + expect(container._comparator).toBe(container._sortX); + container.sortOn = "z"; + expect(container._comparator).toBe(container._sortZ); + }); + }); + describe("enableChildBoundsUpdate", () => { it("child bounds should reflect absolute position after being added", () => { container.enableChildBoundsUpdate = true;