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;