diff --git a/examples/package-lock.json b/examples/package-lock.json index 5d2ad205b92..5487e1eea1c 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -43,7 +43,7 @@ }, "..": { "name": "playcanvas", - "version": "2.8.0-dev.0", + "version": "2.9.0-beta.1", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/core/math/mat4.js b/src/core/math/mat4.js index 061106abc36..b46c352930c 100644 --- a/src/core/math/mat4.js +++ b/src/core/math/mat4.js @@ -360,6 +360,74 @@ class Mat4 { return this; } + /** + * Multiplies two affine transformation matrices that are known to have unit scale. + * This is an optimized version of mulAffine2 for transforms containing only rotation + * and translation (no scale). Both input matrices must have unit scale for correct results. + * + * @param {Mat4} lhs - The 4x4 matrix used as the first multiplicand of the operation (parent transform). + * @param {Mat4} rhs - The 4x4 matrix used as the second multiplicand of the operation (child transform). + * @returns {Mat4} Self for chaining. + * @private + */ + mulAffine2NoScale(lhs, rhs) { + const a = lhs.data; + const b = rhs.data; + const r = this.data; + + const a00 = a[0]; + const a01 = a[1]; + const a02 = a[2]; + const a10 = a[4]; + const a11 = a[5]; + const a12 = a[6]; + const a20 = a[8]; + const a21 = a[9]; + const a22 = a[10]; + const a30 = a[12]; + const a31 = a[13]; + const a32 = a[14]; + + // For unit scale transforms, the 3x3 rotation matrix multiplication + // is the same as mulAffine2, but we can be confident that no scale + // is being compounded through the hierarchy + let b0, b1, b2; + + b0 = b[0]; + b1 = b[1]; + b2 = b[2]; + r[0] = a00 * b0 + a10 * b1 + a20 * b2; + r[1] = a01 * b0 + a11 * b1 + a21 * b2; + r[2] = a02 * b0 + a12 * b1 + a22 * b2; + r[3] = 0; + + b0 = b[4]; + b1 = b[5]; + b2 = b[6]; + r[4] = a00 * b0 + a10 * b1 + a20 * b2; + r[5] = a01 * b0 + a11 * b1 + a21 * b2; + r[6] = a02 * b0 + a12 * b1 + a22 * b2; + r[7] = 0; + + b0 = b[8]; + b1 = b[9]; + b2 = b[10]; + r[8] = a00 * b0 + a10 * b1 + a20 * b2; + r[9] = a01 * b0 + a11 * b1 + a21 * b2; + r[10] = a02 * b0 + a12 * b1 + a22 * b2; + r[11] = 0; + + b0 = b[12]; + b1 = b[13]; + b2 = b[14]; + r[12] = a00 * b0 + a10 * b1 + a20 * b2 + a30; + r[13] = a01 * b0 + a11 * b1 + a21 * b2 + a31; + r[14] = a02 * b0 + a12 * b1 + a22 * b2 + a32; + r[15] = 1; + + return this; + } + /** * Multiplies the current instance by the specified 4x4 matrix. * @@ -965,10 +1033,6 @@ class Mat4 { const qz = r.z; const qw = r.w; - const sx = s.x; - const sy = s.y; - const sz = s.z; - const x2 = qx + qx; const y2 = qy + qy; const z2 = qz + qz; @@ -984,20 +1048,42 @@ class Mat4 { const m = this.data; - m[0] = (1 - (yy + zz)) * sx; - m[1] = (xy + wz) * sx; - m[2] = (xz - wy) * sx; - m[3] = 0; - - m[4] = (xy - wz) * sy; - m[5] = (1 - (xx + zz)) * sy; - m[6] = (yz + wx) * sy; - m[7] = 0; - - m[8] = (xz + wy) * sz; - m[9] = (yz - wx) * sz; - m[10] = (1 - (xx + yy)) * sz; - m[11] = 0; + // Fast path for unit scale - skip 9 scale multiplications + if (s.x === 1 && s.y === 1 && s.z === 1) { + m[0] = 1 - (yy + zz); + m[1] = xy + wz; + m[2] = xz - wy; + m[3] = 0; + + m[4] = xy - wz; + m[5] = 1 - (xx + zz); + m[6] = yz + wx; + m[7] = 0; + + m[8] = xz + wy; + m[9] = yz - wx; + m[10] = 1 - (xx + yy); + m[11] = 0; + } else { + const sx = s.x; + const sy = s.y; + const sz = s.z; + + m[0] = (1 - (yy + zz)) * sx; + m[1] = (xy + wz) * sx; + m[2] = (xz - wy) * sx; + m[3] = 0; + + m[4] = (xy - wz) * sy; + m[5] = (1 - (xx + zz)) * sy; + m[6] = (yz + wx) * sy; + m[7] = 0; + + m[8] = (xz + wy) * sz; + m[9] = (yz - wx) * sz; + m[10] = (1 - (xx + yy)) * sz; + m[11] = 0; + } m[12] = t.x; m[13] = t.y; diff --git a/src/scene/graph-node.js b/src/scene/graph-node.js index d633b3d86ac..dffe6905a23 100644 --- a/src/scene/graph-node.js +++ b/src/scene/graph-node.js @@ -227,6 +227,24 @@ class GraphNode extends EventHandler { */ _worldScaleSign = 0; + /** + * Tracks whether this node's localScale is exactly (1, 1, 1). Used to optimize transform + * calculations by skipping scale multiplications. + * + * @type {boolean} + * @private + */ + _hasUnitScale = true; + + /** + * Tracks whether this node and all of its ancestors up to the root have unit scale. + * Enables significant optimizations in hierarchy transformation calculations. + * + * @type {boolean} + * @private + */ + _ancestorsHaveUnitScale = true; + /** * @type {Mat3} * @private @@ -1137,6 +1155,15 @@ class GraphNode extends EventHandler { this.localScale.set(x, y, z); } + // Update unit scale flag + const hadUnitScale = this._hasUnitScale; + this._hasUnitScale = (this.localScale.x === 1 && this.localScale.y === 1 && this.localScale.z === 1); + + // If unit scale status changed, propagate to children + if (hadUnitScale !== this._hasUnitScale) { + this._updateAncestorsHaveUnitScale(); + } + if (!this._dirtyLocal) { this._dirtifyLocal(); } @@ -1185,6 +1212,21 @@ class GraphNode extends EventHandler { this._aabbVer++; } + /** @private */ + _updateAncestorsHaveUnitScale() { + // Recalculate whether this node and all ancestors have unit scale + const newValue = this._hasUnitScale && (!this._parent || this._parent._ancestorsHaveUnitScale); + + if (this._ancestorsHaveUnitScale !== newValue) { + this._ancestorsHaveUnitScale = newValue; + + // Propagate change to all children + for (let i = 0; i < this._children.length; i++) { + this._children[i]._updateAncestorsHaveUnitScale(); + } + } + } + /** * Sets the world space position of the specified graph node. * @@ -1457,6 +1499,9 @@ class GraphNode extends EventHandler { // The graph depth of the child and all of its descendants will now change node._updateGraphDepth(); + // Update unit scale flags for the child and its descendants + node._updateAncestorsHaveUnitScale(); + // The child (plus subhierarchy) will need world transforms to be recalculated node._dirtifyWorld(); // node might be already marked as dirty, in that case the whole chain stays frozen, so let's enforce unfreeze @@ -1504,6 +1549,9 @@ class GraphNode extends EventHandler { // Clear parent child._parent = null; + // Update unit scale flags for the removed child and its descendants + child._updateAncestorsHaveUnitScale(); + // NOTE: see PR #4047 - this fix is removed for now as it breaks other things // notify the child hierarchy it has been removed from the parent, // which marks them as not enabled in hierarchy @@ -1569,7 +1617,12 @@ class GraphNode extends EventHandler { this.worldTransform.setTRS(scaleCompensatePos, scaleCompensateRot, scale); } else { - this.worldTransform.mulAffine2(this._parent.worldTransform, this.localTransform); + // Use optimized matrix multiplication for unit scale chains + if (this._hasUnitScale && this._parent._ancestorsHaveUnitScale) { + this.worldTransform.mulAffine2NoScale(this._parent.worldTransform, this.localTransform); + } else { + this.worldTransform.mulAffine2(this._parent.worldTransform, this.localTransform); + } } }