diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index d2ce909d3..1f3375840 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -1,9 +1,9 @@ import { MathUtil, Matrix, Matrix3x3, Quaternion, Vector3 } from "@galacean/engine-math"; import { BoolUpdateFlag } from "./BoolUpdateFlag"; -import { deepClone, ignoreClone } from "./clone/CloneManager"; import { Component } from "./Component"; import { Entity } from "./Entity"; import { UpdateFlagManager } from "./UpdateFlagManager"; +import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; /** * Used to implement transformation related functions. @@ -27,12 +27,16 @@ export class Transform extends Component { private _rotationQuaternion: Quaternion = new Quaternion(); @deepClone private _scale: Vector3 = new Vector3(1, 1, 1); + @assignmentClone + private _localUniformScaling: boolean = true; @deepClone private _worldPosition: Vector3 = new Vector3(); @deepClone private _worldRotation: Vector3 = new Vector3(); @deepClone private _worldRotationQuaternion: Quaternion = new Quaternion(); + @assignmentClone + private _worldUniformScaling: boolean = true; @deepClone private _lossyWorldScale: Vector3 = new Vector3(1, 1, 1); @deepClone @@ -225,12 +229,13 @@ export class Transform extends Component { /** * Local lossy scaling. - * @remarks The value obtained may not be correct under certain conditions(for example, the parent node has scaling, - * and the child node has a rotation), the scaling will be tilted. Vector3 cannot be used to correctly represent the scaling. Must use Matrix3x3. + * @remarks The value obtained may not be correct under certain conditions(for example, the parent node has non-uniform world scaling, + * and the child node has a rotation), the scaling will be tilted. */ get lossyWorldScale(): Vector3 { if (this._isContainDirtyFlag(TransformModifyFlags.WorldScale)) { if (this._getParentTransform()) { + // Vector3 cannot be used to correctly represent the scaling. Must use Matrix3x3 const scaleMat = this._getScaleMatrix(); const e = scaleMat.elements; this._lossyWorldScale.set(e[0], e[4], e[8]); @@ -258,20 +263,26 @@ export class Transform extends Component { if (this._localMatrix !== value) { this._localMatrix.copyFrom(value); } - + const { _position: position, _rotationQuaternion: rotationQuaternion, _scale: scale } = this; // @ts-ignore - this._position._onValueChanged = this._rotationQuaternion._onValueChanged = this._scale._onValueChanged = null; - this._localMatrix.decompose(this._position, this._rotationQuaternion, this._scale); + position._onValueChanged = rotationQuaternion._onValueChanged = scale._onValueChanged = null; + this._localMatrix.decompose(position, rotationQuaternion, scale); // @ts-ignore - this._position._onValueChanged = this._onPositionChanged; + position._onValueChanged = this._onPositionChanged; // @ts-ignore - this._rotationQuaternion._onValueChanged = this._onRotationQuaternionChanged; + rotationQuaternion._onValueChanged = this._onRotationQuaternionChanged; // @ts-ignore - this._scale._onValueChanged = this._onScaleChanged; + scale._onValueChanged = this._onScaleChanged; this._setDirtyFlagTrue(TransformModifyFlags.LocalEuler); this._setDirtyFlagFalse(TransformModifyFlags.LocalMatrix | TransformModifyFlags.LocalQuat); - this._updateAllWorldFlag(); + const localUniformScaling = scale.x === scale.y && scale.y === scale.z; + if (this._localUniformScaling !== localUniformScaling) { + this._localUniformScaling = localUniformScaling; + this._updateAllWorldFlag(TransformModifyFlags.WmWpWeWqWsWus); + } else { + this._updateAllWorldFlag(TransformModifyFlags.WmWpWeWqWs); + } } /** @@ -563,7 +574,7 @@ export class Transform extends Component { */ _parentChange(): void { this._isParentDirty = true; - this._updateAllWorldFlag(); + this._updateAllWorldFlag(TransformModifyFlags.WmWpWeWqWsWus); } /** @@ -603,9 +614,9 @@ export class Transform extends Component { private _updateWorldPositionFlag(): void { if (!this._isContainDirtyFlags(TransformModifyFlags.WmWp)) { this._worldAssociatedChange(TransformModifyFlags.WmWp); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateWorldPositionFlag(); + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateWorldPositionFlag(); } } } @@ -615,14 +626,19 @@ export class Transform extends Component { * Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities. * Get worldRotationQuaternion: Will trigger the world rotation (in quaternion) update of itself and all parent entities. * Get worldRotation: Will trigger the world rotation(in euler and quaternion) update of itself and world rotation(in quaternion) update of all parent entities. + * Get worldScale: Will trigger the scaling update of itself and all parent entities. * In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix or worldRotationQuaternion) to be false. */ private _updateWorldRotationFlag() { - if (!this._isContainDirtyFlags(TransformModifyFlags.WmWeWq)) { - this._worldAssociatedChange(TransformModifyFlags.WmWeWq); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateWorldPositionAndRotationFlag(); // Rotation update of parent entity will trigger world position and rotation update of all child entity. + const parent = this._getParentTransform(); + const parentWorldUniformScaling = parent ? parent._getWorldUniformScaling() : true; + let flags = parentWorldUniformScaling ? TransformModifyFlags.WmWeWq : TransformModifyFlags.WmWeWqWs; + if (!this._isContainDirtyFlags(flags)) { + this._worldAssociatedChange(flags); + flags = this._getWorldUniformScaling() ? TransformModifyFlags.WmWpWeWq : TransformModifyFlags.WmWpWeWqWs; + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateWorldPositionAndRotationFlag(flags); // Rotation update of parent entity will trigger world position, rotation and scale update of all child entity. } } } @@ -632,14 +648,17 @@ export class Transform extends Component { * Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities. * Get worldRotationQuaternion: Will trigger the world rotation (in quaternion) update of itself and all parent entities. * Get worldRotation: Will trigger the world rotation(in euler and quaternion) update of itself and world rotation(in quaternion) update of all parent entities. + * Get worldScale: Will trigger the scaling update of itself and all parent entities. * In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix or worldRotationQuaternion) to be false. + * @param flags - Dirty flag */ - private _updateWorldPositionAndRotationFlag() { - if (!this._isContainDirtyFlags(TransformModifyFlags.WmWpWeWq)) { - this._worldAssociatedChange(TransformModifyFlags.WmWpWeWq); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateWorldPositionAndRotationFlag(); + private _updateWorldPositionAndRotationFlag(flags: TransformModifyFlags): void { + if (!this._isContainDirtyFlags(flags)) { + this._worldAssociatedChange(flags); + flags = this._getWorldUniformScaling() ? TransformModifyFlags.WmWpWeWq : TransformModifyFlags.WmWpWeWqWs; + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateWorldPositionAndRotationFlag(flags); } } } @@ -649,13 +668,15 @@ export class Transform extends Component { * Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities. * Get worldScale: Will trigger the scaling update of itself and all parent entities. * In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix) to be false. + * @param flags - Dirty flag */ - private _updateWorldScaleFlag() { - if (!this._isContainDirtyFlags(TransformModifyFlags.WmWs)) { - this._worldAssociatedChange(TransformModifyFlags.WmWs); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateWorldPositionAndScaleFlag(); + private _updateWorldScaleFlag(flags: TransformModifyFlags): void { + if (!this._isContainDirtyFlags(flags)) { + this._worldAssociatedChange(flags); + flags |= TransformModifyFlags.WorldPosition; + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateWorldPositionAndScaleFlag(flags); } } } @@ -665,26 +686,28 @@ export class Transform extends Component { * Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities. * Get worldScale: Will trigger the scaling update of itself and all parent entities. * In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix) to be false. + * @param flags - Dirty flag */ - private _updateWorldPositionAndScaleFlag(): void { - if (!this._isContainDirtyFlags(TransformModifyFlags.WmWpWs)) { - this._worldAssociatedChange(TransformModifyFlags.WmWpWs); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateWorldPositionAndScaleFlag(); + private _updateWorldPositionAndScaleFlag(flags: TransformModifyFlags): void { + if (!this._isContainDirtyFlags(flags)) { + this._worldAssociatedChange(flags); + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateWorldPositionAndScaleFlag(flags); } } } /** * Update all world transform property dirty flag, the principle is the same as above. + * @param flags - Dirty flag */ - private _updateAllWorldFlag(): void { - if (!this._isContainDirtyFlags(TransformModifyFlags.WmWpWeWqWs)) { - this._worldAssociatedChange(TransformModifyFlags.WmWpWeWqWs); - const nodeChildren = this._entity._children; - for (let i: number = 0, n: number = nodeChildren.length; i < n; i++) { - nodeChildren[i].transform?._updateAllWorldFlag(); + private _updateAllWorldFlag(flags: TransformModifyFlags): void { + if (!this._isContainDirtyFlags(flags)) { + this._worldAssociatedChange(flags); + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i].transform?._updateAllWorldFlag(flags); } } } @@ -739,7 +762,7 @@ export class Transform extends Component { private _worldAssociatedChange(type: number): void { this._dirtyFlag |= type; - this._updateFlagManager.dispatch(TransformModifyFlags.WorldMatrix); + this._updateFlagManager.dispatch(type); } private _rotateByQuat(rotateQuat: Quaternion, relativeToLocal: boolean): void { @@ -828,8 +851,29 @@ export class Transform extends Component { @ignoreClone private _onScaleChanged(): void { + const { x, y, z } = this._scale; this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix); - this._updateWorldScaleFlag(); + const localUniformScaling = x == y && y == z; + if (this._localUniformScaling !== localUniformScaling) { + this._localUniformScaling = localUniformScaling; + this._updateWorldScaleFlag(TransformModifyFlags.WmWsWus); + } else { + this._updateWorldScaleFlag(TransformModifyFlags.WmWs); + } + } + + private _getWorldUniformScaling(): boolean { + if (this._isContainDirtyFlag(TransformModifyFlags.IsWorldUniformScaling)) { + const localUniformScaling = this._localUniformScaling; + if (localUniformScaling) { + const parent = this._getParentTransform(); + this._worldUniformScaling = localUniformScaling && (parent ? parent._getWorldUniformScaling() : true); + } else { + this._worldUniformScaling = false; + } + this._setDirtyFlagFalse(TransformModifyFlags.IsWorldUniformScaling); + } + return this._worldUniformScaling; } } @@ -846,16 +890,27 @@ export enum TransformModifyFlags { LocalMatrix = 0x40, WorldMatrix = 0x80, + /** This is an internal flag used to assist in determining the dispatch + * of world scaling dirty flags in the case of non-uniform scaling. + */ + IsWorldUniformScaling = 0x100, + /** WorldMatrix | WorldPosition */ WmWp = 0x84, /** WorldMatrix | WorldEuler | WorldQuat */ WmWeWq = 0x98, + /** WorldMatrix | WorldEuler | WorldQuat | WorldScale*/ + WmWeWqWs = 0xb8, /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat */ WmWpWeWq = 0x9c, /** WorldMatrix | WorldScale */ WmWs = 0xa0, + /** WorldMatrix | WorldScale | WorldUniformScaling */ + WmWsWus = 0x1a0, /** WorldMatrix | WorldPosition | WorldScale */ WmWpWs = 0xa4, /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale */ - WmWpWeWqWs = 0xbc + WmWpWeWqWs = 0xbc, + /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling */ + WmWpWeWqWsWus = 0x1bc } diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index 5a0eb9a73..1e31bcc05 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -26,6 +26,18 @@ describe("Transform test", function () { expect(transform.worldUp).to.deep.equal(new Vector3(0, 1, 0)); }); + it("World Scale", () => { + const root = scene.createRootEntity(); + root.transform.setScale(1, 2, 3); + const entity = root.createChild(); + const transform = entity.transform; + transform.setScale(4, 5, 6); + transform.setRotation(0, 0, 0); + expect(transform.lossyWorldScale).to.deep.equal(new Vector3(4, 10, 18)); + transform.setRotation(90, 0, 0); + expect(transform.lossyWorldScale).to.deep.equal(new Vector3(4, 15, 12)); + }); + it("Parent Dirty", () => { const root1 = scene.createRootEntity(); const root2 = scene.createRootEntity();