From 7c5d468ad87e60c3b55541b3d70f9b09b56d30fb Mon Sep 17 00:00:00 2001 From: hhhhkrx Date: Thu, 16 Apr 2026 17:38:24 +0800 Subject: [PATCH 1/6] feat(particle): add transform to shape module (#2965) * feat(particle): add position and rotation transform to shape module --- e2e/case/particleRenderer-shape-transform.ts | 89 ++++++ e2e/config.ts | 6 + ...ticle_particleRenderer-shape-transform.jpg | 3 + .../core/src/particle/ParticleGenerator.ts | 2 +- .../src/particle/modules/shape/BaseShape.ts | 157 +++++++++- .../src/particle/modules/shape/BoxShape.ts | 10 +- .../src/particle/modules/shape/CircleShape.ts | 12 +- .../src/particle/modules/shape/ConeShape.ts | 19 +- .../particle/modules/shape/HemisphereShape.ts | 6 +- .../src/particle/modules/shape/MeshShape.ts | 6 +- .../src/particle/modules/shape/SphereShape.ts | 6 +- packages/math/src/BoundingBox.ts | 51 ++-- .../core/particle/ParticleBoundingBox.test.ts | 67 +++++ .../particle/ParticleShapeTransform.test.ts | 270 ++++++++++++++++++ tests/src/math/BoundingBox.test.ts | 5 +- 15 files changed, 650 insertions(+), 59 deletions(-) create mode 100644 e2e/case/particleRenderer-shape-transform.ts create mode 100644 e2e/fixtures/originImage/Particle_particleRenderer-shape-transform.jpg create mode 100644 tests/src/core/particle/ParticleShapeTransform.test.ts diff --git a/e2e/case/particleRenderer-shape-transform.ts b/e2e/case/particleRenderer-shape-transform.ts new file mode 100644 index 000000000..3fc0bf00d --- /dev/null +++ b/e2e/case/particleRenderer-shape-transform.ts @@ -0,0 +1,89 @@ +/** + * @title Particle Shape Transform + * @category Particle + */ +import { + Camera, + Color, + ConeShape, + BoxShape, + Engine, + Entity, + Logger, + ParticleMaterial, + ParticleRenderer, + ParticleSimulationSpace, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 30); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + camera.nearClipPlane = 0.3; + camera.farClipPlane = 1000; + + // Cone with shape position offset + createParticle(rootEntity, engine, -6, () => { + const shape = new ConeShape(); + shape.position.set(0, 3, 0); + return shape; + }); + + // Cone with shape rotation + createParticle(rootEntity, engine, -2, () => { + const shape = new ConeShape(); + shape.rotation.set(0, 0, 90); + return shape; + }); + + // Box with shape scale + createParticle(rootEntity, engine, 2, () => { + const shape = new BoxShape(); + shape.scale.set(3, 1, 1); + return shape; + }); + + // Cone with combined transform + createParticle(rootEntity, engine, 6, () => { + const shape = new ConeShape(); + shape.position.set(0, 2, 0); + shape.rotation.set(0, 0, 45); + shape.scale.set(2, 1, 1); + return shape; + }); + + updateForE2E(engine, 500); + initScreenshot(engine, camera); +}); + +function createParticle(rootEntity: Entity, engine: Engine, xPos: number, createShape: () => any): void { + const particleEntity = rootEntity.createChild("Particle"); + particleEntity.transform.position.set(xPos, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + particleRenderer.setMaterial(material); + + const generator = particleRenderer.generator; + generator.useAutoRandomSeed = false; + + const { main, emission } = generator; + main.startSpeed.constant = 3; + main.startSize.constant = 0.15; + main.simulationSpace = ParticleSimulationSpace.Local; + + emission.shape = createShape(); +} diff --git a/e2e/config.ts b/e2e/config.ts index 05c61ad75..5d3f223b4 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -436,6 +436,12 @@ export const E2E_CONFIG = { caseFileName: "particleRenderer-noise", threshold: 0, diffPercentage: 0 + }, + shapeTransform: { + category: "Particle", + caseFileName: "particleRenderer-shape-transform", + threshold: 0, + diffPercentage: 0.334 } }, PostProcess: { diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-shape-transform.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-shape-transform.jpg new file mode 100644 index 000000000..6fb96ec8f --- /dev/null +++ b/e2e/fixtures/originImage/Particle_particleRenderer-shape-transform.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7a4bc8c57d966ad7ca49f6ea4f7177bb5d4e9c4c666f3c92ecc600c08b28f5 +size 23645 diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index ba478cd21..3c62ef385 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -1228,7 +1228,7 @@ export class ParticleGenerator { // StartSpeed's impact const { shape } = this.emission; if (shape?.enabled) { - shape._getPositionRange(min, max); + shape._getPositionRange(bounds); shape._getDirectionRange(directionMin, directionMax); } else { min.set(0, 0, 0); diff --git a/packages/core/src/particle/modules/shape/BaseShape.ts b/packages/core/src/particle/modules/shape/BaseShape.ts index 87347b3e0..9e00891b5 100644 --- a/packages/core/src/particle/modules/shape/BaseShape.ts +++ b/packages/core/src/particle/modules/shape/BaseShape.ts @@ -1,12 +1,21 @@ -import { Rand, Vector3 } from "@galacean/engine-math"; +import { BoundingBox, MathUtil, Matrix, Quaternion, Rand, Vector2, Vector3 } from "@galacean/engine-math"; import { ParticleShapeType } from "./enums/ParticleShapeType"; import { UpdateFlagManager } from "../../../UpdateFlagManager"; -import { ignoreClone } from "../../../clone/CloneManager"; +import { deepClone, ignoreClone } from "../../../clone/CloneManager"; /** * Base class for all particle shapes. */ export abstract class BaseShape { + /** @internal */ + static _tempVector20 = new Vector2(); + /** @internal */ + static _tempVector21 = new Vector2(); + /** @internal */ + static _tempVector30 = new Vector3(); + /** @internal */ + static _tempVector31 = new Vector3(); + private static _tempQuaternion = new Quaternion(); /** The type of shape to emit particles from. */ abstract readonly shapeType: ParticleShapeType; @@ -16,6 +25,19 @@ export abstract class BaseShape { private _enabled = true; private _randomDirectionAmount = 0; + @deepClone + private _position = new Vector3(0, 0, 0); + @deepClone + private _rotation = new Vector3(0, 0, 0); + @deepClone + private _scale = new Vector3(1, 1, 1); + @ignoreClone + private _matrix = new Matrix(); + @ignoreClone + private _transformDirty = false; + @ignoreClone + private _hasShapeTransform = false; + /** * Specifies whether the ShapeModule is enabled or disabled. */ @@ -44,6 +66,54 @@ export abstract class BaseShape { } } + /** + * Apply a local position offset to the shape. + */ + get position(): Vector3 { + return this._position; + } + + set position(value: Vector3) { + if (value !== this._position) { + this._position.copyFrom(value); + } + } + + /** + * Apply a local rotation to the shape, specified as euler angles in degrees. + */ + get rotation(): Vector3 { + return this._rotation; + } + + set rotation(value: Vector3) { + if (value !== this._rotation) { + this._rotation.copyFrom(value); + } + } + + /** + * Apply a local scale to the shape. + */ + get scale(): Vector3 { + return this._scale; + } + + set scale(value: Vector3) { + if (value !== this._scale) { + this._scale.copyFrom(value); + } + } + + constructor() { + // @ts-ignore + this._position._onValueChanged = this._onTransformChanged; + // @ts-ignore + this._rotation._onValueChanged = this._onTransformChanged; + // @ts-ignore + this._scale._onValueChanged = this._onTransformChanged; + } + /** * @internal */ @@ -61,15 +131,92 @@ export abstract class BaseShape { /** * @internal */ - abstract _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void; + _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + this._generateLocalPositionAndDirection(rand, emitTime, position, direction); + if (this._hasShapeTransform) { + const matrix = this._getMatrix(); + Vector3.transformToVec3(position, matrix, position); + Vector3.transformNormal(direction, matrix, direction); + direction.normalize(); + } + } /** * @internal */ - abstract _getDirectionRange(outMin: Vector3, outMax: Vector3): void; + _getPositionRange(bounds: BoundingBox): void { + this._getLocalPositionRange(bounds.min, bounds.max); + if (this._hasShapeTransform) { + BoundingBox.transform(bounds, this._getMatrix(), bounds); + } + } /** * @internal */ - abstract _getPositionRange(outMin: Vector3, outMax: Vector3): void; + _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + this._getLocalDirectionRange(outMin, outMax); + if (this._hasShapeTransform) { + this._transformDirectionRange(outMin, outMax); + } + } + + protected abstract _generateLocalPositionAndDirection( + rand: Rand, + emitTime: number, + position: Vector3, + direction: Vector3 + ): void; + + protected abstract _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void; + + protected abstract _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void; + + @ignoreClone + protected _onTransformChanged = (): void => { + this._transformDirty = true; + const { _position: p, _rotation: r, _scale: s } = this; + this._hasShapeTransform = + p.x !== 0 || p.y !== 0 || p.z !== 0 || r.x !== 0 || r.y !== 0 || r.z !== 0 || s.x !== 1 || s.y !== 1 || s.z !== 1; + this._updateManager.dispatch(); + }; + + private _getMatrix(): Matrix { + if (this._transformDirty) { + const { _rotation: r } = this; + const q = BaseShape._tempQuaternion; + Quaternion.rotationEuler( + MathUtil.degreeToRadian(r.x), + MathUtil.degreeToRadian(r.y), + MathUtil.degreeToRadian(r.z), + q + ); + Matrix.affineTransformation(this._scale, q, this._position, this._matrix); + this._transformDirty = false; + } + return this._matrix; + } + + // Arvo min/max method without translation, only apply RS part of the matrix + private _transformDirectionRange(outMin: Vector3, outMax: Vector3): void { + const e = this._getMatrix().elements; + const { x: minX, y: minY, z: minZ } = outMin; + const { x: maxX, y: maxY, z: maxZ } = outMax; + // prettier-ignore + const e0 = e[0], e1 = e[1], e2 = e[2], + e4 = e[4], e5 = e[5], e6 = e[6], + e8 = e[8], e9 = e[9], e10 = e[10]; + + outMin.set( + (e0 > 0 ? e0 * minX : e0 * maxX) + (e4 > 0 ? e4 * minY : e4 * maxY) + (e8 > 0 ? e8 * minZ : e8 * maxZ), + (e1 > 0 ? e1 * minX : e1 * maxX) + (e5 > 0 ? e5 * minY : e5 * maxY) + (e9 > 0 ? e9 * minZ : e9 * maxZ), + (e2 > 0 ? e2 * minX : e2 * maxX) + (e6 > 0 ? e6 * minY : e6 * maxY) + (e10 > 0 ? e10 * minZ : e10 * maxZ) + ); + + outMax.set( + (e0 > 0 ? e0 * maxX : e0 * minX) + (e4 > 0 ? e4 * maxY : e4 * minY) + (e8 > 0 ? e8 * maxZ : e8 * minZ), + (e1 > 0 ? e1 * maxX : e1 * minX) + (e5 > 0 ? e5 * maxY : e5 * minY) + (e9 > 0 ? e9 * maxZ : e9 * minZ), + (e2 > 0 ? e2 * maxX : e2 * minX) + (e6 > 0 ? e6 * maxY : e6 * minY) + (e10 > 0 ? e10 * maxZ : e10 * minZ) + ); + } } diff --git a/packages/core/src/particle/modules/shape/BoxShape.ts b/packages/core/src/particle/modules/shape/BoxShape.ts index 5d23f54c8..7ad762e2e 100644 --- a/packages/core/src/particle/modules/shape/BoxShape.ts +++ b/packages/core/src/particle/modules/shape/BoxShape.ts @@ -8,8 +8,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; * Particle shape that emits particles from a box. */ export class BoxShape extends BaseShape { - private static _tempVector30 = new Vector3(); - readonly shapeType = ParticleShapeType.Box; @deepClone @@ -37,11 +35,11 @@ export class BoxShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { ShapeUtils._randomPointInsideHalfUnitBox(position, rand); position.multiply(this.size); - const defaultDirection = BoxShape._tempVector30; + const defaultDirection = BaseShape._tempVector30; defaultDirection.set(0.0, 0.0, -1.0); ShapeUtils._randomPointUnitSphere(direction, rand); Vector3.lerp(defaultDirection, direction, this.randomDirectionAmount, direction); @@ -50,7 +48,7 @@ export class BoxShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { const radian = Math.PI * this.randomDirectionAmount; if (this.randomDirectionAmount < 0.5) { @@ -67,7 +65,7 @@ export class BoxShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { const { x, y, z } = this._size; outMin.set(-x * 0.5, -y * 0.5, -z * 0.5); outMax.set(x * 0.5, y * 0.5, z * 0.5); diff --git a/packages/core/src/particle/modules/shape/CircleShape.ts b/packages/core/src/particle/modules/shape/CircleShape.ts index 75e18e850..cc9156f93 100644 --- a/packages/core/src/particle/modules/shape/CircleShape.ts +++ b/packages/core/src/particle/modules/shape/CircleShape.ts @@ -1,4 +1,4 @@ -import { MathUtil, Rand, Vector2, Vector3 } from "@galacean/engine-math"; +import { MathUtil, Rand, Vector3 } from "@galacean/engine-math"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeArcMode } from "./enums/ParticleShapeArcMode"; @@ -8,8 +8,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; * Particle shape that emits particles from a circle. */ export class CircleShape extends BaseShape { - private static _tempPositionPoint = new Vector2(); - readonly shapeType = ParticleShapeType.Circle; private _radius = 1.0; @@ -76,8 +74,8 @@ export class CircleShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { - const positionPoint = CircleShape._tempPositionPoint; + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + const positionPoint = BaseShape._tempVector20; switch (this.arcMode) { case ParticleShapeArcMode.Loop: @@ -101,7 +99,7 @@ export class CircleShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { const randomDirZ = this.randomDirectionAmount > 0.5 ? 1 : Math.sin(this.randomDirectionAmount * Math.PI); const randomDegreeOnXY = 0.5 * (360 - this._arc) * this.randomDirectionAmount; const randomDirY = randomDegreeOnXY > 90 ? -1 : -Math.sin(randomDegreeOnXY); @@ -111,7 +109,7 @@ export class CircleShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { this._getUnitArcRange(this._arc, outMin, outMax, 0, 0); outMin.scale(this._radius); outMax.scale(this._radius); diff --git a/packages/core/src/particle/modules/shape/ConeShape.ts b/packages/core/src/particle/modules/shape/ConeShape.ts index 4fc2b4144..fec2ee7af 100644 --- a/packages/core/src/particle/modules/shape/ConeShape.ts +++ b/packages/core/src/particle/modules/shape/ConeShape.ts @@ -7,11 +7,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; * Cone shape. */ export class ConeShape extends BaseShape { - private static _tempVector20 = new Vector2(); - private static _tempVector21 = new Vector2(); - private static _tempVector30 = new Vector3(); - private static _tempVector31 = new Vector3(); - readonly shapeType = ParticleShapeType.Cone; private _angle = 25.0; @@ -78,8 +73,8 @@ export class ConeShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { - const unitPosition = ConeShape._tempVector20; + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + const unitPosition = BaseShape._tempVector20; const radian = MathUtil.degreeToRadian(this.angle); const dirSinA = Math.sin(radian); const dirCosA = Math.cos(radian); @@ -89,7 +84,7 @@ export class ConeShape extends BaseShape { ShapeUtils.randomPointInsideUnitCircle(unitPosition, rand); position.set(unitPosition.x * this.radius, unitPosition.y * this.radius, 0); - const unitDirection = ConeShape._tempVector21; + const unitDirection = BaseShape._tempVector21; ShapeUtils.randomPointInsideUnitCircle(unitDirection, rand); Vector2.lerp(unitPosition, unitDirection, this.randomDirectionAmount, unitDirection); direction.set(unitDirection.x * dirSinA, unitDirection.y * dirSinA, -dirCosA); @@ -101,11 +96,11 @@ export class ConeShape extends BaseShape { direction.set(unitPosition.x * dirSinA, unitPosition.y * dirSinA, -dirCosA); direction.normalize(); - const distance = ConeShape._tempVector30; + const distance = BaseShape._tempVector30; Vector3.scale(direction, this.length * rand.random(), distance); position.add(distance); - const randomDirection = ConeShape._tempVector31; + const randomDirection = BaseShape._tempVector31; ShapeUtils._randomPointUnitSphere(randomDirection, rand); Vector3.lerp(direction, randomDirection, this.randomDirectionAmount, direction); break; @@ -115,7 +110,7 @@ export class ConeShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { let radian = 0; switch (this.emitType) { case ConeEmitType.Base: @@ -135,7 +130,7 @@ export class ConeShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { const { radius } = this; switch (this.emitType) { diff --git a/packages/core/src/particle/modules/shape/HemisphereShape.ts b/packages/core/src/particle/modules/shape/HemisphereShape.ts index 6001460b5..62ad271be 100644 --- a/packages/core/src/particle/modules/shape/HemisphereShape.ts +++ b/packages/core/src/particle/modules/shape/HemisphereShape.ts @@ -28,7 +28,7 @@ export class HemisphereShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { ShapeUtils._randomPointInsideUnitSphere(position, rand); position.scale(this.radius); @@ -42,7 +42,7 @@ export class HemisphereShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { const randomDir = Math.sin(0.5 * this.randomDirectionAmount * Math.PI); outMin.set(-1, -1, -1); outMax.set(1, 1, randomDir); @@ -51,7 +51,7 @@ export class HemisphereShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { const radius = this._radius; outMin.set(-radius, -radius, -radius); outMax.set(radius, radius, 0); diff --git a/packages/core/src/particle/modules/shape/MeshShape.ts b/packages/core/src/particle/modules/shape/MeshShape.ts index cbdd66acf..e42500370 100644 --- a/packages/core/src/particle/modules/shape/MeshShape.ts +++ b/packages/core/src/particle/modules/shape/MeshShape.ts @@ -54,7 +54,7 @@ export class MeshShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { const { _positionBuffer: positions, _positionElementInfo: positionInfo, @@ -78,7 +78,7 @@ export class MeshShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { const { bounds } = this._mesh; bounds.min.copyTo(outMin); bounds.max.copyTo(outMax); @@ -87,7 +87,7 @@ export class MeshShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { // @todo: Should use min and max of normal, use bounds is worst, but we can't get the min and max of normal by fast way. const { bounds } = this._mesh; bounds.min.copyTo(outMin); diff --git a/packages/core/src/particle/modules/shape/SphereShape.ts b/packages/core/src/particle/modules/shape/SphereShape.ts index 269ff20d4..6bfa0caff 100644 --- a/packages/core/src/particle/modules/shape/SphereShape.ts +++ b/packages/core/src/particle/modules/shape/SphereShape.ts @@ -28,7 +28,7 @@ export class SphereShape extends BaseShape { /** * @internal */ - _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { + _generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void { ShapeUtils._randomPointInsideUnitSphere(position, rand); position.scale(this.radius); @@ -39,7 +39,7 @@ export class SphereShape extends BaseShape { /** * @internal */ - _getDirectionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void { outMin.set(-1, -1, -1); outMax.set(1, 1, 1); } @@ -47,7 +47,7 @@ export class SphereShape extends BaseShape { /** * @internal */ - _getPositionRange(outMin: Vector3, outMax: Vector3): void { + _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void { const radius = this._radius; outMin.set(-radius, -radius, -radius); outMax.set(radius, radius, radius); diff --git a/packages/math/src/BoundingBox.ts b/packages/math/src/BoundingBox.ts index 9db87c851..da387edac 100644 --- a/packages/math/src/BoundingBox.ts +++ b/packages/math/src/BoundingBox.ts @@ -8,9 +8,6 @@ import { Vector3, Vector3Like } from "./Vector3"; * Axis Aligned Bound Box (AABB). */ export class BoundingBox implements IClone, ICopy { - private static _tempVec30: Vector3 = new Vector3(); - private static _tempVec31: Vector3 = new Vector3(); - /** * Calculate a bounding box from the center point and the extent of the bounding box. * @param center - The center point @@ -67,26 +64,46 @@ export class BoundingBox implements IClone, ICopy 0 ? e0 * minX : e0 < 0 ? e0 * maxX : 0) + + (e4 > 0 ? e4 * minY : e4 < 0 ? e4 * maxY : 0) + + (e8 > 0 ? e8 * minZ : e8 < 0 ? e8 * maxZ : 0) + + e[12], + (e1 > 0 ? e1 * minX : e1 < 0 ? e1 * maxX : 0) + + (e5 > 0 ? e5 * minY : e5 < 0 ? e5 * maxY : 0) + + (e9 > 0 ? e9 * minZ : e9 < 0 ? e9 * maxZ : 0) + + e[13], + (e2 > 0 ? e2 * minX : e2 < 0 ? e2 * maxX : 0) + + (e6 > 0 ? e6 * minY : e6 < 0 ? e6 * maxY : 0) + + (e10 > 0 ? e10 * minZ : e10 < 0 ? e10 * maxZ : 0) + + e[14] + ); + + out.max.set( + (e0 > 0 ? e0 * maxX : e0 < 0 ? e0 * minX : 0) + + (e4 > 0 ? e4 * maxY : e4 < 0 ? e4 * minY : 0) + + (e8 > 0 ? e8 * maxZ : e8 < 0 ? e8 * minZ : 0) + + e[12], + (e1 > 0 ? e1 * maxX : e1 < 0 ? e1 * minX : 0) + + (e5 > 0 ? e5 * maxY : e5 < 0 ? e5 * minY : 0) + + (e9 > 0 ? e9 * maxZ : e9 < 0 ? e9 * minZ : 0) + + e[13], + (e2 > 0 ? e2 * maxX : e2 < 0 ? e2 * minX : 0) + + (e6 > 0 ? e6 * maxY : e6 < 0 ? e6 * minY : 0) + + (e10 > 0 ? e10 * maxZ : e10 < 0 ? e10 * minZ : 0) + + e[14] ); - // set min、max - Vector3.subtract(center, extent, out.min); - Vector3.add(center, extent, out.max); } /** diff --git a/tests/src/core/particle/ParticleBoundingBox.test.ts b/tests/src/core/particle/ParticleBoundingBox.test.ts index 8b4e78b8f..24f38f6e9 100644 --- a/tests/src/core/particle/ParticleBoundingBox.test.ts +++ b/tests/src/core/particle/ParticleBoundingBox.test.ts @@ -404,6 +404,73 @@ describe("ParticleBoundingBox", function () { ); }); + it("ShapeTransform-Position", function () { + const shape = new BoxShape(); + shape.position.set(5, 0, 0); + particleRenderer.generator.emission.shape = shape; + + // Same as default BoxShape bounds shifted by (5,0,0) + testParticleRendererBounds( + engine, + particleRenderer, + { x: 3.086, y: -1.914, z: -26.914 }, + { x: 6.914, y: 1.914, z: 1.914 }, + delta + ); + }); + + it("ShapeTransform-Rotation", function () { + const shape = new BoxShape(); + shape.size.set(1, 2, 1); + shape.rotation.set(0, 0, 90); + particleRenderer.generator.emission.shape = shape; + + // size(1,2,1): local pos range (-0.5,-1,-0.5)~(0.5,1,0.5) + // rotated 90 Z: x<->y swapped -> (-1,-0.5,-0.5)~(1,0.5,0.5) + testParticleRendererBounds( + engine, + particleRenderer, + { x: -2.414, y: -1.914, z: -26.914 }, + { x: 2.414, y: 1.914, z: 1.914 }, + delta + ); + }); + + it("ShapeTransform-Scale", function () { + const shape = new BoxShape(); + shape.scale.set(3, 1, 1); + particleRenderer.generator.emission.shape = shape; + + // Default box pos range (-0.5,-0.5,-0.5)~(0.5,0.5,0.5), X scaled 3x -> (-1.5,...)~(1.5,...) + testParticleRendererBounds( + engine, + particleRenderer, + { x: -2.914, y: -1.914, z: -26.914 }, + { x: 2.914, y: 1.914, z: 1.914 }, + delta + ); + }); + + it("ShapeTransform-Combined", function () { + const shape = new BoxShape(); + shape.position.set(0, 0, 5); + shape.rotation.set(0, 0, 90); + shape.scale.set(2, 1, 1); + particleRenderer.generator.emission.shape = shape; + + // Default size(1,1,1): local (-0.5,-0.5,-0.5)~(0.5,0.5,0.5) + // scale(2,1,1) -> (-1,-0.5,-0.5)~(1,0.5,0.5) + // rotate 90 Z: x<->y -> (-0.5,-1,-0.5)~(0.5,1,0.5) + // + position(0,0,5) -> (-0.5,-1,4.5)~(0.5,1,5.5) + testParticleRendererBounds( + engine, + particleRenderer, + { x: -1.914, y: -2.414, z: -21.914 }, + { x: 1.914, y: 2.414, z: 6.914 }, + delta + ); + }); + it("Transform", function () { entity.transform.position.set(1, 2, 3); testParticleRendererBounds( diff --git a/tests/src/core/particle/ParticleShapeTransform.test.ts b/tests/src/core/particle/ParticleShapeTransform.test.ts new file mode 100644 index 000000000..d3ac6d2ac --- /dev/null +++ b/tests/src/core/particle/ParticleShapeTransform.test.ts @@ -0,0 +1,270 @@ +import { BoxShape, SphereShape, ConeShape } from "@galacean/engine-core"; +import { BoundingBox, Rand, Vector3 } from "@galacean/engine-math"; +import { describe, beforeEach, expect, it } from "vitest"; + +describe("ParticleShapeTransform", function () { + const position = new Vector3(); + const direction = new Vector3(); + const rand = new Rand(0, 1234); + const epsilon = 1e-5; + + describe("Position offset", function () { + it("should offset generated position by shape position", function () { + const shape = new BoxShape(); + shape.size.set(0, 0, 0); + shape.position.set(3, 5, 7); + + shape._generatePositionAndDirection(rand, 0, position, direction); + + expect(position.x).to.be.closeTo(3, epsilon); + expect(position.y).to.be.closeTo(5, epsilon); + expect(position.z).to.be.closeTo(7, epsilon); + }); + + it("should offset position range by shape position", function () { + const shape = new BoxShape(); + shape.size.set(2, 2, 2); + shape.position.set(10, 0, 0); + + const bounds = new BoundingBox(); + shape._getPositionRange(bounds); + + expect(bounds.min.x).to.be.closeTo(9, epsilon); + expect(bounds.max.x).to.be.closeTo(11, epsilon); + expect(bounds.min.y).to.be.closeTo(-1, epsilon); + expect(bounds.max.y).to.be.closeTo(1, epsilon); + }); + }); + + describe("Rotation", function () { + it("should rotate position range by shape rotation", function () { + const shape = new BoxShape(); + shape.size.set(2, 0, 0); + shape.rotation.set(0, 0, 90); + + const bounds = new BoundingBox(); + shape._getPositionRange(bounds); + + // Local range: (-1,0,0) to (1,0,0), rotated 90 around Z -> (0,-1,0) to (0,1,0) + expect(bounds.min.x).to.be.closeTo(0, epsilon); + expect(bounds.max.x).to.be.closeTo(0, epsilon); + expect(bounds.min.y).to.be.closeTo(-1, epsilon); + expect(bounds.max.y).to.be.closeTo(1, epsilon); + }); + + it("should rotate box position range", function () { + const shape = new BoxShape(); + shape.size.set(2, 4, 2); + shape.rotation.set(0, 0, 90); + + const bounds = new BoundingBox(); + shape._getPositionRange(bounds); + + // Original range: (-1,-2,-1) to (1,2,1) + // After 90 Z rotation: x<->y swapped + expect(bounds.min.x).to.be.closeTo(-2, epsilon); + expect(bounds.max.x).to.be.closeTo(2, epsilon); + expect(bounds.min.y).to.be.closeTo(-1, epsilon); + expect(bounds.max.y).to.be.closeTo(1, epsilon); + }); + + it("sphere bounds should be conservative after rotation", function () { + const shape = new SphereShape(); + shape.radius = 2; + + const boundsBefore = new BoundingBox(); + shape._getPositionRange(boundsBefore); + const minBefore = new Vector3(); + const maxBefore = new Vector3(); + minBefore.copyFrom(boundsBefore.min); + maxBefore.copyFrom(boundsBefore.max); + + shape.rotation.set(45, 30, 60); + const boundsAfter = new BoundingBox(); + shape._getPositionRange(boundsAfter); + + // Arvo rotates the AABB (cube), which expands it. Bounds should be >= original. + expect(boundsAfter.min.x).to.be.lessThanOrEqual(minBefore.x + epsilon); + expect(boundsAfter.min.y).to.be.lessThanOrEqual(minBefore.y + epsilon); + expect(boundsAfter.min.z).to.be.lessThanOrEqual(minBefore.z + epsilon); + expect(boundsAfter.max.x).to.be.greaterThanOrEqual(maxBefore.x - epsilon); + expect(boundsAfter.max.y).to.be.greaterThanOrEqual(maxBefore.y - epsilon); + expect(boundsAfter.max.z).to.be.greaterThanOrEqual(maxBefore.z - epsilon); + }); + }); + + describe("Scale", function () { + it("should scale generated position", function () { + const shape = new BoxShape(); + shape.size.set(0, 0, 0); + shape.position.set(1, 0, 0); + shape.scale.set(3, 1, 1); + + shape._generatePositionAndDirection(rand, 0, position, direction); + + // position (0,0,0) scaled then rotated then + position offset (1,0,0) + expect(position.x).to.be.closeTo(1, epsilon); + expect(position.y).to.be.closeTo(0, epsilon); + }); + + it("should scale position range", function () { + const shape = new BoxShape(); + shape.size.set(2, 2, 2); + shape.scale.set(3, 1, 1); + + const bounds = new BoundingBox(); + shape._getPositionRange(bounds); + + // Original: (-1,-1,-1) to (1,1,1), scaled X by 3 + expect(bounds.min.x).to.be.closeTo(-3, epsilon); + expect(bounds.max.x).to.be.closeTo(3, epsilon); + expect(bounds.min.y).to.be.closeTo(-1, epsilon); + expect(bounds.max.y).to.be.closeTo(1, epsilon); + }); + + it("negative scale should flip and reorder min/max", function () { + const shape = new BoxShape(); + shape.size.set(2, 2, 2); + shape.scale.set(-1, 1, 1); + + const bounds = new BoundingBox(); + shape._getPositionRange(bounds); + + // After negative X scale, reorder ensures min < max + expect(bounds.min.x).to.be.closeTo(-1, epsilon); + expect(bounds.max.x).to.be.closeTo(1, epsilon); + }); + }); + + describe("Combined transform", function () { + it("should apply scale then rotation then position", function () { + const shape = new BoxShape(); + shape.size.set(0, 0, 0); + shape.position.set(0, 0, 5); + shape.rotation.set(0, 90, 0); + shape.scale.set(2, 1, 1); + + // Generate from zero-size box at origin + shape._generatePositionAndDirection(rand, 0, position, direction); + + // Local pos (0,0,0) -> scale -> (0,0,0) -> rotate -> (0,0,0) -> + offset (0,0,5) + expect(position.x).to.be.closeTo(0, epsilon); + expect(position.y).to.be.closeTo(0, epsilon); + expect(position.z).to.be.closeTo(5, epsilon); + }); + }); + + describe("Default transform (no-op fast path)", function () { + it("should produce identical results when no transform set", function () { + const shape1 = new BoxShape(); + const shape2 = new BoxShape(); + shape1.size.set(2, 3, 4); + shape2.size.set(2, 3, 4); + + const bounds1 = new BoundingBox(); + const bounds2 = new BoundingBox(); + + shape1._getPositionRange(bounds1); + shape2._getPositionRange(bounds2); + + expect(bounds1.min.x).to.be.closeTo(bounds2.min.x, epsilon); + expect(bounds1.min.y).to.be.closeTo(bounds2.min.y, epsilon); + expect(bounds1.min.z).to.be.closeTo(bounds2.min.z, epsilon); + expect(bounds1.max.x).to.be.closeTo(bounds2.max.x, epsilon); + expect(bounds1.max.y).to.be.closeTo(bounds2.max.y, epsilon); + expect(bounds1.max.z).to.be.closeTo(bounds2.max.z, epsilon); + }); + }); + + describe("Direction range", function () { + it("should rotate direction range by shape rotation", function () { + const shape = new BoxShape(); + // Default direction range: min=(0,0,-1), max=(0,0,0) + shape.rotation.set(90, 0, 0); + + const min = new Vector3(); + const max = new Vector3(); + shape._getDirectionRange(min, max); + + // (0,0,-1) rotated 90 around X -> (0,1,0) + // Rotated AABB: min=(0,0,0), max=(0,1,0) + expect(min.x).to.be.closeTo(0, epsilon); + expect(min.y).to.be.closeTo(0, epsilon); + expect(min.z).to.be.closeTo(0, epsilon); + expect(max.y).to.be.closeTo(1, epsilon); + }); + }); + + describe("Clone", function () { + // Simulate CloneManager: deepClone calls copyFrom, then _cloneTo + function simulateClone(source: BoxShape): BoxShape { + const clone = new BoxShape(); + // @deepClone step: copyFrom on existing Vector3 (preserves constructor-bound callbacks) + clone.position.copyFrom(source.position); + clone.rotation.copyFrom(source.rotation); + clone.scale.copyFrom(source.scale); + return clone; + } + + it("cloned shape should have correct transform values", function () { + const shape = new BoxShape(); + shape.position.set(1, 2, 3); + shape.rotation.set(45, 0, 0); + shape.scale.set(2, 2, 2); + + const clone = simulateClone(shape); + + expect(clone.position.x).to.be.closeTo(1, epsilon); + expect(clone.position.y).to.be.closeTo(2, epsilon); + expect(clone.position.z).to.be.closeTo(3, epsilon); + expect(clone.rotation.x).to.be.closeTo(45, epsilon); + expect(clone.scale.x).to.be.closeTo(2, epsilon); + }); + + it("cloned shape should rebuild matrix correctly", function () { + const shape = new BoxShape(); + shape.size.set(2, 0, 0); + shape.rotation.set(0, 0, 90); + + const clone = simulateClone(shape); + clone.size.set(2, 0, 0); + + const boundsOrig = new BoundingBox(); + shape._getPositionRange(boundsOrig); + + const boundsClone = new BoundingBox(); + clone._getPositionRange(boundsClone); + + // Both should have: local (-1,0,0)~(1,0,0) rotated 90Z -> (0,-1,0)~(0,1,0) + expect(boundsClone.min.x).to.be.closeTo(boundsOrig.min.x, epsilon); + expect(boundsClone.min.y).to.be.closeTo(boundsOrig.min.y, epsilon); + expect(boundsClone.max.x).to.be.closeTo(boundsOrig.max.x, epsilon); + expect(boundsClone.max.y).to.be.closeTo(boundsOrig.max.y, epsilon); + }); + + it("modifying clone should not affect original", function () { + const shape = new BoxShape(); + shape.position.set(1, 2, 3); + + const clone = simulateClone(shape); + clone.position.set(10, 20, 30); + + expect(shape.position.x).to.be.closeTo(1, epsilon); + expect(shape.position.y).to.be.closeTo(2, epsilon); + expect(shape.position.z).to.be.closeTo(3, epsilon); + }); + + it("clone callback should trigger on cloned shape", function () { + const shape = new BoxShape(); + const clone = simulateClone(shape); + + let notified = false; + clone._registerOnValueChanged(() => { + notified = true; + }); + + clone.position.x = 10; + expect(notified).to.be.true; + }); + }); +}); diff --git a/tests/src/math/BoundingBox.test.ts b/tests/src/math/BoundingBox.test.ts index 71e4a8245..b097f2891 100644 --- a/tests/src/math/BoundingBox.test.ts +++ b/tests/src/math/BoundingBox.test.ts @@ -63,8 +63,9 @@ describe("BoundingBox test", () => { new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) ); BoundingBox.transform(maxValueBox, matrixWithoutScale, newBox); - expect(newBox.min).to.deep.eq(compare.set(-Infinity, -Infinity, -Infinity)); - expect(newBox.max).to.deep.eq(compare.set(Infinity, Infinity, Infinity)); + // Identity rotation * MAX_VALUE = MAX_VALUE, adding small translation doesn't overflow + expect(Math.abs(newBox.min.x)).eq(Number.MAX_VALUE); + expect(Math.abs(newBox.max.x)).eq(Number.MAX_VALUE); BoundingBox.transform(maxValueBox, matrixWithScale, newBox); expect(newBox.min).to.deep.eq(compare.set(-Infinity, -Infinity, -Infinity)); expect(newBox.max).to.deep.eq(compare.set(Infinity, Infinity, Infinity)); From 79b0698338506637b7647564b73bd0fee9f28d03 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 16 Apr 2026 17:49:49 +0800 Subject: [PATCH 2/6] chore: release v2.0.0-alpha.28 --- e2e/package.json | 2 +- examples/package.json | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/design/package.json | 2 +- packages/galacean/package.json | 2 +- packages/loader/package.json | 2 +- packages/math/package.json | 2 +- packages/physics-lite/package.json | 2 +- packages/physics-physx/package.json | 2 +- packages/rhi-webgl/package.json | 2 +- packages/shader-lab/package.json | 2 +- packages/shader/package.json | 2 +- packages/ui/package.json | 2 +- packages/xr-webxr/package.json | 2 +- packages/xr/package.json | 2 +- tests/package.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index bae4d315c..3037edd53 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index 11f00a3e9..38838618e 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index c1d0e3206..fa57625c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "packageManager": "pnpm@9.3.0", "private": true, "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 55ce008fe..3d487e384 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/package.json b/packages/design/package.json index 7565148cf..6ee535a97 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index 9f4a418c0..1b4c84651 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index e1c2de918..e8ba7132b 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 93002bd8b..8ee71252b 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-lite/package.json b/packages/physics-lite/package.json index 879ee51cf..d10afe471 100644 --- a/packages/physics-lite/package.json +++ b/packages/physics-lite/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-lite", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index e4f0a5103..213d89a82 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 8fd81015a..eeb25a491 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/shader-lab/package.json b/packages/shader-lab/package.json index e8180f256..771bf9835 100644 --- a/packages/shader-lab/package.json +++ b/packages/shader-lab/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shaderlab", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index 85fb031b7..686ef52d4 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index 6dc7ed472..4500c519d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index ead97cb18..a72cfa71c 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index 87b6de977..4399c063a 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index 9fe84811d..b3aef245d 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "2.0.0-alpha.27", + "version": "2.0.0-alpha.28", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", From 00942cc808bc7d3f32d5e23fb0c44f14854886c7 Mon Sep 17 00:00:00 2001 From: ChenMo Date: Fri, 17 Apr 2026 14:58:35 +0800 Subject: [PATCH 3/6] refactor(particle): rename ParticleScaleMode enums for clarity (#2970) * refactor(particle): rename ParticleScaleMode enums for clarity --- docs/en/graphics/particle/renderer-main-module.mdx | 6 +++--- docs/zh/graphics/particle/renderer-main-module.mdx | 6 +++--- e2e/case/particleRenderer-fire.ts | 8 ++++---- packages/core/src/particle/enums/ParticleScaleMode.ts | 10 +++++----- packages/core/src/particle/modules/MainModule.ts | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/en/graphics/particle/renderer-main-module.mdx b/docs/en/graphics/particle/renderer-main-module.mdx index 4e3888fea..ea6d2c8e5 100644 --- a/docs/en/graphics/particle/renderer-main-module.mdx +++ b/docs/en/graphics/particle/renderer-main-module.mdx @@ -44,6 +44,6 @@ You can debug each property in the provided example to better understand and con The scalingMode has the following options: -- **Local**: Particles inherit the local transformation of the particle generator, meaning the particle transformation occurs in the generator's local coordinate system. -- **World**: Particles inherit the global transformation of the particle generator, meaning the particle transformation occurs in the world coordinate system. -- **Hierarchy**: Particles inherit transformations from the entire transformation hierarchy, meaning particles consider transformations of the generator's parent and higher-level transformations. \ No newline at end of file +- **World**: Scale particle emission position and size using the world scale, including all parent transforms. +- **Local**: Scale particle emission position and size using only its own transform scale, ignoring parent scale. +- **Shape**: Only scale the emission shape area, particles themselves are not affected. \ No newline at end of file diff --git a/docs/zh/graphics/particle/renderer-main-module.mdx b/docs/zh/graphics/particle/renderer-main-module.mdx index b551210dc..1faa4e8be 100644 --- a/docs/zh/graphics/particle/renderer-main-module.mdx +++ b/docs/zh/graphics/particle/renderer-main-module.mdx @@ -44,6 +44,6 @@ label: Graphics/Particle scalingMode 有以下几种模式: -- **Local**:粒子会继承粒子生成器的局部变换,即粒子的变换是在生成器的本地坐标系中进行的 -- **World**:粒子会继承粒子生成器的全局变换,即粒子的变换是在世界坐标系中进行的 -- **Hierarchy**:粒子会继承整个变换层级中的变换,即粒子会考虑到生成器的父级及更上级的变换 +- **World**:使用世界缩放(包含所有父级变换)来缩放粒子的发射位置和大小 +- **Local**:仅使用自身 Transform 的缩放来缩放粒子的发射位置和大小,忽略父级缩放 +- **Shape**:仅缩放发射形状区域,粒子本身的大小不受影响 diff --git a/e2e/case/particleRenderer-fire.ts b/e2e/case/particleRenderer-fire.ts index 563269198..d1b0c5689 100644 --- a/e2e/case/particleRenderer-fire.ts +++ b/e2e/case/particleRenderer-fire.ts @@ -44,7 +44,7 @@ WebGLEngine.create({ // Create camera const cameraEntity = rootEntity.createChild("camera_entity"); - cameraEntity.transform.position = new Vector3(-10, 1, 3);// -10 can test bounds transform + cameraEntity.transform.position = new Vector3(-10, 1, 3); // -10 can test bounds transform const camera = cameraEntity.addComponent(Camera); camera.fieldOfView = 60; @@ -199,7 +199,7 @@ function createFireGlowParticle(fireEntity: Entity, texture: Texture2D): void { main.simulationSpace = ParticleSimulationSpace.World; - main.scalingMode = ParticleScaleMode.Hierarchy; + main.scalingMode = ParticleScaleMode.World; // Emission module emission.rateOverTime.constant = 20; @@ -270,7 +270,7 @@ function createFireSmokeParticle(fireEntity: Entity, texture: Texture2D): void { main.simulationSpace = ParticleSimulationSpace.World; - main.scalingMode = ParticleScaleMode.Hierarchy; + main.scalingMode = ParticleScaleMode.World; // Emission module emission.rateOverTime.constant = 25; @@ -353,7 +353,7 @@ function createFireEmbersParticle(fireEntity: Entity, texture: Texture2D): void main.simulationSpace = ParticleSimulationSpace.World; - main.scalingMode = ParticleScaleMode.Hierarchy; + main.scalingMode = ParticleScaleMode.World; // Emission module emission.rateOverTime.constant = 65; diff --git a/packages/core/src/particle/enums/ParticleScaleMode.ts b/packages/core/src/particle/enums/ParticleScaleMode.ts index 0f780ef94..b3ec3a3fe 100644 --- a/packages/core/src/particle/enums/ParticleScaleMode.ts +++ b/packages/core/src/particle/enums/ParticleScaleMode.ts @@ -2,10 +2,10 @@ * Control how Particle Generator apply transform scale. */ export enum ParticleScaleMode { - /** Scale the Particle Generator using the entire transform hierarchy. */ - Hierarchy, - /** Scale the Particle Generator using only its own transform scale. (Ignores parent scale). */ + /** Scale the Particle Generator using the world scale, including all parent transforms. */ + World, + /** Scale the Particle Generator using only its own transform scale, ignoring parent scale. */ Local, - /** Only apply transform scale to the shape component, which controls where particles are spawned, but does not affect their size or movement. */ - World + /** Scale only the emitter shape positions; particle size and movement are unaffected. */ + Shape } diff --git a/packages/core/src/particle/modules/MainModule.ts b/packages/core/src/particle/modules/MainModule.ts index 9318cbdb4..d17c156d9 100644 --- a/packages/core/src/particle/modules/MainModule.ts +++ b/packages/core/src/particle/modules/MainModule.ts @@ -277,8 +277,8 @@ export class MainModule implements ICustomClone { _getPositionScale(): Vector3 { const transform = this._generator._renderer.entity.transform; switch (this.scalingMode) { - case ParticleScaleMode.Hierarchy: case ParticleScaleMode.World: + case ParticleScaleMode.Shape: return transform.lossyWorldScale; case ParticleScaleMode.Local: return transform.scale; @@ -306,7 +306,7 @@ export class MainModule implements ICustomClone { } switch (this.scalingMode) { - case ParticleScaleMode.Hierarchy: + case ParticleScaleMode.World: var scale = transform.lossyWorldScale; shaderData.setVector3(MainModule._positionScale, scale); shaderData.setVector3(MainModule._sizeScale, scale); @@ -316,7 +316,7 @@ export class MainModule implements ICustomClone { shaderData.setVector3(MainModule._positionScale, scale); shaderData.setVector3(MainModule._sizeScale, scale); break; - case ParticleScaleMode.World: + case ParticleScaleMode.Shape: shaderData.setVector3(MainModule._positionScale, transform.lossyWorldScale); shaderData.setVector3(MainModule._sizeScale, MainModule._vector3One); break; From 35bc25807171ca99684a3ceb0c42dc826e82e76d Mon Sep 17 00:00:00 2001 From: zhuxudong Date: Fri, 17 Apr 2026 15:05:05 +0800 Subject: [PATCH 4/6] fix(shader): add missing camera_VPMat declaration in Transform.glsl (#2969) fix(shader): add missing camera_VPMat declaration in Transform.glsl --- packages/shader/src/shaders/Transform.glsl | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shader/src/shaders/Transform.glsl b/packages/shader/src/shaders/Transform.glsl index b5d1a52a6..7fccb9426 100644 --- a/packages/shader/src/shaders/Transform.glsl +++ b/packages/shader/src/shaders/Transform.glsl @@ -5,6 +5,7 @@ mat4 renderer_LocalMat; mat4 renderer_ModelMat; mat4 camera_ViewMat; mat4 camera_ProjMat; +mat4 camera_VPMat; mat4 renderer_MVMat; mat4 renderer_MVPMat; mat4 renderer_NormalMat; From 3f24a7d7ad8e521c2cb318afe9b792bdab530f85 Mon Sep 17 00:00:00 2001 From: zhuxudong Date: Fri, 17 Apr 2026 15:09:49 +0800 Subject: [PATCH 5/6] fix(shader-lab): add missing semicolon in GLES100 fragment return conversion (#2968) fix(shader-lab): add missing semicolon in GLES100 fragment return conversion --- packages/shader-lab/src/codeGen/GLES100.ts | 2 +- tests/src/shader-lab/ShaderLab.test.ts | 6 +++ .../shaders/frag-return-vec4.shader | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/src/shader-lab/shaders/frag-return-vec4.shader diff --git a/packages/shader-lab/src/codeGen/GLES100.ts b/packages/shader-lab/src/codeGen/GLES100.ts index 995dccdee..2c2e0faa9 100644 --- a/packages/shader-lab/src/codeGen/GLES100.ts +++ b/packages/shader-lab/src/codeGen/GLES100.ts @@ -47,7 +47,7 @@ export class GLES100Visitor extends GLESVisitor { return ""; } const expression = node.children[1] as ASTNode.Expression; - return `gl_FragColor = ${expression.codeGen(this)}`; + return `gl_FragColor = ${expression.codeGen(this)};`; } return super.visitJumpStatement(node); } diff --git a/tests/src/shader-lab/ShaderLab.test.ts b/tests/src/shader-lab/ShaderLab.test.ts index e998533e5..3031cebc0 100644 --- a/tests/src/shader-lab/ShaderLab.test.ts +++ b/tests/src/shader-lab/ShaderLab.test.ts @@ -251,4 +251,10 @@ describe("ShaderLab", async () => { const shaderSource = await readFile("./shaders/mrt-struct.shader"); glslValidate(engine, shaderSource, shaderLabRelease); }); + + it("frag-return-vec4 (Cocos pattern: fragment entry returns vec4 instead of void)", async () => { + const shaderSource = await readFile("./shaders/frag-return-vec4.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + glslValidate(engine, shaderSource, shaderLabVerbose); + }); }); diff --git a/tests/src/shader-lab/shaders/frag-return-vec4.shader b/tests/src/shader-lab/shaders/frag-return-vec4.shader new file mode 100644 index 000000000..9b3ea3d4c --- /dev/null +++ b/tests/src/shader-lab/shaders/frag-return-vec4.shader @@ -0,0 +1,41 @@ +Shader "frag-return-vec4-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + vec3 SRGBToLinear(vec3 gamma) { return gamma * gamma; } + vec3 LinearToSRGB(vec3 linear) { return sqrt(linear); } + + vec4 SurfacesFragmentModifyBaseColorAndTransparency(Varyings input) { + vec4 color = texture2D(u_texture, input.v_uv); + color.rgb = SRGBToLinear(color.rgb); + return color; + } + + Varyings vert(Attributes attr) { + Varyings o; + vec4 pos = renderer_MVPMat * vec4(attr.POSITION, 1.0); + o.v_worldPos = pos.xyz; + o.v_normal = vec4(attr.NORMAL, 1.0); + o.v_uv = attr.TEXCOORD_0; + gl_Position = pos; + return o; + } + + vec4 frag(Varyings input) { + vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(input); + color.rgb = LinearToSRGB(color.rgb); + return color; + } + } + } +} From f9064e7fc54c56d960fad7aabe00b95ebfd08848 Mon Sep 17 00:00:00 2001 From: "chenmo.gl" Date: Fri, 17 Apr 2026 22:52:07 +0800 Subject: [PATCH 6/6] chore: release v2.0.0-alpha.29 --- e2e/package.json | 2 +- examples/package.json | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/design/package.json | 2 +- packages/galacean/package.json | 2 +- packages/loader/package.json | 2 +- packages/math/package.json | 2 +- packages/physics-lite/package.json | 2 +- packages/physics-physx/package.json | 2 +- packages/rhi-webgl/package.json | 2 +- packages/shader-lab/package.json | 2 +- packages/shader/package.json | 2 +- packages/ui/package.json | 2 +- packages/xr-webxr/package.json | 2 +- packages/xr/package.json | 2 +- tests/package.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index 3037edd53..3b4e78453 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index 38838618e..4c8f46486 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index fa57625c0..d79cd2197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "packageManager": "pnpm@9.3.0", "private": true, "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 3d487e384..f91716e7b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/package.json b/packages/design/package.json index 6ee535a97..74b7c814d 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index 1b4c84651..cea4e6f89 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index e8ba7132b..a86b25877 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 8ee71252b..8e8019abf 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-lite/package.json b/packages/physics-lite/package.json index d10afe471..e1fd33443 100644 --- a/packages/physics-lite/package.json +++ b/packages/physics-lite/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-lite", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index 213d89a82..852c01169 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index eeb25a491..2940b6b25 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/shader-lab/package.json b/packages/shader-lab/package.json index 771bf9835..47c2e6c34 100644 --- a/packages/shader-lab/package.json +++ b/packages/shader-lab/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shaderlab", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index 686ef52d4..0ae7db048 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index 4500c519d..60f127ffd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index a72cfa71c..5efd1b3eb 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index 4399c063a..aa5ece738 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index b3aef245d..0c3087776 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js",