diff --git a/e2e/case/particleRenderer-limitVelocity.ts b/e2e/case/particleRenderer-limitVelocity.ts new file mode 100644 index 0000000000..4674c4190a --- /dev/null +++ b/e2e/case/particleRenderer-limitVelocity.ts @@ -0,0 +1,134 @@ +/** + * @title Particle Limit Velocity Over Lifetime + * @category Particle + */ +import { + AssetType, + BlendMode, + Burst, + Camera, + Color, + Engine, + Entity, + SphereShape, + ParticleCompositeCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleSimulationSpace, + PostProcess, + BloomEffect, + TonemappingEffect, + Texture2D, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor = new Color(0, 0, 0, 1); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(2, 1.43, 30); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + camera.enableHDR = true; + camera.enablePostProcess = true; + + // Post process + const postProcess = rootEntity.addComponent(PostProcess); + const bloom = postProcess.addEffect(BloomEffect); + bloom.intensity.value = 1; + bloom.threshold.value = 0.8; + postProcess.addEffect(TonemappingEffect); + + engine.resourceManager + .load({ + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D + }) + .then((texture) => { + createParticle(engine, rootEntity, texture); + + updateForE2E(engine, 30); + initScreenshot(engine, camera); + }); +}); + +function createParticle(engine: Engine, rootEntity: Entity, texture: Texture2D): void { + const particleEntity = new Entity(engine, "LimitVelocity"); + particleEntity.transform.setPosition(2.006557, 1.43, 12.35); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + const generator = particleRenderer.generator; + generator.useAutoRandomSeed = false; + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(0.2, 0.6, 1.0, 1.0); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + + const { main, emission, limitVelocityOverLifetime, colorOverLifetime, velocityOverLifetime } = generator; + + // Main + main.duration = 2; + main.isLoop = true; + main.startDelay.constant = 0; + main.startLifetime.constantMin = 0.6; + main.startLifetime.constantMax = 1; + main.startLifetime.mode = ParticleCurveMode.TwoConstants; + main.startSpeed.constantMin = 20; + main.startSpeed.constantMax = 40; + main.startSpeed.mode = ParticleCurveMode.TwoConstants; + main.startSize.constantMin = 0.05; + main.startSize.constantMax = 0.15; + main.startSize.mode = ParticleCurveMode.TwoConstants; + main.startColor.constantMin.set(280 / 255, 670 / 255, 2550 / 255, 1); + main.startColor.constantMax.set(1130 / 255, 740 / 255, 2550 / 255, 1); + main.startColor.mode = ParticleGradientMode.TwoConstants; + main.gravityModifier.constant = 0; + main.simulationSpace = ParticleSimulationSpace.Local; + main.maxParticles = 100; + + // Emission + emission.rateOverTime.constant = 0; + emission.addBurst(new Burst(0, new ParticleCompositeCurve(10, 30))); + const sphereShape = new SphereShape(); + sphereShape.radius = 0.8; + emission.shape = sphereShape; + + // Color over lifetime + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + const gradient = colorOverLifetime.color.gradient; + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.2, 1.0); + gradient.addAlphaKey(0.8, 1.0); + + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constant = 1; + velocityOverLifetime.velocityY.constant = 20; + velocityOverLifetime.velocityZ.constant = 1; + + // Limit velocity over lifetime + limitVelocityOverLifetime.enabled = true; + limitVelocityOverLifetime.separateAxes = true; + limitVelocityOverLifetime.limitX = new ParticleCompositeCurve(1); + limitVelocityOverLifetime.limitY = new ParticleCompositeCurve(1); + limitVelocityOverLifetime.limitZ = new ParticleCompositeCurve(0); + limitVelocityOverLifetime.space = ParticleSimulationSpace.World; + limitVelocityOverLifetime.dampen = 0.25; + limitVelocityOverLifetime.drag = new ParticleCompositeCurve(0.0); + limitVelocityOverLifetime.multiplyDragByParticleSize = true; + limitVelocityOverLifetime.multiplyDragByParticleVelocity = true; + + rootEntity.addChild(particleEntity); +} diff --git a/e2e/config.ts b/e2e/config.ts index f4f205fed8..2672153f06 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -335,6 +335,12 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0.1630209 }, + limitVelocityOverLifetime: { + category: "Particle", + caseFileName: "particleRenderer-limitVelocity", + threshold: 0, + diffPercentage: 0.0364 + }, textureSheetAnimation: { category: "Particle", caseFileName: "particleRenderer-textureSheetAnimation", @@ -345,7 +351,7 @@ export const E2E_CONFIG = { category: "Particle", caseFileName: "particleRenderer-shape-mesh", threshold: 0, - diffPercentage: 0.0162 + diffPercentage: 0.01698 }, particleEmissive: { category: "Particle", diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-cone.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-cone.jpg index cf9a99302e..291522d4ce 100644 --- a/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-cone.jpg +++ b/e2e/fixtures/originImage/Particle_particleRenderer-emit-mesh-cone.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c2168c5fdd661c8871e245223845f2d3841e3c25a57ede9d34fc73128378d85 -size 178324 +oid sha256:154344f6c6ef52832319dde824e4035ffcabce4484fbbd616bc66da8956ee300 +size 178327 diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg index a232c21efd..7bc9108edd 100644 --- a/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg +++ b/e2e/fixtures/originImage/Particle_particleRenderer-force.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da082916612b6fbeb51732f0c25f0f8b74310f4fd2ac6cad6d29ee5cad74aa70 -size 489962 +oid sha256:ae33bd68046a7fb89c485721302d4039f98678b618e580b34e665773b55bc763 +size 489996 diff --git a/e2e/fixtures/originImage/Particle_particleRenderer-limitVelocity.jpg b/e2e/fixtures/originImage/Particle_particleRenderer-limitVelocity.jpg new file mode 100644 index 0000000000..146cfc4390 --- /dev/null +++ b/e2e/fixtures/originImage/Particle_particleRenderer-limitVelocity.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f94855dbe8ce1233dceb3932f1429f703bb0bdef71025e8014596fc7041d7a87 +size 27975 diff --git a/e2e/package.json b/e2e/package.json index 5639e60ee6..c4e8dc3e7f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index aaaf429bc3..953fea685b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index 4704a72565..77b734b367 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "packageManager": "pnpm@9.3.0", "private": true, "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 333ba6c9f1..34e6110f2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index b179899b8e..141f352d35 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -196,8 +196,8 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { /** * @internal */ - override _cloneTo(target: SpriteMask, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); + override _cloneTo(target: SpriteMask): void { + super._cloneTo(target); target.sprite = this._sprite; } diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index 4ee335010f..c1b183ee7a 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -286,8 +286,8 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { /** * @internal */ - override _cloneTo(target: SpriteRenderer, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); + override _cloneTo(target: SpriteRenderer): void { + super._cloneTo(target); target.sprite = this._sprite; target.drawMode = this._drawMode; } diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 5c54487b01..143e46a7da 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -333,8 +333,8 @@ export class TextRenderer extends Renderer implements ITextRenderer { /** * @internal */ - override _cloneTo(target: TextRenderer, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); + override _cloneTo(target: TextRenderer): void { + super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; } @@ -458,8 +458,8 @@ export class TextRenderer extends Renderer implements ITextRenderer { // prettier-ignore const e0 = e[0], e1 = e[1], e2 = e[2], - e4 = e[4], e5 = e[5], e6 = e[6], - e12 = e[12], e13 = e[13], e14 = e[14]; + e4 = e[4], e5 = e[5], e6 = e[6], + e12 = e[12], e13 = e[13], e14 = e[14]; const up = TextRenderer._tempVec31.set(e4, e5, e6); const right = TextRenderer._tempVec30.set(e0, e1, e2); diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index c849653f11..53b689dd84 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -829,7 +829,7 @@ export class Camera extends Component { /** * @internal */ - _cloneTo(target: Camera, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Camera): void { this._renderTarget?._addReferCount(1); } diff --git a/packages/core/src/Component.ts b/packages/core/src/Component.ts index b3af438d07..2ebafbcd05 100644 --- a/packages/core/src/Component.ts +++ b/packages/core/src/Component.ts @@ -1,6 +1,7 @@ import { IReferable } from "./asset/IReferable"; import { EngineObject } from "./base"; import { assignmentClone, ignoreClone } from "./clone/CloneManager"; +import { CloneUtils } from "./clone/CloneUtils"; import { Entity } from "./Entity"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { Scene } from "./Scene"; @@ -10,7 +11,6 @@ import { Scene } from "./Scene"; */ export class Component extends EngineObject { /** @internal */ - @ignoreClone _entity: Entity; /** @internal */ @@ -154,6 +154,13 @@ export class Component extends EngineObject { } } + /** + * @internal + */ + _remap(srcRoot: Entity, targetRoot: Entity): T { + return CloneUtils.remapComponent(srcRoot, targetRoot, this) as unknown as T; + } + protected _addResourceReferCount(resource: IReferable, count: number): void { this._entity._isTemplate || resource._addReferCount(count); } diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index 72030c18cf..8e06cc6344 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -32,7 +32,6 @@ import { PostProcessUberPass } from "./postProcess/PostProcessUberPass"; import { Shader } from "./shader/Shader"; import { ShaderMacro } from "./shader/ShaderMacro"; import { ShaderMacroCollection } from "./shader/ShaderMacroCollection"; -import { ShaderPass } from "./shader/ShaderPass"; import { ShaderPool } from "./shader/ShaderPool"; import { ShaderProgramPool } from "./shader/ShaderProgramPool"; import { RenderState } from "./shader/state/RenderState"; @@ -542,8 +541,7 @@ export class Engine extends EventDispatcher { /** * @internal */ - _getShaderProgramPool(shaderPass: ShaderPass): ShaderProgramPool { - const index = shaderPass._shaderPassId; + _getShaderProgramPool(index: number, trackPools?: ShaderProgramPool[]): ShaderProgramPool { const shaderProgramPools = this._shaderProgramPools; let pool = shaderProgramPools[index]; if (!pool) { @@ -552,7 +550,7 @@ export class Engine extends EventDispatcher { shaderProgramPools.length = length; } shaderProgramPools[index] = pool = new ShaderProgramPool(this); - shaderPass._shaderProgramPools.push(pool); + trackPools?.push(pool); } return pool; } diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 0dd3fdb289..08503f5498 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -10,6 +10,7 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { ReferResource } from "./asset/ReferResource"; import { EngineObject } from "./base"; +import { CloneUtils } from "./clone/CloneUtils"; import { ComponentCloner } from "./clone/ComponentCloner"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { EntityModifyFlags } from "./enums/EntityModifyFlags"; @@ -431,6 +432,13 @@ export class Entity extends EngineObject { return this._updateFlagManager.createFlag(BoolUpdateFlag); } + /** + * @internal + */ + _remap(srcRoot: Entity, targetRoot: Entity): Entity { + return CloneUtils.remapEntity(srcRoot, targetRoot, this); + } + /** * @internal */ diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index 339c7ed9bd..ab9f743c0c 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -8,7 +8,6 @@ import { RenderContext } from "./RenderPipeline/RenderContext"; import { SubRenderElement } from "./RenderPipeline/SubRenderElement"; import { Transform, TransformModifyFlags } from "./Transform"; import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; -import { IComponentCustomClone } from "./clone/ComponentCloner"; import { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; import { Material } from "./material"; import { ShaderMacro, ShaderProperty } from "./shader"; @@ -21,7 +20,7 @@ import { ShaderDataGroup } from "./shader/enums/ShaderDataGroup"; * @decorator `@dependentComponents(Transform, DependentMode.CheckOnly)` */ @dependentComponents(Transform, DependentMode.CheckOnly) -export class Renderer extends Component implements IComponentCustomClone { +export class Renderer extends Component { private static _tempVector0 = new Vector3(); private static _receiveShadowMacro = ShaderMacro.getByName("RENDERER_IS_RECEIVE_SHADOWS"); @@ -66,7 +65,6 @@ export class Renderer extends Component implements IComponentCustomClone { protected _rendererLayer: Vector4 = new Vector4(); @ignoreClone protected _bounds: BoundingBox = new BoundingBox(); - @ignoreClone protected _transformEntity: Entity; @deepClone @@ -351,7 +349,7 @@ export class Renderer extends Component implements IComponentCustomClone { /** * @internal */ - _cloneTo(target: Renderer, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Renderer): void { const materials = this._materials; for (let i = 0, n = materials.length; i < n; i++) { target._setMaterial(i, materials[i]); diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index 7312b4ef7f..f822474dcc 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -51,7 +51,6 @@ export class Transform extends Component { @ignoreClone protected _isParentDirty: boolean = true; - @ignoreClone private _parentTransformCache: Transform = null; @ignoreClone private _dirtyFlag: number = TransformModifyFlags.LqLmWmWpWeWqWsWus; @@ -581,7 +580,7 @@ export class Transform extends Component { /** * @internal */ - _cloneTo(target: Transform, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Transform): void { const { _position: position, _rotation: rotation, _scale: scale } = target; // @ts-ignore diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 0a6ce0a819..180c2d1356 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -334,7 +334,7 @@ export class Animator extends Component { /** * @internal */ - _cloneTo(target: Animator, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Animator): void { const animatorController = target._animatorController; if (animatorController) { target._addResourceReferCount(animatorController, 1); diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index d343008363..29a3ed8bc1 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -217,7 +217,7 @@ export class AudioSource extends Component { /** * @internal */ - _cloneTo(target: AudioSource, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: AudioSource): void { target._clip?._addReferCount(1); target._gainNode.gain.setValueAtTime(target._volume, AudioManager.getContext().currentTime); } diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index 2ff2b87a30..be46462982 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -1,6 +1,6 @@ import { Entity } from "../Entity"; import { TypedArray } from "../base/Constant"; -import { IComponentCustomClone, ICustomClone } from "./ComponentCloner"; +import { ICustomClone } from "./ComponentCloner"; import { CloneMode } from "./enums/CloneMode"; /** @@ -103,98 +103,90 @@ export class CloneManager { targetRoot: Entity, deepInstanceMap: Map ): void { - if (cloneMode === CloneMode.Ignore) { + const sourceProperty = source[k]; + + // Remappable references (Entity/Component) are always remapped, regardless of clone decorator + if (sourceProperty instanceof Object && (sourceProperty)._remap) { + target[k] = (sourceProperty)._remap(srcRoot, targetRoot); return; } - const sourceProperty = source[k]; - if (sourceProperty instanceof Object) { - if (cloneMode === undefined || cloneMode === CloneMode.Assignment) { - target[k] = sourceProperty; - return; - } + if (cloneMode === CloneMode.Ignore) return; - const type = sourceProperty.constructor; - switch (type) { - case Uint8Array: - case Uint16Array: - case Uint32Array: - case Int8Array: - case Int16Array: - case Int32Array: - case Float32Array: - case Float64Array: - let targetPropertyT = target[k]; - if (targetPropertyT == null || targetPropertyT.length !== (sourceProperty).length) { - target[k] = (sourceProperty).slice(); - } else { - targetPropertyT.set(sourceProperty); - } - break; - case Array: - let targetPropertyA = >target[k]; - const length = (>sourceProperty).length; - if (targetPropertyA == null) { - target[k] = targetPropertyA = new Array(length); - } else { - targetPropertyA.length = length; + // Primitives, undecorated, or @assignmentClone: direct assign + if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) { + target[k] = sourceProperty; + return; + } + + // @shallowClone / @deepClone: deep copy complex objects + const type = sourceProperty.constructor; + switch (type) { + case Uint8Array: + case Uint16Array: + case Uint32Array: + case Int8Array: + case Int16Array: + case Int32Array: + case Float32Array: + case Float64Array: + let targetPropertyT = target[k]; + if (targetPropertyT == null || targetPropertyT.length !== (sourceProperty).length) { + target[k] = (sourceProperty).slice(); + } else { + targetPropertyT.set(sourceProperty); + } + break; + case Array: + let targetPropertyA = >target[k]; + const length = (>sourceProperty).length; + if (targetPropertyA == null) { + target[k] = targetPropertyA = new Array(length); + } else { + targetPropertyA.length = length; + } + for (let i = 0; i < length; i++) { + CloneManager.cloneProperty( + >sourceProperty, + targetPropertyA, + i, + cloneMode, + srcRoot, + targetRoot, + deepInstanceMap + ); + } + break; + default: + let targetProperty = target[k]; + // If the target property is undefined, create new instance and keep reference sharing like the source + if (!targetProperty) { + targetProperty = deepInstanceMap.get(sourceProperty); + if (!targetProperty) { + targetProperty = new sourceProperty.constructor(); + deepInstanceMap.set(sourceProperty, targetProperty); } - for (let i = 0; i < length; i++) { + target[k] = targetProperty; + } + + if ((sourceProperty).copyFrom) { + (targetProperty).copyFrom(sourceProperty); + } else { + const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); + for (let k in sourceProperty) { CloneManager.cloneProperty( - >sourceProperty, - targetPropertyA, - i, - cloneMode, + sourceProperty, + targetProperty, + k, + cloneModes[k], srcRoot, targetRoot, deepInstanceMap ); } - break; - default: - let targetProperty = target[k]; - // If the target property is undefined, create new instance and keep reference sharing like the source - if (!targetProperty) { - targetProperty = deepInstanceMap.get(sourceProperty); - if (!targetProperty) { - targetProperty = new sourceProperty.constructor(); - deepInstanceMap.set(sourceProperty, targetProperty); - } - target[k] = targetProperty; - } - - if ((sourceProperty).copyFrom) { - // Custom clone - (targetProperty).copyFrom(sourceProperty); - } else { - // Universal clone - const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); - for (let k in sourceProperty) { - CloneManager.cloneProperty( - sourceProperty, - targetProperty, - k, - cloneModes[k], - srcRoot, - targetRoot, - deepInstanceMap - ); - } - - // Custom incremental clone - if ((sourceProperty)._cloneTo) { - (sourceProperty)._cloneTo( - targetProperty, - srcRoot, - targetRoot - ); - } - } - break; - } - } else { - // null, undefined, primitive type, function - target[k] = sourceProperty; + (sourceProperty)._cloneTo?.(targetProperty, srcRoot, targetRoot); + } + break; } } diff --git a/packages/core/src/clone/CloneUtils.ts b/packages/core/src/clone/CloneUtils.ts index 048fdae2d8..bded5ed474 100644 --- a/packages/core/src/clone/CloneUtils.ts +++ b/packages/core/src/clone/CloneUtils.ts @@ -1,5 +1,5 @@ import { Component } from "../Component"; -import { Entity, ComponentConstructor } from "../Entity"; +import { Entity } from "../Entity"; /** * @internal @@ -9,19 +9,18 @@ export class CloneUtils { private static _tempRemapPath: number[] = []; static remapEntity(srcRoot: Entity, targetRoot: Entity, entity: Entity): Entity { - const paths = CloneUtils._tempRemapPath; - const success = CloneUtils._getEntityHierarchyPath(srcRoot, entity, paths); - return success ? CloneUtils._getEntityByHierarchyPath(targetRoot, paths) : entity; + const path = CloneUtils._tempRemapPath; + if (!CloneUtils._getEntityHierarchyPath(srcRoot, entity, path)) return entity; + return CloneUtils._getEntityByHierarchyPath(targetRoot, path); } static remapComponent(srcRoot: Entity, targetRoot: Entity, component: T): T { - const paths = CloneUtils._tempRemapPath; - const success = CloneUtils._getEntityHierarchyPath(srcRoot, component.entity, paths); - return success - ? (CloneUtils._getEntityByHierarchyPath(targetRoot, paths)?.getComponent( - >component.constructor - ) as T) - : component; + const path = CloneUtils._tempRemapPath; + const srcEntity = component.entity; + if (!CloneUtils._getEntityHierarchyPath(srcRoot, srcEntity, path)) return component; + return CloneUtils._getEntityByHierarchyPath(targetRoot, path)._components[ + srcEntity._components.indexOf(component) + ] as T; } private static _getEntityHierarchyPath(rootEntity: Entity, searchEntity: Entity, inversePath: number[]): boolean { diff --git a/packages/core/src/clone/ComponentCloner.ts b/packages/core/src/clone/ComponentCloner.ts index a3124ee5f4..f4e1b11651 100644 --- a/packages/core/src/clone/ComponentCloner.ts +++ b/packages/core/src/clone/ComponentCloner.ts @@ -9,18 +9,15 @@ export interface ICustomClone { /** * @internal */ - _cloneTo?(target: ICustomClone): void; + _remap?(srcRoot: Entity, targetRoot: Entity): Object; /** * @internal */ - copyFrom?(source: ICustomClone): void; -} - -export interface IComponentCustomClone { + _cloneTo?(target: ICustomClone, srcRoot?: Entity, targetRoot?: Entity): void; /** * @internal */ - _cloneTo(target: IComponentCustomClone, srcRoot: Entity, targetRoot: Entity): void; + copyFrom?(source: ICustomClone): void; } export class ComponentCloner { @@ -37,17 +34,9 @@ export class ComponentCloner { deepInstanceMap: Map ): void { const cloneModes = CloneManager.getCloneMode(source.constructor); - for (let k in source) { CloneManager.cloneProperty(source, target, k, cloneModes[k], srcRoot, targetRoot, deepInstanceMap); } - - if (((source as unknown))._cloneTo) { - ((source as unknown))._cloneTo( - (target as unknown), - srcRoot, - targetRoot - ); - } + ((source as unknown))._cloneTo?.(target, srcRoot, targetRoot); } } diff --git a/packages/core/src/graphic/Buffer.ts b/packages/core/src/graphic/Buffer.ts index 3a8cd911e1..a5969a5cb5 100644 --- a/packages/core/src/graphic/Buffer.ts +++ b/packages/core/src/graphic/Buffer.ts @@ -13,11 +13,12 @@ import { SetDataOptions } from "./enums/SetDataOptions"; export class Buffer extends GraphicsResource { /** @internal */ _dataUpdateManager: UpdateFlagManager = new UpdateFlagManager(); + /** @internal */ + _platformBuffer: IPlatformBuffer; private _type: BufferBindFlag; private _byteLength: number; private _bufferUsage: BufferUsage; - private _platformBuffer: IPlatformBuffer; private _readable: boolean; private _data: Uint8Array; @@ -227,6 +228,17 @@ export class Buffer extends GraphicsResource { this._platformBuffer.getData(data, bufferByteOffset, dataOffset, dataLength); } + /** + * Copy data from another buffer on the GPU. + * @param srcBuffer - Source buffer + * @param srcByteOffset - Byte offset in the source buffer + * @param dstByteOffset - Byte offset in this buffer + * @param byteLength - Number of bytes to copy + */ + copyFromBuffer(srcBuffer: Buffer, srcByteOffset: number, dstByteOffset: number, byteLength: number): void { + this._platformBuffer.copyFromBuffer(srcBuffer._platformBuffer, srcByteOffset, dstByteOffset, byteLength); + } + /** * Mark buffer as readable, the `data` property will be not accessible anymore. */ diff --git a/packages/core/src/graphic/TransformFeedback.ts b/packages/core/src/graphic/TransformFeedback.ts new file mode 100644 index 0000000000..1903ec1cfd --- /dev/null +++ b/packages/core/src/graphic/TransformFeedback.ts @@ -0,0 +1,76 @@ +import { GraphicsResource } from "../asset/GraphicsResource"; +import { Engine } from "../Engine"; +import { IPlatformTransformFeedback } from "../renderingHardwareInterface"; +import { Buffer } from "./Buffer"; +import { MeshTopology } from "./enums/MeshTopology"; + +/** + * Transform Feedback object for GPU-based data capture. + * @internal + */ +export class TransformFeedback extends GraphicsResource { + /** @internal */ + _platformTransformFeedback: IPlatformTransformFeedback; + + constructor(engine: Engine) { + super(engine); + this._platformTransformFeedback = engine._hardwareRenderer.createPlatformTransformFeedback(); + } + + /** + * Bind this Transform Feedback object as active. + */ + bind(): void { + this._platformTransformFeedback.bind(); + } + + /** + * Bind a buffer range as output at the given index. + * @param index - Output binding point index (corresponds to varying index in shader) + * @param buffer - Output buffer to capture data into + * @param byteOffset - Starting byte offset in the buffer + * @param byteSize - Size in bytes of the capture range + */ + bindBufferRange(index: number, buffer: Buffer, byteOffset: number, byteSize: number): void { + this._platformTransformFeedback.bindBufferRange(index, buffer._platformBuffer, byteOffset, byteSize); + } + + /** + * Begin a Transform Feedback pass. + * @param primitiveMode - Primitive topology mode + */ + begin(primitiveMode: MeshTopology): void { + this._platformTransformFeedback.begin(primitiveMode); + } + + /** + * End the current Transform Feedback pass. + */ + end(): void { + this._platformTransformFeedback.end(); + } + + /** + * Unbind the output buffer at the given index from the Transform Feedback target. + * @param index - Output binding point index + */ + unbindBuffer(index: number): void { + this._platformTransformFeedback.unbindBuffer(index); + } + + /** + * Unbind this Transform Feedback object. + */ + unbind(): void { + this._platformTransformFeedback.unbind(); + } + + override _rebuild(): void { + this._platformTransformFeedback = this._engine._hardwareRenderer.createPlatformTransformFeedback(); + } + + protected override _onDestroy(): void { + super._onDestroy(); + this._platformTransformFeedback.destroy(); + } +} diff --git a/packages/core/src/graphic/TransformFeedbackPrimitive.ts b/packages/core/src/graphic/TransformFeedbackPrimitive.ts new file mode 100644 index 0000000000..3089ac3c3b --- /dev/null +++ b/packages/core/src/graphic/TransformFeedbackPrimitive.ts @@ -0,0 +1,146 @@ +import { Engine } from "../Engine"; +import { GraphicsResource } from "../asset/GraphicsResource"; +import { IPlatformTransformFeedbackPrimitive } from "../renderingHardwareInterface"; +import { ShaderProgram } from "../shader/ShaderProgram"; +import { Buffer } from "./Buffer"; +import { BufferBindFlag } from "./enums/BufferBindFlag"; +import { BufferUsage } from "./enums/BufferUsage"; +import { MeshTopology } from "./enums/MeshTopology"; +import { TransformFeedback } from "./TransformFeedback"; +import { VertexBufferBinding } from "./VertexBufferBinding"; +import { VertexElement } from "./VertexElement"; + +/** + * @internal + * Primitive for Transform Feedback simulation with read/write buffer swapping. + */ +export class TransformFeedbackPrimitive extends GraphicsResource { + /** @internal */ + _platformPrimitive: IPlatformTransformFeedbackPrimitive; + + private _transformFeedback: TransformFeedback; + private _bindingA: VertexBufferBinding; + private _bindingB: VertexBufferBinding; + private _byteStride: number; + private _readIsA = true; + + /** + * The current read buffer binding. + */ + get readBinding(): VertexBufferBinding { + return this._readIsA ? this._bindingA : this._bindingB; + } + + /** + * The current write buffer binding. + */ + get writeBinding(): VertexBufferBinding { + return this._readIsA ? this._bindingB : this._bindingA; + } + + /** + * @param engine - Engine instance + * @param byteStride - Bytes per vertex + */ + constructor(engine: Engine, byteStride: number) { + super(engine); + this._byteStride = byteStride; + this._transformFeedback = new TransformFeedback(engine); + this._transformFeedback.isGCIgnored = true; + this._platformPrimitive = engine._hardwareRenderer.createPlatformTransformFeedbackPrimitive(); + this.isGCIgnored = true; + } + + /** + * Resize read and write buffers. + * @param vertexCount - Number of vertices to allocate + */ + resize(vertexCount: number): void { + const byteLength = this._byteStride * vertexCount; + const bufferA = new Buffer(this._engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); + bufferA.isGCIgnored = true; + const bufferB = new Buffer(this._engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); + bufferB.isGCIgnored = true; + + this._bindingA = new VertexBufferBinding(bufferA, this._byteStride); + this._bindingB = new VertexBufferBinding(bufferB, this._byteStride); + this._readIsA = true; + this._platformPrimitive.invalidate(); + } + + /** + * Update vertex layout, only rebuilds when program changes. + * @param program - Shader program for attribute locations + * @param feedbackElements - Vertex elements describing the read/write buffer + * @param inputBinding - Additional input buffer binding + * @param inputElements - Vertex elements describing the input buffer + */ + updateVertexLayout( + program: ShaderProgram, + feedbackElements: VertexElement[], + inputBinding: VertexBufferBinding, + inputElements: VertexElement[] + ): void { + this._platformPrimitive.updateVertexLayout( + program, + this._bindingA, + this._bindingB, + feedbackElements, + inputBinding, + inputElements + ); + } + + /** + * Bind state before issuing draw calls. + */ + beginDraw(): void { + this._engine._hardwareRenderer.enableRasterizerDiscard(); + this._transformFeedback.bind(); + this._platformPrimitive.bind(this._readIsA); + } + + /** + * Issue a draw call for a vertex range, capturing output to the write buffer. + * @param mode - Primitive topology + * @param first - First vertex index + * @param count - Number of vertices + */ + draw(mode: MeshTopology, first: number, count: number): void { + const transformFeedback = this._transformFeedback; + transformFeedback.bindBufferRange(0, this.writeBinding.buffer, first * this._byteStride, count * this._byteStride); + transformFeedback.begin(mode); + this._platformPrimitive.draw(mode, first, count); + transformFeedback.end(); + } + + /** + * Unbind state after draw calls. + */ + endDraw(): void { + this._platformPrimitive.unbind(); + this._transformFeedback.unbindBuffer(0); + this._transformFeedback.unbind(); + this._engine._hardwareRenderer.disableRasterizerDiscard(); + this._engine._hardwareRenderer.invalidateShaderProgramState(); + } + + /** + * Swap read and write buffers. + */ + swap(): void { + this._readIsA = !this._readIsA; + } + + override _rebuild(): void { + this._platformPrimitive = this._engine._hardwareRenderer.createPlatformTransformFeedbackPrimitive(); + } + + protected override _onDestroy(): void { + super._onDestroy(); + this._platformPrimitive?.destroy(); + this._bindingA?.buffer.destroy(); + this._bindingB?.buffer.destroy(); + this._transformFeedback?.destroy(); + } +} diff --git a/packages/core/src/graphic/TransformFeedbackSimulator.ts b/packages/core/src/graphic/TransformFeedbackSimulator.ts new file mode 100644 index 0000000000..189d1a7337 --- /dev/null +++ b/packages/core/src/graphic/TransformFeedbackSimulator.ts @@ -0,0 +1,147 @@ +import { Engine } from "../Engine"; +import { MeshTopology } from "./enums/MeshTopology"; +import { TransformFeedbackPrimitive } from "./TransformFeedbackPrimitive"; +import { VertexBufferBinding } from "./VertexBufferBinding"; +import { VertexElement } from "./VertexElement"; +import { ShaderFactory } from "../shaderlib/ShaderFactory"; +import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { ShaderProgram } from "../shader/ShaderProgram"; +import { ShaderPass } from "../shader/ShaderPass"; +import { ShaderData } from "../shader/ShaderData"; +import { Logger } from "../base/Logger"; + +/** + * @internal + * General-purpose Transform Feedback simulator. + * Manages shader compilation, program caching, and per-frame simulation. + */ +export class TransformFeedbackSimulator { + private _engine: Engine; + private _primitive: TransformFeedbackPrimitive; + private _simulatorId: number; + private _vertexSource: string; + private _fragmentSource: string; + private _feedbackVaryings: string[]; + + /** + * The current read buffer binding. + */ + get readBinding(): VertexBufferBinding { + return this._primitive.readBinding; + } + + /** + * The current write buffer binding. + */ + get writeBinding(): VertexBufferBinding { + return this._primitive.writeBinding; + } + + /** + * @param engine - Engine instance + * @param byteStride - Bytes per vertex in the feedback buffer + * @param vertexSource - Vertex shader source (may contain #include) + * @param fragmentSource - Fragment shader source + * @param feedbackVaryings - Transform Feedback varying names + */ + constructor( + engine: Engine, + byteStride: number, + vertexSource: string, + fragmentSource: string, + feedbackVaryings: string[] + ) { + this._engine = engine; + this._primitive = new TransformFeedbackPrimitive(engine, byteStride); + this._simulatorId = ShaderPass._shaderPassCounter++; + this._vertexSource = vertexSource; + this._fragmentSource = fragmentSource; + this._feedbackVaryings = feedbackVaryings; + } + + /** + * Resize feedback buffers. + * @param vertexCount - Number of vertices to allocate + */ + resize(vertexCount: number): void { + this._primitive.resize(vertexCount); + } + + /** + * Begin a simulation step: compile/cache program, bind, upload uniforms, update layout. + * @param shaderData - Shader data with current macros and uniforms + * @param feedbackElements - Vertex elements for the feedback buffer + * @param inputBinding - Input buffer binding + * @param inputElements - Vertex elements for the input buffer + */ + beginUpdate( + shaderData: ShaderData, + feedbackElements: VertexElement[], + inputBinding: VertexBufferBinding, + inputElements: VertexElement[] + ): boolean { + const primitive = this._primitive; + const pool = this._engine._getShaderProgramPool(this._simulatorId); + + let program = pool.get(shaderData._macroCollection); + if (!program) { + program = this._compileProgram(shaderData._macroCollection); + if (!program) return false; + pool.cache(program); + } + + program.bind(); + program.uploadUniforms(program.rendererUniformBlock, shaderData); + program.uploadUniforms(program.otherUniformBlock, shaderData); + + primitive.updateVertexLayout(program, feedbackElements, inputBinding, inputElements); + primitive.beginDraw(); + return true; + } + + /** + * Issue a draw call for a vertex range. + * @param mode - Primitive topology + * @param first - First vertex index + * @param count - Number of vertices + */ + draw(mode: MeshTopology, first: number, count: number): void { + this._primitive.draw(mode, first, count); + } + + /** + * End the simulation step: unbind state and swap buffers. + */ + endUpdate(): void { + this._primitive.endDraw(); + this._primitive.swap(); + } + + destroy(): void { + this._primitive?.destroy(); + const pool = this._engine._shaderProgramPools[this._simulatorId]; + if (pool) { + pool._destroy(); + delete this._engine._shaderProgramPools[this._simulatorId]; + } + } + + private _compileProgram(macroCollection: ShaderMacroCollection): ShaderProgram | null { + const engine = this._engine; + const { vertexSource, fragmentSource } = ShaderFactory.compilePlatformSource( + engine, + macroCollection, + this._vertexSource, + this._fragmentSource + ); + + const program = new ShaderProgram(engine, vertexSource, fragmentSource, this._feedbackVaryings); + + if (!program.isValid) { + Logger.error("TransformFeedbackSimulator: Failed to compile shader program."); + return null; + } + + return program; + } +} diff --git a/packages/core/src/graphic/enums/BufferBindFlag.ts b/packages/core/src/graphic/enums/BufferBindFlag.ts index 9696dc947e..152b9d2e54 100644 --- a/packages/core/src/graphic/enums/BufferBindFlag.ts +++ b/packages/core/src/graphic/enums/BufferBindFlag.ts @@ -2,8 +2,8 @@ * Buffer binding flag. */ export enum BufferBindFlag { - /** Vertex buffer binding flag */ + /** Vertex buffer binding flag. */ VertexBuffer, - /** Index buffer binding flag */ + /** Index buffer binding flag. */ IndexBuffer } diff --git a/packages/core/src/mesh/MeshRenderer.ts b/packages/core/src/mesh/MeshRenderer.ts index b749f7d4d8..e08e51ac4c 100644 --- a/packages/core/src/mesh/MeshRenderer.ts +++ b/packages/core/src/mesh/MeshRenderer.ts @@ -77,8 +77,8 @@ export class MeshRenderer extends Renderer { /** * @internal */ - override _cloneTo(target: MeshRenderer, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); + override _cloneTo(target: MeshRenderer): void { + super._cloneTo(target); target.mesh = this._mesh; } diff --git a/packages/core/src/mesh/Skin.ts b/packages/core/src/mesh/Skin.ts index f1ea809f55..7a9b3444b0 100644 --- a/packages/core/src/mesh/Skin.ts +++ b/packages/core/src/mesh/Skin.ts @@ -1,17 +1,15 @@ import { Matrix } from "@galacean/engine-math"; import { Entity } from "../Entity"; -import { CloneUtils } from "../clone/CloneUtils"; import { UpdateFlagManager } from "../UpdateFlagManager"; import { Utils } from "../Utils"; import { EngineObject } from "../base/EngineObject"; import { deepClone, ignoreClone } from "../clone/CloneManager"; -import { IComponentCustomClone } from "../clone/ComponentCloner"; import { SkinnedMeshRenderer } from "./SkinnedMeshRenderer"; /** * Skin used for skinned mesh renderer. */ -export class Skin extends EngineObject implements IComponentCustomClone { +export class Skin extends EngineObject { /** Inverse bind matrices. */ @deepClone inverseBindMatrices = new Array(); @@ -23,9 +21,8 @@ export class Skin extends EngineObject implements IComponentCustomClone { @ignoreClone _updatedManager = new UpdateFlagManager(); - @ignoreClone private _rootBone: Entity; - @ignoreClone + @deepClone private _bones = new Array(); @ignoreClone private _updateMark = -1; @@ -95,28 +92,6 @@ export class Skin extends EngineObject implements IComponentCustomClone { this._updateMark = renderer.engine.time.frameCount; } - /** - * @internal - */ - _cloneTo(target: Skin, srcRoot: Entity, targetRoot: Entity): void { - // Clone rootBone - const rootBone = this.rootBone; - if (rootBone) { - target.rootBone = CloneUtils.remapEntity(srcRoot, targetRoot, rootBone); - } - - // Clone bones - const bones = this.bones; - if (bones.length > 0) { - const boneCount = bones.length; - const destBones = new Array(boneCount); - for (let i = 0; i < boneCount; i++) { - destBones[i] = CloneUtils.remapEntity(srcRoot, targetRoot, bones[i]); - } - target.bones = destBones; - } - } - /** @deprecated Please use `bones` instead. */ public joints: string[] = []; diff --git a/packages/core/src/mesh/SkinnedMeshRenderer.ts b/packages/core/src/mesh/SkinnedMeshRenderer.ts index abd662409e..d8e83e16bb 100644 --- a/packages/core/src/mesh/SkinnedMeshRenderer.ts +++ b/packages/core/src/mesh/SkinnedMeshRenderer.ts @@ -136,8 +136,8 @@ export class SkinnedMeshRenderer extends MeshRenderer { /** * @internal */ - override _cloneTo(target: SkinnedMeshRenderer, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); + override _cloneTo(target: SkinnedMeshRenderer): void { + super._cloneTo(target); if (this.skin) { target._applySkin(null, target.skin); diff --git a/packages/core/src/particle/ParticleBufferUtils.ts b/packages/core/src/particle/ParticleBufferUtils.ts index d39b0f169b..55531139d7 100644 --- a/packages/core/src/particle/ParticleBufferUtils.ts +++ b/packages/core/src/particle/ParticleBufferUtils.ts @@ -9,12 +9,32 @@ import { BufferUsage } from "../graphic/enums/BufferUsage"; import { IndexFormat } from "../graphic/enums/IndexFormat"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { ParticleBillboardVertexAttribute } from "./enums/attributes/BillboardParticleVertexAttribute"; +import { ParticleFeedbackVertexAttribute } from "./enums/attributes/ParticleFeedbackVertexAttribute"; import { ParticleInstanceVertexAttribute } from "./enums/attributes/ParticleInstanceVertexAttribute"; /** * @internal */ export class ParticleBufferUtils { + static readonly feedbackVertexStride = 24; + + static readonly feedbackVertexElements = [ + new VertexElement(ParticleFeedbackVertexAttribute.Position, 0, VertexElementFormat.Vector3, 0), + new VertexElement(ParticleFeedbackVertexAttribute.Velocity, 12, VertexElementFormat.Vector3, 0) + ]; + + static readonly feedbackInstanceElements = [ + new VertexElement(ParticleInstanceVertexAttribute.ShapePositionStartLifeTime, 0, VertexElementFormat.Vector4, 0), + new VertexElement(ParticleInstanceVertexAttribute.DirectionTime, 16, VertexElementFormat.Vector4, 0), + new VertexElement(ParticleInstanceVertexAttribute.StartSize, 48, VertexElementFormat.Vector3, 0), + new VertexElement(ParticleInstanceVertexAttribute.StartSpeed, 72, VertexElementFormat.Float, 0), + new VertexElement(ParticleInstanceVertexAttribute.Random0, 76, VertexElementFormat.Vector4, 0), + new VertexElement(ParticleInstanceVertexAttribute.Random1, 92, VertexElementFormat.Vector4, 0), + new VertexElement(ParticleInstanceVertexAttribute.SimulationWorldPosition, 108, VertexElementFormat.Vector3, 0), + new VertexElement(ParticleInstanceVertexAttribute.SimulationWorldRotation, 120, VertexElementFormat.Vector4, 0), + new VertexElement(ParticleInstanceVertexAttribute.Random2, 152, VertexElementFormat.Vector4, 0) + ]; + static readonly instanceVertexStride = 168; static readonly instanceVertexFloatStride = ParticleBufferUtils.instanceVertexStride / 4; diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index 28fd23c1cc..d3c7ea7225 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -10,19 +10,24 @@ import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { SetDataOptions } from "../graphic/enums/SetDataOptions"; +import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { MeshRenderer, VertexAttribute } from "../mesh"; import { ShaderData } from "../shader"; +import { ShaderMacro } from "../shader/ShaderMacro"; import { Buffer } from "./../graphic/Buffer"; import { ParticleBufferUtils } from "./ParticleBufferUtils"; import { ParticleRenderer, ParticleUpdateFlags } from "./ParticleRenderer"; +import { ParticleTransformFeedbackSimulator } from "./ParticleTransformFeedbackSimulator"; import { ParticleCurveMode } from "./enums/ParticleCurveMode"; import { ParticleGradientMode } from "./enums/ParticleGradientMode"; import { ParticleRenderMode } from "./enums/ParticleRenderMode"; import { ParticleSimulationSpace } from "./enums/ParticleSimulationSpace"; import { ParticleStopMode } from "./enums/ParticleStopMode"; +import { ParticleFeedbackVertexAttribute } from "./enums/attributes/ParticleFeedbackVertexAttribute"; import { ColorOverLifetimeModule } from "./modules/ColorOverLifetimeModule"; import { EmissionModule } from "./modules/EmissionModule"; import { ForceOverLifetimeModule } from "./modules/ForceOverLifetimeModule"; +import { LimitVelocityOverLifetimeModule } from "./modules/LimitVelocityOverLifetimeModule"; import { MainModule } from "./modules/MainModule"; import { ParticleCompositeCurve } from "./modules/ParticleCompositeCurve"; import { RotationOverLifetimeModule } from "./modules/RotationOverLifetimeModule"; @@ -39,12 +44,14 @@ export class ParticleGenerator { private static _tempVector22 = new Vector2(); private static _tempVector30 = new Vector3(); private static _tempVector31 = new Vector3(); + private static _tempVector32 = new Vector3(); private static _tempMat = new Matrix(); private static _tempColor0 = new Color(); private static _tempParticleRenderers = new Array(); private static readonly _particleIncreaseCount = 128; private static readonly _transformedBoundsIncreaseCount = 16; + private static readonly _transformFeedbackMacro = ShaderMacro.getByName("RENDERER_TRANSFORM_FEEDBACK"); /** Use auto random seed. */ useAutoRandomSeed = true; @@ -61,6 +68,9 @@ export class ParticleGenerator { /** Force over lifetime module. */ @deepClone readonly forceOverLifetime: ForceOverLifetimeModule; + /** Limit velocity over lifetime module. */ + @deepClone + readonly limitVelocityOverLifetime: LimitVelocityOverLifetimeModule; /** Size over lifetime module. */ @deepClone readonly sizeOverLifetime: SizeOverLifetimeModule; @@ -102,9 +112,18 @@ export class ParticleGenerator { @ignoreClone _subPrimitive = new SubMesh(0, 0, MeshTopology.Triangles); /** @internal */ - @ignoreClone readonly _renderer: ParticleRenderer; + /** @internal */ + @ignoreClone + _feedbackSimulator: ParticleTransformFeedbackSimulator; + /** @internal */ + @ignoreClone + _useTransformFeedback = false; + /** @internal */ + @ignoreClone + private _feedbackBindingIndex = -1; + @ignoreClone private _isPlaying = false; @ignoreClone @@ -170,6 +189,7 @@ export class ParticleGenerator { this.velocityOverLifetime = new VelocityOverLifetimeModule(this); this.forceOverLifetime = new ForceOverLifetimeModule(this); this.sizeOverLifetime = new SizeOverLifetimeModule(this); + this.limitVelocityOverLifetime = new LimitVelocityOverLifetimeModule(this); this.emission.enabled = true; } @@ -347,6 +367,23 @@ export class ParticleGenerator { } } + /** + * @internal + * Run Transform Feedback simulation pass. + */ + _updateFeedback(shaderData: ShaderData, deltaTime: number): void { + this._feedbackSimulator.update( + shaderData, + this._currentParticleCount, + this._firstActiveElement, + this._firstFreeElement, + deltaTime + ); + + // After swap, update the render pass buffer binding to point to the latest output + this._primitive.vertexBufferBindings[this._feedbackBindingIndex] = this._feedbackSimulator.readBinding; + } + /** * @internal */ @@ -416,6 +453,32 @@ export class ParticleGenerator { vertexBufferBindings.push(this._instanceVertexBufferBinding); } + // Add feedback buffer binding for render pass + if (this._useTransformFeedback) { + this._feedbackBindingIndex = vertexBufferBindings.length; + primitive.addVertexElement( + new VertexElement( + ParticleFeedbackVertexAttribute.Position, + 0, + VertexElementFormat.Vector3, + this._feedbackBindingIndex, + 1 + ) + ); + primitive.addVertexElement( + new VertexElement( + ParticleFeedbackVertexAttribute.Velocity, + 12, + VertexElementFormat.Vector3, + this._feedbackBindingIndex, + 1 + ) + ); + vertexBufferBindings.push(this._feedbackSimulator.readBinding); + } else { + this._feedbackBindingIndex = -1; + } + primitive.setVertexBufferBindings(vertexBufferBindings); } @@ -441,25 +504,37 @@ export class ParticleGenerator { const vertexBufferBindings = this._primitive.vertexBufferBindings; const vertexBufferBinding = new VertexBufferBinding(vertexInstanceBuffer, stride); + const lastInstanceVertices = this._instanceVertices; + const useFeedback = this._useTransformFeedback; + const instanceVertices = new Float32Array(newByteLength / 4); + if (useFeedback) { + this._feedbackSimulator.resize(newParticleCount, vertexBufferBinding); + } - const lastInstanceVertices = this._instanceVertices; if (lastInstanceVertices) { - const floatStride = ParticleBufferUtils.instanceVertexFloatStride; - + const { instanceVertexFloatStride: floatStride, feedbackVertexStride } = ParticleBufferUtils; const firstFreeElement = this._firstFreeElement; const firstRetiredElement = this._firstRetiredElement; + if (isIncrease) { + // Copy front segment [0, firstFreeElement) instanceVertices.set(new Float32Array(lastInstanceVertices.buffer, 0, firstFreeElement * floatStride)); + // Copy tail segment shifted by increaseCount const nextFreeElement = firstFreeElement + 1; - const freeEndOffset = (nextFreeElement + increaseCount) * floatStride; + const tailCount = this._currentParticleCount - nextFreeElement; + const tailDstElement = nextFreeElement + increaseCount; instanceVertices.set( new Float32Array(lastInstanceVertices.buffer, nextFreeElement * floatStride * 4), - freeEndOffset + tailDstElement * floatStride ); - // Maintain expanded pointers + if (useFeedback) { + this._feedbackSimulator.copyOldBufferData(0, 0, firstFreeElement * feedbackVertexStride); + this._feedbackSimulator.copyOldBufferData(nextFreeElement * feedbackVertexStride, tailDstElement * feedbackVertexStride, tailCount * feedbackVertexStride); + } + this._firstNewElement > firstFreeElement && (this._firstNewElement += increaseCount); this._firstActiveElement > firstFreeElement && (this._firstActiveElement += increaseCount); firstRetiredElement > firstFreeElement && (this._firstRetiredElement += increaseCount); @@ -468,8 +543,6 @@ export class ParticleGenerator { if (firstRetiredElement <= firstFreeElement) { migrateCount = firstFreeElement - firstRetiredElement; bufferOffset = 0; - - // Maintain expanded pointers this._firstFreeElement -= firstRetiredElement; this._firstNewElement -= firstRetiredElement; this._firstActiveElement -= firstRetiredElement; @@ -477,8 +550,6 @@ export class ParticleGenerator { } else { migrateCount = this._currentParticleCount - firstRetiredElement; bufferOffset = firstFreeElement; - - // Maintain expanded pointers this._firstNewElement > firstFreeElement && (this._firstNewElement -= firstFreeElement); this._firstActiveElement > firstFreeElement && (this._firstActiveElement -= firstFreeElement); firstRetiredElement > firstFreeElement && (this._firstRetiredElement -= firstFreeElement); @@ -492,19 +563,35 @@ export class ParticleGenerator { ), bufferOffset * floatStride ); + + if (useFeedback) { + this._feedbackSimulator.copyOldBufferData( + firstRetiredElement * feedbackVertexStride, + bufferOffset * feedbackVertexStride, + migrateCount * feedbackVertexStride + ); + } } + if (useFeedback) { + this._feedbackSimulator.destroyOldBuffers(); + } this._instanceBufferResized = true; } - // Instance buffer always at last - this._primitive.setVertexBufferBinding( - lastInstanceVertices ? vertexBufferBindings.length - 1 : vertexBufferBindings.length, - vertexBufferBinding - ); + + // Update instance buffer binding + const instanceBindingIndex = lastInstanceVertices + ? vertexBufferBindings.length - 1 - (useFeedback ? 1 : 0) + : vertexBufferBindings.length; + this._primitive.setVertexBufferBinding(instanceBindingIndex, vertexBufferBinding); this._instanceVertices = instanceVertices; this._instanceVertexBufferBinding = vertexBufferBinding; this._currentParticleCount = newParticleCount; + + if (useFeedback) { + this._primitive.setVertexBufferBinding(this._feedbackBindingIndex, this._feedbackSimulator.readBinding); + } } /** @@ -514,6 +601,7 @@ export class ParticleGenerator { this.main._updateShaderData(shaderData); this.velocityOverLifetime._updateShaderData(shaderData); this.forceOverLifetime._updateShaderData(shaderData); + this.limitVelocityOverLifetime._updateShaderData(shaderData); this.textureSheetAnimation._updateShaderData(shaderData); this.sizeOverLifetime._updateShaderData(shaderData); this.rotationOverLifetime._updateShaderData(shaderData); @@ -530,10 +618,30 @@ export class ParticleGenerator { this.textureSheetAnimation._resetRandomSeed(seed); this.velocityOverLifetime._resetRandomSeed(seed); this.forceOverLifetime._resetRandomSeed(seed); + this.limitVelocityOverLifetime._resetRandomSeed(seed); this.rotationOverLifetime._resetRandomSeed(seed); this.colorOverLifetime._resetRandomSeed(seed); } + /** + * @internal + */ + _setTransformFeedback(enabled: boolean): void { + this._useTransformFeedback = enabled; + + if (enabled) { + if (!this._feedbackSimulator) { + this._feedbackSimulator = new ParticleTransformFeedbackSimulator(this._renderer.engine); + } + this._feedbackSimulator.resize(this._currentParticleCount, this._instanceVertexBufferBinding); + this._renderer.shaderData.enableMacro(ParticleGenerator._transformFeedbackMacro); + } else { + this._renderer.shaderData.disableMacro(ParticleGenerator._transformFeedbackMacro); + } + + this._reorganizeGeometryBuffers(); + } + /** * @internal */ @@ -571,6 +679,7 @@ export class ParticleGenerator { this._instanceVertexBufferBinding.buffer.destroy(); this._primitive.destroy(); this.emission._destroy(); + this._feedbackSimulator?.destroy(); } /** @@ -869,9 +978,44 @@ export class ParticleGenerator { instanceVertices[offset + 40] = rand.random(); } + const { limitVelocityOverLifetime } = this; + if (limitVelocityOverLifetime.enabled && limitVelocityOverLifetime._isRandomMode()) { + instanceVertices[offset + 41] = limitVelocityOverLifetime._limitRand.random(); + } + + // Initialize feedback buffer for this particle + if (this._useTransformFeedback) { + this._addFeedbackParticle(firstFreeElement, position, direction, startSpeed, transform); + } + this._firstFreeElement = nextFreeElement; } + private _addFeedbackParticle( + index: number, + shapePosition: Vector3, + direction: Vector3, + startSpeed: number, + transform: Transform + ): void { + let position: Vector3; + if (this.main.simulationSpace === ParticleSimulationSpace.Local) { + position = shapePosition; + } else { + position = ParticleGenerator._tempVector32; + Vector3.transformByQuat(shapePosition, transform.worldRotationQuaternion, position); + position.add(transform.worldPosition); + } + + this._feedbackSimulator.writeParticleData( + index, + position, + direction.x * startSpeed, + direction.y * startSpeed, + direction.z * startSpeed + ); + } + private _retireActiveParticles(): void { const engine = this._renderer.engine; @@ -929,24 +1073,37 @@ export class ParticleGenerator { } const byteStride = ParticleBufferUtils.instanceVertexStride; - const start = firstActiveElement * byteStride; const instanceBuffer = this._instanceVertexBufferBinding.buffer; const dataBuffer = this._instanceVertices.buffer; + // Feedback mode: upload in-place (indices match feedback buffer slots) + // Non-feedback mode: compact to GPU offset 0 + const compact = !this._useTransformFeedback; + const start = firstActiveElement * byteStride; if (firstActiveElement < firstFreeElement) { instanceBuffer.setData( - dataBuffer, - 0, + dataBuffer as ArrayBuffer, + compact ? 0 : start, start, (firstFreeElement - firstActiveElement) * byteStride, SetDataOptions.Discard ); } else { - const firstSegmentCount = (this._currentParticleCount - firstActiveElement) * byteStride; - instanceBuffer.setData(dataBuffer, 0, start, firstSegmentCount, SetDataOptions.Discard); - + const firstSegmentSize = (this._currentParticleCount - firstActiveElement) * byteStride; + instanceBuffer.setData( + dataBuffer as ArrayBuffer, + compact ? 0 : start, + start, + firstSegmentSize, + SetDataOptions.Discard + ); if (firstFreeElement > 0) { - instanceBuffer.setData(dataBuffer, firstSegmentCount, 0, firstFreeElement * byteStride); + instanceBuffer.setData( + dataBuffer as ArrayBuffer, + compact ? firstSegmentSize : 0, + 0, + firstFreeElement * byteStride + ); } } this._firstNewElement = firstFreeElement; diff --git a/packages/core/src/particle/ParticleRenderer.ts b/packages/core/src/particle/ParticleRenderer.ts index f2a7082e55..47fa9ab963 100644 --- a/packages/core/src/particle/ParticleRenderer.ts +++ b/packages/core/src/particle/ParticleRenderer.ts @@ -218,6 +218,11 @@ export class ParticleRenderer extends Renderer { shaderData.setVector3(ParticleRenderer._pivotOffsetProperty, this.pivot); this.generator._updateShaderData(shaderData); + + // Run Transform Feedback simulation after shader data is up to date + if (generator._useTransformFeedback) { + generator._updateFeedback(shaderData, this.engine.time.deltaTime); + } } protected override _render(context: RenderContext): void { @@ -228,8 +233,11 @@ export class ParticleRenderer extends Renderer { if (!aliveParticleCount) { return; } - - generator._primitive.instanceCount = aliveParticleCount; + // Transform Feedback: render all slots (instance buffer not compacted, dead particles discarded in shader) + // Non-Transform Feedback: render only alive particles (instance buffer compacted) + generator._primitive.instanceCount = generator._useTransformFeedback + ? generator._currentParticleCount + : aliveParticleCount; let material = this.getMaterial(); if (!material || (this._renderMode === ParticleRenderMode.Mesh && !this._mesh)) { diff --git a/packages/core/src/particle/ParticleTransformFeedbackSimulator.ts b/packages/core/src/particle/ParticleTransformFeedbackSimulator.ts new file mode 100644 index 0000000000..a566c1026c --- /dev/null +++ b/packages/core/src/particle/ParticleTransformFeedbackSimulator.ts @@ -0,0 +1,134 @@ +import { Buffer } from "../graphic/Buffer"; +import { MeshTopology } from "../graphic/enums/MeshTopology"; +import { TransformFeedbackSimulator } from "../graphic/TransformFeedbackSimulator"; +import { VertexBufferBinding } from "../graphic/VertexBufferBinding"; +import { ShaderData } from "../shader/ShaderData"; +import { ShaderProperty } from "../shader/ShaderProperty"; +import { Vector3 } from "@galacean/engine-math"; +import { Engine } from "../Engine"; +import { ParticleBufferUtils } from "./ParticleBufferUtils"; + +/** + * @internal + * Particle-specific Transform Feedback simulation. + */ +export class ParticleTransformFeedbackSimulator { + private static readonly _deltaTimeProperty = ShaderProperty.getByName("renderer_DeltaTime"); + + private _simulator: TransformFeedbackSimulator; + private _particleInitData = new Float32Array(6); + private _instanceBinding: VertexBufferBinding; + private _oldReadBuffer: Buffer; + private _oldWriteBuffer: Buffer; + + /** + * The current read buffer binding for the render pass. + */ + get readBinding(): VertexBufferBinding { + return this._simulator.readBinding; + } + + constructor(engine: Engine) { + this._simulator = new TransformFeedbackSimulator( + engine, + ParticleBufferUtils.feedbackVertexStride, + `#include `, + `void main() { discard; }`, + ["v_FeedbackPosition", "v_FeedbackVelocity"] + ); + } + + /** + * Resize feedback buffers. + * Saves pre-resize buffers internally for subsequent `copyOldBufferData` / `destroyOldBuffers` calls. + * @param particleCount - Number of particles to allocate + * @param instanceBinding - New instance vertex buffer binding + */ + resize(particleCount: number, instanceBinding: VertexBufferBinding): void { + this._oldReadBuffer = this._simulator.readBinding?.buffer; + this._oldWriteBuffer = this._simulator.writeBinding?.buffer; + this._simulator.resize(particleCount); + this._instanceBinding = instanceBinding; + } + + /** + * Write initial position and velocity for a newly emitted particle. + */ + writeParticleData(index: number, position: Vector3, vx: number, vy: number, vz: number): void { + const data = this._particleInitData; + data[0] = position.x; + data[1] = position.y; + data[2] = position.z; + data[3] = vx; + data[4] = vy; + data[5] = vz; + const simulator = this._simulator; + const byteOffset = index * ParticleBufferUtils.feedbackVertexStride; + simulator.readBinding.buffer.setData(data, byteOffset); + simulator.writeBinding.buffer.setData(data, byteOffset); + } + + /** + * Copy data from pre-resize buffers to current buffers. + * Must be called after `resize` which saves the old buffers. + */ + copyOldBufferData(srcByteOffset: number, dstByteOffset: number, byteLength: number): void { + this._simulator.readBinding.buffer.copyFromBuffer(this._oldReadBuffer, srcByteOffset, dstByteOffset, byteLength); + this._simulator.writeBinding.buffer.copyFromBuffer(this._oldWriteBuffer, srcByteOffset, dstByteOffset, byteLength); + } + + /** + * Destroy pre-resize buffers saved during `resize`. + */ + destroyOldBuffers(): void { + this._oldReadBuffer.destroy(); + this._oldWriteBuffer.destroy(); + this._oldReadBuffer = null; + this._oldWriteBuffer = null; + } + + /** + * Run one simulation step. + * @param shaderData - Shader data with current macros and uniforms + * @param particleCount - Total particle slot count + * @param firstActive - First active particle index in ring buffer + * @param firstFree - First free particle index in ring buffer + * @param deltaTime - Frame delta time + */ + update( + shaderData: ShaderData, + particleCount: number, + firstActive: number, + firstFree: number, + deltaTime: number + ): void { + if (firstActive === firstFree) return; + + shaderData.setFloat(ParticleTransformFeedbackSimulator._deltaTimeProperty, deltaTime); + + if ( + !this._simulator.beginUpdate( + shaderData, + ParticleBufferUtils.feedbackVertexElements, + this._instanceBinding, + ParticleBufferUtils.feedbackInstanceElements + ) + ) + return; + + if (firstActive < firstFree) { + this._simulator.draw(MeshTopology.Points, firstActive, firstFree - firstActive); + } else { + this._simulator.draw(MeshTopology.Points, firstActive, particleCount - firstActive); + if (firstFree > 0) { + this._simulator.draw(MeshTopology.Points, 0, firstFree); + } + } + + this._simulator.endUpdate(); + } + + destroy(): void { + this._simulator?.destroy(); + } +} diff --git a/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts b/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts index 160aed325a..2e344e8115 100644 --- a/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts +++ b/packages/core/src/particle/enums/ParticleRandomSubSeeds.ts @@ -17,5 +17,6 @@ export enum ParticleRandomSubSeeds { TextureSheetAnimation = 0xbc524e5, Shape = 0xaf502044, GravityModifier = 0xa47b8c4d, - ForceOverLifetime = 0xe6fb937c + ForceOverLifetime = 0xe6fb937c, + LimitVelocityOverLifetime = 0xb5a21f7e } diff --git a/packages/core/src/particle/enums/attributes/ParticleFeedbackVertexAttribute.ts b/packages/core/src/particle/enums/attributes/ParticleFeedbackVertexAttribute.ts new file mode 100644 index 0000000000..25ed30a416 --- /dev/null +++ b/packages/core/src/particle/enums/attributes/ParticleFeedbackVertexAttribute.ts @@ -0,0 +1,8 @@ +/** + * @internal + * Vertex attributes for the Transform Feedback buffer. + */ +export enum ParticleFeedbackVertexAttribute { + Position = "a_FeedbackPosition", + Velocity = "a_FeedbackVelocity" +} diff --git a/packages/core/src/particle/index.ts b/packages/core/src/particle/index.ts index fa115084fc..e43f1ae4ed 100644 --- a/packages/core/src/particle/index.ts +++ b/packages/core/src/particle/index.ts @@ -19,4 +19,5 @@ export { RotationOverLifetimeModule } from "./modules/RotationOverLifetimeModule export { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule"; export { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule"; export { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule"; +export { LimitVelocityOverLifetimeModule } from "./modules/LimitVelocityOverLifetimeModule"; export * from "./modules/shape/index"; diff --git a/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts new file mode 100644 index 0000000000..ca76827f0b --- /dev/null +++ b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts @@ -0,0 +1,456 @@ +import { Rand, Vector2, Vector3 } from "@galacean/engine-math"; +import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ShaderData, ShaderMacro } from "../../shader"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import { ParticleCurveMode } from "../enums/ParticleCurveMode"; +import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; +import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace"; +import { ParticleGenerator } from "../ParticleGenerator"; +import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; +import { ParticleGeneratorModule } from "./ParticleGeneratorModule"; + +/** + * Limit velocity over lifetime module. + */ +export class LimitVelocityOverLifetimeModule extends ParticleGeneratorModule { + static readonly _enabledMacro = ShaderMacro.getByName("RENDERER_LVL_MODULE_ENABLED"); + static readonly _separateAxesMacro = ShaderMacro.getByName("RENDERER_LVL_SEPARATE_AXES"); + static readonly _limitConstantModeMacro = ShaderMacro.getByName("RENDERER_LVL_LIMIT_CONSTANT_MODE"); + static readonly _limitCurveModeMacro = ShaderMacro.getByName("RENDERER_LVL_LIMIT_CURVE_MODE"); + static readonly _limitIsRandomMacro = ShaderMacro.getByName("RENDERER_LVL_LIMIT_IS_RANDOM_TWO"); + static readonly _dragCurveModeMacro = ShaderMacro.getByName("RENDERER_LVL_DRAG_CURVE_MODE"); + static readonly _dragIsRandomMacro = ShaderMacro.getByName("RENDERER_LVL_DRAG_IS_RANDOM_TWO"); + static readonly _multiplyDragBySizeMacro = ShaderMacro.getByName("RENDERER_LVL_DRAG_MULTIPLY_SIZE"); + static readonly _multiplyDragByVelocityMacro = ShaderMacro.getByName("RENDERER_LVL_DRAG_MULTIPLY_VELOCITY"); + + static readonly _limitMaxConstProperty = ShaderProperty.getByName("renderer_LVLLimitMaxConst"); + static readonly _limitMinConstProperty = ShaderProperty.getByName("renderer_LVLLimitMinConst"); + static readonly _limitMaxCurveProperty = ShaderProperty.getByName("renderer_LVLLimitMaxCurve"); + static readonly _limitMinCurveProperty = ShaderProperty.getByName("renderer_LVLLimitMinCurve"); + static readonly _limitMaxConstVecProperty = ShaderProperty.getByName("renderer_LVLLimitMaxConstVector"); + static readonly _limitMinConstVecProperty = ShaderProperty.getByName("renderer_LVLLimitMinConstVector"); + static readonly _limitXMaxCurveProperty = ShaderProperty.getByName("renderer_LVLLimitXMaxCurve"); + static readonly _limitXMinCurveProperty = ShaderProperty.getByName("renderer_LVLLimitXMinCurve"); + static readonly _limitYMaxCurveProperty = ShaderProperty.getByName("renderer_LVLLimitYMaxCurve"); + static readonly _limitYMinCurveProperty = ShaderProperty.getByName("renderer_LVLLimitYMinCurve"); + static readonly _limitZMaxCurveProperty = ShaderProperty.getByName("renderer_LVLLimitZMaxCurve"); + static readonly _limitZMinCurveProperty = ShaderProperty.getByName("renderer_LVLLimitZMinCurve"); + static readonly _dampenProperty = ShaderProperty.getByName("renderer_LVLDampen"); + static readonly _dragConstantProperty = ShaderProperty.getByName("renderer_LVLDragConstant"); + static readonly _dragMaxCurveProperty = ShaderProperty.getByName("renderer_LVLDragMaxCurve"); + static readonly _dragMinCurveProperty = ShaderProperty.getByName("renderer_LVLDragMinCurve"); + static readonly _spaceProperty = ShaderProperty.getByName("renderer_LVLSpace"); + + /** @internal */ + @ignoreClone + _limitRand = new Rand(0, ParticleRandomSubSeeds.LimitVelocityOverLifetime); + + @ignoreClone + private _limitMinConstantVec = new Vector3(); + @ignoreClone + private _limitMaxConstantVec = new Vector3(); + @ignoreClone + private _dragConstantVec = new Vector2(); + + @ignoreClone + private _enabledModuleMacro: ShaderMacro; + @ignoreClone + private _separateAxesCachedMacro: ShaderMacro; + @ignoreClone + private _limitModeMacro: ShaderMacro; + @ignoreClone + private _limitRandomMacro: ShaderMacro; + @ignoreClone + private _dragCurveCachedMacro: ShaderMacro; + @ignoreClone + private _dragRandomCachedMacro: ShaderMacro; + @ignoreClone + private _dragSizeMacro: ShaderMacro; + @ignoreClone + private _dragVelocityMacro: ShaderMacro; + + private _separateAxes = false; + @deepClone + private _limitX: ParticleCompositeCurve; + @deepClone + private _limitY: ParticleCompositeCurve; + @deepClone + private _limitZ: ParticleCompositeCurve; + private _dampen: number = 1; + @deepClone + private _drag: ParticleCompositeCurve; + private _multiplyDragByParticleSize = false; + private _multiplyDragByParticleVelocity = false; + private _space = ParticleSimulationSpace.Local; + + /** + * Whether to limit velocity on each axis separately. + */ + get separateAxes(): boolean { + return this._separateAxes; + } + + set separateAxes(value: boolean) { + if (value !== this._separateAxes) { + this._separateAxes = value; + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + /** + * Speed limit when separateAxes is false. + */ + get limit(): ParticleCompositeCurve { + return this._limitX; + } + + set limit(value: ParticleCompositeCurve) { + this.limitX = value; + } + + /** + * Speed limit for the x-axis (or overall limit when separateAxes is false). + */ + get limitX(): ParticleCompositeCurve { + return this._limitX; + } + + set limitX(value: ParticleCompositeCurve) { + const lastValue = this._limitX; + if (value !== lastValue) { + this._limitX = value; + this._onCompositeCurveChange(lastValue, value); + } + } + + /** + * Speed limit for the y-axis. + */ + get limitY(): ParticleCompositeCurve { + return this._limitY; + } + + set limitY(value: ParticleCompositeCurve) { + const lastValue = this._limitY; + if (value !== lastValue) { + this._limitY = value; + this._onCompositeCurveChange(lastValue, value); + } + } + + /** + * Speed limit for the z-axis. + */ + get limitZ(): ParticleCompositeCurve { + return this._limitZ; + } + + set limitZ(value: ParticleCompositeCurve) { + const lastValue = this._limitZ; + if (value !== lastValue) { + this._limitZ = value; + this._onCompositeCurveChange(lastValue, value); + } + } + + /** + * Controls how much the velocity is dampened when it exceeds the limit. + * @remarks Value is clamped to [0, 1]. 0 means no damping, 1 means full damping. + */ + get dampen(): number { + return this._dampen; + } + + set dampen(value: number) { + value = Math.max(0, Math.min(1, value)); + if (value !== this._dampen) { + this._dampen = value; + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + /** + * Controls the amount of drag applied to particle velocities. + */ + get drag(): ParticleCompositeCurve { + return this._drag; + } + + set drag(value: ParticleCompositeCurve) { + const lastValue = this._drag; + if (value !== lastValue) { + this._drag = value; + this._onCompositeCurveChange(lastValue, value); + } + } + + /** + * Adjust the amount of drag based on particle sizes. + */ + get multiplyDragByParticleSize(): boolean { + return this._multiplyDragByParticleSize; + } + + set multiplyDragByParticleSize(value: boolean) { + if (value !== this._multiplyDragByParticleSize) { + this._multiplyDragByParticleSize = value; + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + /** + * Adjust the amount of drag based on particle speeds. + */ + get multiplyDragByParticleVelocity(): boolean { + return this._multiplyDragByParticleVelocity; + } + + set multiplyDragByParticleVelocity(value: boolean) { + if (value !== this._multiplyDragByParticleVelocity) { + this._multiplyDragByParticleVelocity = value; + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + /** + * Specifies if the velocity limits are in local space or world space. + */ + get space(): ParticleSimulationSpace { + return this._space; + } + + set space(value: ParticleSimulationSpace) { + if (value !== this._space) { + this._space = value; + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + /** + * Specifies whether the module is enabled. + * @remarks This module requires WebGL2, On WebGL1, enabling will be silently ignored. + */ + override get enabled(): boolean { + return this._enabled; + } + + override set enabled(value: boolean) { + if (value !== this._enabled) { + if (value && !this._generator._renderer.engine._hardwareRenderer.isWebGL2) { + return; + } + this._enabled = value; + this._generator._setTransformFeedback(value); + this._generator._renderer._onGeneratorParamsChanged(); + } + } + + constructor(generator: ParticleGenerator) { + super(generator); + + this.limitX = new ParticleCompositeCurve(1); + this.limitY = new ParticleCompositeCurve(1); + this.limitZ = new ParticleCompositeCurve(1); + this.drag = new ParticleCompositeCurve(0); + } + + /** + * @internal + */ + _isRandomMode(): boolean { + if (this._separateAxes) { + return ( + (this._limitX.mode === ParticleCurveMode.TwoConstants || this._limitX.mode === ParticleCurveMode.TwoCurves) && + (this._limitY.mode === ParticleCurveMode.TwoConstants || this._limitY.mode === ParticleCurveMode.TwoCurves) && + (this._limitZ.mode === ParticleCurveMode.TwoConstants || this._limitZ.mode === ParticleCurveMode.TwoCurves) + ); + } + return this._limitX.mode === ParticleCurveMode.TwoConstants || this._limitX.mode === ParticleCurveMode.TwoCurves; + } + + /** + * @internal + */ + _updateShaderData(shaderData: ShaderData): void { + let enabledModuleMacro = null; + let separateAxesMacro = null; + let limitModeMacro = null; + let limitRandomMacro = null; + let dragCurveMacro = null; + let dragRandomMacro = null; + let dragSizeMacro = null; + let dragVelocityMacro = null; + + if (this.enabled) { + enabledModuleMacro = LimitVelocityOverLifetimeModule._enabledMacro; + + // Dampen + shaderData.setFloat(LimitVelocityOverLifetimeModule._dampenProperty, this._dampen); + + // Space + shaderData.setInt(LimitVelocityOverLifetimeModule._spaceProperty, this._space); + + // Limit + if (this._separateAxes) { + separateAxesMacro = LimitVelocityOverLifetimeModule._separateAxesMacro; + const result = this._uploadSeparateAxisLimits(shaderData); + limitModeMacro = result.modeMacro; + limitRandomMacro = result.randomMacro; + } else { + const result = this._uploadScalarLimit(shaderData); + limitModeMacro = result.modeMacro; + limitRandomMacro = result.randomMacro; + } + + // Drag + const dragResult = this._uploadDrag(shaderData); + dragCurveMacro = dragResult.curveMacro; + dragRandomMacro = dragResult.randomMacro; + + // Drag modifiers + if (this._multiplyDragByParticleSize) { + dragSizeMacro = LimitVelocityOverLifetimeModule._multiplyDragBySizeMacro; + } + if (this._multiplyDragByParticleVelocity) { + dragVelocityMacro = LimitVelocityOverLifetimeModule._multiplyDragByVelocityMacro; + } + } + + this._enabledModuleMacro = this._enableMacro(shaderData, this._enabledModuleMacro, enabledModuleMacro); + this._separateAxesCachedMacro = this._enableMacro(shaderData, this._separateAxesCachedMacro, separateAxesMacro); + this._limitModeMacro = this._enableMacro(shaderData, this._limitModeMacro, limitModeMacro); + this._limitRandomMacro = this._enableMacro(shaderData, this._limitRandomMacro, limitRandomMacro); + this._dragCurveCachedMacro = this._enableMacro(shaderData, this._dragCurveCachedMacro, dragCurveMacro); + this._dragRandomCachedMacro = this._enableMacro(shaderData, this._dragRandomCachedMacro, dragRandomMacro); + this._dragSizeMacro = this._enableMacro(shaderData, this._dragSizeMacro, dragSizeMacro); + this._dragVelocityMacro = this._enableMacro(shaderData, this._dragVelocityMacro, dragVelocityMacro); + } + + /** + * @internal + */ + _resetRandomSeed(seed: number): void { + this._limitRand.reset(seed, ParticleRandomSubSeeds.LimitVelocityOverLifetime); + } + + private _uploadScalarLimit(shaderData: ShaderData): { modeMacro: ShaderMacro; randomMacro: ShaderMacro } { + const limitX = this._limitX; + let modeMacro: ShaderMacro = null; + let randomMacro: ShaderMacro = null; + + const isRandomCurveMode = limitX.mode === ParticleCurveMode.TwoCurves; + if (isRandomCurveMode || limitX.mode === ParticleCurveMode.Curve) { + shaderData.setFloatArray(LimitVelocityOverLifetimeModule._limitMaxCurveProperty, limitX.curveMax._getTypeArray()); + modeMacro = LimitVelocityOverLifetimeModule._limitCurveModeMacro; + if (isRandomCurveMode) { + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitMinCurveProperty, + limitX.curveMin._getTypeArray() + ); + randomMacro = LimitVelocityOverLifetimeModule._limitIsRandomMacro; + } + } else { + shaderData.setFloat(LimitVelocityOverLifetimeModule._limitMaxConstProperty, limitX.constantMax); + modeMacro = LimitVelocityOverLifetimeModule._limitConstantModeMacro; + if (limitX.mode === ParticleCurveMode.TwoConstants) { + shaderData.setFloat(LimitVelocityOverLifetimeModule._limitMinConstProperty, limitX.constantMin); + randomMacro = LimitVelocityOverLifetimeModule._limitIsRandomMacro; + } + } + + return { modeMacro, randomMacro }; + } + + private _uploadSeparateAxisLimits(shaderData: ShaderData): { modeMacro: ShaderMacro; randomMacro: ShaderMacro } { + const limitX = this._limitX; + const limitY = this._limitY; + const limitZ = this._limitZ; + let modeMacro: ShaderMacro = null; + let randomMacro: ShaderMacro = null; + + const isRandomCurveMode = + limitX.mode === ParticleCurveMode.TwoCurves && + limitY.mode === ParticleCurveMode.TwoCurves && + limitZ.mode === ParticleCurveMode.TwoCurves; + + if ( + isRandomCurveMode || + (limitX.mode === ParticleCurveMode.Curve && + limitY.mode === ParticleCurveMode.Curve && + limitZ.mode === ParticleCurveMode.Curve) + ) { + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitXMaxCurveProperty, + limitX.curveMax._getTypeArray() + ); + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitYMaxCurveProperty, + limitY.curveMax._getTypeArray() + ); + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitZMaxCurveProperty, + limitZ.curveMax._getTypeArray() + ); + modeMacro = LimitVelocityOverLifetimeModule._limitCurveModeMacro; + if (isRandomCurveMode) { + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitXMinCurveProperty, + limitX.curveMin._getTypeArray() + ); + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitYMinCurveProperty, + limitY.curveMin._getTypeArray() + ); + shaderData.setFloatArray( + LimitVelocityOverLifetimeModule._limitZMinCurveProperty, + limitZ.curveMin._getTypeArray() + ); + randomMacro = LimitVelocityOverLifetimeModule._limitIsRandomMacro; + } + } else { + const constantMax = this._limitMaxConstantVec; + constantMax.set(limitX.constantMax, limitY.constantMax, limitZ.constantMax); + shaderData.setVector3(LimitVelocityOverLifetimeModule._limitMaxConstVecProperty, constantMax); + modeMacro = LimitVelocityOverLifetimeModule._limitConstantModeMacro; + + if ( + limitX.mode === ParticleCurveMode.TwoConstants && + limitY.mode === ParticleCurveMode.TwoConstants && + limitZ.mode === ParticleCurveMode.TwoConstants + ) { + const constantMin = this._limitMinConstantVec; + constantMin.set(limitX.constantMin, limitY.constantMin, limitZ.constantMin); + shaderData.setVector3(LimitVelocityOverLifetimeModule._limitMinConstVecProperty, constantMin); + randomMacro = LimitVelocityOverLifetimeModule._limitIsRandomMacro; + } + } + + return { modeMacro, randomMacro }; + } + + private _uploadDrag(shaderData: ShaderData): { curveMacro: ShaderMacro; randomMacro: ShaderMacro } { + const drag = this._drag; + let curveMacro: ShaderMacro = null; + let randomMacro: ShaderMacro = null; + + const isRandomCurveMode = drag.mode === ParticleCurveMode.TwoCurves; + if (isRandomCurveMode || drag.mode === ParticleCurveMode.Curve) { + shaderData.setFloatArray(LimitVelocityOverLifetimeModule._dragMaxCurveProperty, drag.curveMax._getTypeArray()); + curveMacro = LimitVelocityOverLifetimeModule._dragCurveModeMacro; + if (isRandomCurveMode) { + shaderData.setFloatArray(LimitVelocityOverLifetimeModule._dragMinCurveProperty, drag.curveMin._getTypeArray()); + randomMacro = LimitVelocityOverLifetimeModule._dragIsRandomMacro; + } + } else { + const dragVec = this._dragConstantVec; + if (drag.mode === ParticleCurveMode.TwoConstants) { + dragVec.set(drag.constantMin, drag.constantMax); + } else { + dragVec.set(drag.constantMax, drag.constantMax); + } + shaderData.setVector2(LimitVelocityOverLifetimeModule._dragConstantProperty, dragVec); + } + + return { curveMacro, randomMacro }; + } +} diff --git a/packages/core/src/particle/modules/shape/MeshShape.ts b/packages/core/src/particle/modules/shape/MeshShape.ts index 9af6df3004..cbdd66acfc 100644 --- a/packages/core/src/particle/modules/shape/MeshShape.ts +++ b/packages/core/src/particle/modules/shape/MeshShape.ts @@ -166,7 +166,7 @@ export class MeshShape extends BaseShape { /** * @internal */ - _cloneTo(target: MeshShape, _: Entity, __: Entity): void { + _cloneTo(target: MeshShape): void { target.mesh = this._mesh; } } diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 9489905606..2ac82319fd 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -14,7 +14,6 @@ export abstract class ColliderShape implements ICustomClone { private static _idGenerator: number = 0; /** @internal */ - @ignoreClone _collider: Collider; /** @internal */ @ignoreClone diff --git a/packages/core/src/renderingHardwareInterface/IPlatformBuffer.ts b/packages/core/src/renderingHardwareInterface/IPlatformBuffer.ts index b9dd7bac8a..f8caada6da 100644 --- a/packages/core/src/renderingHardwareInterface/IPlatformBuffer.ts +++ b/packages/core/src/renderingHardwareInterface/IPlatformBuffer.ts @@ -14,5 +14,7 @@ export interface IPlatformBuffer { getData(data: ArrayBufferView, bufferByteOffset?: number, dataOffset?: number, dataLength?: number): void; + copyFromBuffer(srcBuffer: IPlatformBuffer, srcByteOffset: number, dstByteOffset: number, byteLength: number): void; + destroy(): void; } diff --git a/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedback.ts b/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedback.ts new file mode 100644 index 0000000000..03bdeec667 --- /dev/null +++ b/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedback.ts @@ -0,0 +1,43 @@ +import { IPlatformBuffer } from "./IPlatformBuffer"; + +/** + * Platform interface for Transform Feedback operations. + * @internal + */ +export interface IPlatformTransformFeedback { + /** + * Bind a buffer range as Transform Feedback output at the given index. + */ + bindBufferRange(index: number, buffer: IPlatformBuffer, byteOffset: number, byteSize: number): void; + + /** + * Unbind buffer from Transform Feedback output at the given index. + */ + unbindBuffer(index: number): void; + + /** + * Begin a Transform Feedback pass. + * @param primitiveMode - The primitive mode (e.g., POINTS) + */ + begin(primitiveMode: number): void; + + /** + * End the current Transform Feedback pass. + */ + end(): void; + + /** + * Bind this Transform Feedback object as the active TF. + */ + bind(): void; + + /** + * Unbind the Transform Feedback object. + */ + unbind(): void; + + /** + * Destroy native resources. + */ + destroy(): void; +} diff --git a/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedbackPrimitive.ts b/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedbackPrimitive.ts new file mode 100644 index 0000000000..b196fb7b3d --- /dev/null +++ b/packages/core/src/renderingHardwareInterface/IPlatformTransformFeedbackPrimitive.ts @@ -0,0 +1,52 @@ +/** + * Platform interface for Transform Feedback primitive operations. + * @internal + */ +export interface IPlatformTransformFeedbackPrimitive { + /** + * Update vertex layout. Auto-rebuilds when program changes. + * @param program - Shader program (for attribute locations) + * @param readBinding - Current read buffer binding + * @param writeBinding - Current write buffer binding + * @param feedbackElements - Vertex elements for feedback buffer + * @param inputBinding - Input buffer binding + * @param inputElements - Vertex elements for input buffer + */ + updateVertexLayout( + program: any, + readBinding: any, + writeBinding: any, + feedbackElements: any[], + inputBinding: any, + inputElements: any[] + ): void; + + /** + * Bind attribute state for the given read direction. + * @param readIsA - Whether to use direction A as read + */ + bind(readIsA: boolean): void; + + /** + * Unbind attribute state. + */ + unbind(): void; + + /** + * Issue a draw call. + * @param mode - Primitive topology + * @param first - First vertex index + * @param count - Number of vertices + */ + draw(mode: number, first: number, count: number): void; + + /** + * Invalidate cached state, forcing rebuild on next updateVertexLayout. + */ + invalidate(): void; + + /** + * Destroy native resources. + */ + destroy(): void; +} diff --git a/packages/core/src/renderingHardwareInterface/index.ts b/packages/core/src/renderingHardwareInterface/index.ts index 6f3c324509..afb10b6adf 100644 --- a/packages/core/src/renderingHardwareInterface/index.ts +++ b/packages/core/src/renderingHardwareInterface/index.ts @@ -4,3 +4,5 @@ export type { IPlatformTexture } from "./IPlatformTexture"; export type { IPlatformTexture2D } from "./IPlatformTexture2D"; export type { IPlatformTexture2DArray } from "./IPlatformTexture2DArray"; export type { IPlatformTextureCube } from "./IPlatformTextureCube"; +export type { IPlatformTransformFeedback } from "./IPlatformTransformFeedback"; +export type { IPlatformTransformFeedbackPrimitive } from "./IPlatformTransformFeedbackPrimitive"; diff --git a/packages/core/src/shader/ShaderPass.ts b/packages/core/src/shader/ShaderPass.ts index a15da42b98..8c11182df7 100644 --- a/packages/core/src/shader/ShaderPass.ts +++ b/packages/core/src/shader/ShaderPass.ts @@ -26,7 +26,8 @@ const precisionStr = ` * Shader pass containing vertex and fragment source. */ export class ShaderPass extends ShaderPart { - private static _shaderPassCounter: number = 0; + /** @internal */ + static _shaderPassCounter: number = 0; /** @internal */ static _shaderRootPath = "shaders://root/"; @@ -109,7 +110,7 @@ export class ShaderPass extends ShaderPart { * @internal */ _getShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { - const shaderProgramPool = engine._getShaderProgramPool(this); + const shaderProgramPool = engine._getShaderProgramPool(this._shaderPassId, this._shaderProgramPools); let shaderProgram = shaderProgramPool.get(macroCollection); if (shaderProgram) { return shaderProgram; @@ -136,6 +137,21 @@ export class ShaderPass extends ShaderPart { } private _getCanonicalShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { + if (this._platformTarget != undefined) { + return this._getShaderLabProgram(engine, macroCollection); + } + + const { vertexSource, fragmentSource } = ShaderFactory.compilePlatformSource( + engine, + macroCollection, + this._vertexSource, + this._fragmentSource + ); + + return new ShaderProgram(engine, vertexSource, fragmentSource); + } + + private _getShaderLabProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { const isWebGL2: boolean = engine._hardwareRenderer.isWebGL2; const shaderMacroList = new Array(); ShaderMacro._getMacrosElements(macroCollection, shaderMacroList); @@ -147,29 +163,20 @@ export class ShaderPass extends ShaderPart { shaderMacroList.push(ShaderMacro.getByName("HAS_DERIVATIVES")); } - // Compatible with non-shaderlab syntax let noIncludeVertex = ShaderFactory.parseIncludes(this._vertexSource); let noIncludeFrag = ShaderFactory.parseIncludes(this._fragmentSource); - // Parse macros when use shaderlab - if (this._platformTarget != undefined) { - noIncludeVertex = Shader._shaderLab._parseMacros(noIncludeVertex, shaderMacroList); - noIncludeFrag = Shader._shaderLab._parseMacros(noIncludeFrag, shaderMacroList); - } else { - const macroNameStr = ShaderFactory.parseCustomMacros(shaderMacroList); - noIncludeVertex = macroNameStr + noIncludeVertex; - noIncludeFrag = macroNameStr + noIncludeFrag; - } + noIncludeVertex = Shader._shaderLab._parseMacros(noIncludeVertex, shaderMacroList); + noIncludeFrag = Shader._shaderLab._parseMacros(noIncludeFrag, shaderMacroList); - // Need to convert to 300 es when the target is GLSL ES 100 or unkdown - if (isWebGL2 && (this._platformTarget == undefined || this._platformTarget === ShaderLanguage.GLSLES100)) { + if (isWebGL2 && this._platformTarget === ShaderLanguage.GLSLES100) { noIncludeVertex = ShaderFactory.convertTo300(noIncludeVertex); noIncludeFrag = ShaderFactory.convertTo300(noIncludeFrag, true); } const versionStr = isWebGL2 ? "#version 300 es" : "#version 100"; - const vertexSource = ` ${versionStr} + const vertexSource = ` ${versionStr} ${noIncludeVertex} `; const fragmentSource = ` ${versionStr} @@ -178,8 +185,6 @@ export class ShaderPass extends ShaderPart { ${noIncludeFrag} `; - const shaderProgram = new ShaderProgram(engine, vertexSource, fragmentSource); - - return shaderProgram; + return new ShaderProgram(engine, vertexSource, fragmentSource); } } diff --git a/packages/core/src/shader/ShaderProgram.ts b/packages/core/src/shader/ShaderProgram.ts index 7f064511ea..22151e5f77 100644 --- a/packages/core/src/shader/ShaderProgram.ts +++ b/packages/core/src/shader/ShaderProgram.ts @@ -68,10 +68,10 @@ export class ShaderProgram { return this._isValid; } - constructor(engine: Engine, vertexSource: string, fragmentSource: string) { + constructor(engine: Engine, vertexSource: string, fragmentSource: string, transformFeedbackVaryings?: string[]) { this._engine = engine; this._gl = engine._hardwareRenderer.gl; - this._glProgram = this._createProgram(vertexSource, fragmentSource); + this._glProgram = this._createProgram(vertexSource, fragmentSource, transformFeedbackVaryings); if (this._glProgram) { this._isValid = true; @@ -237,7 +237,11 @@ export class ShaderProgram { /** * Init and link program with shader. */ - private _createProgram(vertexSource: string, fragmentSource: string): WebGLProgram | null { + private _createProgram( + vertexSource: string, + fragmentSource: string, + transformFeedbackVaryings?: string[] + ): WebGLProgram | null { const gl = this._gl; // Create and compile shader @@ -259,6 +263,16 @@ export class ShaderProgram { gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); + + // Set Transform Feedback varyings before linking (WebGL2 only) + if (transformFeedbackVaryings?.length) { + (gl).transformFeedbackVaryings( + program, + transformFeedbackVaryings, + (gl).INTERLEAVED_ATTRIBS + ); + } + gl.linkProgram(program); gl.validateProgram(program); diff --git a/packages/core/src/shaderlib/ShaderFactory.ts b/packages/core/src/shaderlib/ShaderFactory.ts index 8510c68d20..e314e4409d 100644 --- a/packages/core/src/shaderlib/ShaderFactory.ts +++ b/packages/core/src/shaderlib/ShaderFactory.ts @@ -1,5 +1,8 @@ +import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; +import { Engine } from "../Engine"; import { ShaderMacro } from "../shader/ShaderMacro"; +import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; import { ShaderLib } from "./ShaderLib"; export class ShaderFactory { @@ -19,6 +22,61 @@ export class ShaderFactory { return macros.map((m) => `#define ${m.value ? m.name + ` ` + m.value : m.name}\n`).join(""); } + /** + * @internal + * Compile vertex and fragment source with standard macros, includes, and version header. + * @param engine - Engine instance + * @param macroCollection - Current macro collection + * @param vertexSource - Raw vertex shader source (may contain #include) + * @param fragmentSource - Raw fragment shader source + * @returns Compiled { vertexSource, fragmentSource } ready for ShaderProgram + */ + static compilePlatformSource( + engine: Engine, + macroCollection: ShaderMacroCollection, + vertexSource: string, + fragmentSource: string + ): { vertexSource: string; fragmentSource: string } { + const isWebGL2 = engine._hardwareRenderer.isWebGL2; + const shaderMacroList = new Array(); + ShaderMacro._getMacrosElements(macroCollection, shaderMacroList); + shaderMacroList.push(ShaderMacro.getByName(isWebGL2 ? "GRAPHICS_API_WEBGL2" : "GRAPHICS_API_WEBGL1")); + if (engine._hardwareRenderer.canIUse(GLCapabilityType.shaderTextureLod)) { + shaderMacroList.push(ShaderMacro.getByName("HAS_TEX_LOD")); + } + if (engine._hardwareRenderer.canIUse(GLCapabilityType.standardDerivatives)) { + shaderMacroList.push(ShaderMacro.getByName("HAS_DERIVATIVES")); + } + + let noIncludeVertex = ShaderFactory.parseIncludes(vertexSource); + let noIncludeFrag = ShaderFactory.parseIncludes(fragmentSource); + + const macroStr = ShaderFactory.parseCustomMacros(shaderMacroList); + noIncludeVertex = macroStr + noIncludeVertex; + noIncludeFrag = macroStr + noIncludeFrag; + + if (isWebGL2) { + noIncludeVertex = ShaderFactory.convertTo300(noIncludeVertex); + noIncludeFrag = ShaderFactory.convertTo300(noIncludeFrag, true); + } + + const versionStr = isWebGL2 ? "#version 300 es" : "#version 100"; + const precisionStr = ` +#ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + precision highp int; +#else + precision mediump float; + precision mediump int; +#endif +`; + + return { + vertexSource: `${versionStr}\nprecision highp float;\n${noIncludeVertex}`, + fragmentSource: `${versionStr}\n${isWebGL2 ? "" : ShaderFactory._shaderExtension}${precisionStr}${noIncludeFrag}` + }; + } + static registerInclude(includeName: string, includeSource: string) { if (ShaderLib[includeName]) { throw `The "${includeName}" shader include already exist`; diff --git a/packages/core/src/shaderlib/extra/particle.vs.glsl b/packages/core/src/shaderlib/extra/particle.vs.glsl index d737c73efb..7f946a9b6d 100644 --- a/packages/core/src/shaderlib/extra/particle.vs.glsl +++ b/packages/core/src/shaderlib/extra/particle.vs.glsl @@ -26,9 +26,18 @@ attribute float a_StartSpeed; attribute vec4 a_Random1; // x:texture sheet animation random #endif +#if defined(RENDERER_FOL_CONSTANT_MODE) || defined(RENDERER_FOL_CURVE_MODE) || defined(RENDERER_LVL_MODULE_ENABLED) + attribute vec4 a_Random2; +#endif + attribute vec3 a_SimulationWorldPosition; attribute vec4 a_SimulationWorldRotation; +#ifdef RENDERER_TRANSFORM_FEEDBACK + attribute vec3 a_FeedbackPosition; + attribute vec3 a_FeedbackVelocity; +#endif + varying vec4 v_Color; #ifdef MATERIAL_HAS_BASETEXTURE attribute vec4 a_SimulationUV; @@ -37,7 +46,6 @@ varying vec4 v_Color; uniform float renderer_CurrentTime; uniform vec3 renderer_Gravity; -uniform vec2 u_DragConstant; uniform vec3 renderer_WorldPosition; uniform vec4 renderer_WorldRotation; uniform bool renderer_ThreeDStartRotation; @@ -67,15 +75,8 @@ uniform int renderer_SimulationSpace; #include #include -vec3 getStartPosition(vec3 startVelocity, float age, vec3 dragData) { - vec3 startPosition; - float lastTime = min(startVelocity.x / dragData.x, age); // todo 0/0 - startPosition = lastTime * (startVelocity - 0.5 * dragData * lastTime); - return startPosition; -} - -vec3 computeParticlePosition(in vec3 startVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, vec3 dragData, inout vec3 localVelocity, inout vec3 worldVelocity) { - vec3 startPosition = getStartPosition(startVelocity, age, dragData); +vec3 computeParticlePosition(in vec3 startVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, inout vec3 localVelocity, inout vec3 worldVelocity) { + vec3 startPosition = startVelocity * age; vec3 finalPosition; vec3 localPositionOffset = startPosition; @@ -122,9 +123,6 @@ void main() { float age = renderer_CurrentTime - a_DirectionTime.w; float normalizedAge = age / a_ShapePositionStartLifeTime.w; if (normalizedAge < 1.0) { - vec3 startVelocity = a_DirectionTime.xyz * a_StartSpeed; - vec3 gravityVelocity = renderer_Gravity * a_Random0.x * age; - vec4 worldRotation; if (renderer_SimulationSpace == 0) { worldRotation = renderer_WorldRotation; @@ -132,12 +130,38 @@ void main() { worldRotation = a_SimulationWorldRotation; } - vec3 localVelocity = startVelocity; - vec3 worldVelocity = gravityVelocity; - - //drag - vec3 dragData = a_DirectionTime.xyz * mix(u_DragConstant.x, u_DragConstant.y, a_Random0.x); - vec3 center = computeParticlePosition(startVelocity, age, normalizedAge, gravityVelocity, worldRotation, dragData, localVelocity, worldVelocity); + vec3 localVelocity; + vec3 worldVelocity; + + #ifdef RENDERER_TRANSFORM_FEEDBACK + // Transform Feedback mode: position in simulation space (local or world). + // Local: transform to world; World: use directly. + vec3 center; + if (renderer_SimulationSpace == 0) { + center = rotationByQuaternions(a_FeedbackPosition, worldRotation) + renderer_WorldPosition; + } else if (renderer_SimulationSpace == 1) { + center = a_FeedbackPosition; + } + localVelocity = a_FeedbackVelocity; + worldVelocity = vec3(0.0); + + #ifdef _VOL_MODULE_ENABLED + vec3 instantVOLVelocity; + computeVelocityPositionOffset(normalizedAge, age, instantVOLVelocity); + if (renderer_VOLSpace == 0) { + localVelocity += instantVOLVelocity; + } else { + worldVelocity += instantVOLVelocity; + } + #endif + #else + // Original analytical path + vec3 startVelocity = a_DirectionTime.xyz * a_StartSpeed; + vec3 gravityVelocity = renderer_Gravity * a_Random0.x * age; + localVelocity = startVelocity; + worldVelocity = gravityVelocity; + vec3 center = computeParticlePosition(startVelocity, age, normalizedAge, gravityVelocity, worldRotation, localVelocity, worldVelocity); + #endif #include #include diff --git a/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl index a40cf87717..612007f30a 100644 --- a/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl +++ b/packages/core/src/shaderlib/particle/force_over_lifetime_module.glsl @@ -3,8 +3,6 @@ #endif #ifdef _FOL_MODULE_ENABLED - attribute vec4 a_Random2; - uniform int renderer_FOLSpace; #ifdef RENDERER_FOL_CONSTANT_MODE diff --git a/packages/core/src/shaderlib/particle/index.ts b/packages/core/src/shaderlib/particle/index.ts index 111997f37e..83893232ef 100644 --- a/packages/core/src/shaderlib/particle/index.ts +++ b/packages/core/src/shaderlib/particle/index.ts @@ -5,6 +5,8 @@ import size_over_lifetime_module from "./size_over_lifetime_module.glsl"; import color_over_lifetime_module from "./color_over_lifetime_module.glsl"; import texture_sheet_animation_module from "./texture_sheet_animation_module.glsl"; import force_over_lifetime_module from "./force_over_lifetime_module.glsl"; +import limit_velocity_over_lifetime_module from "./limit_velocity_over_lifetime_module.glsl"; +import particle_feedback_simulation from "./particle_feedback_simulation.glsl"; import sphere_billboard from "./sphere_billboard.glsl"; import stretched_billboard from "./stretched_billboard.glsl"; @@ -20,6 +22,8 @@ export default { color_over_lifetime_module, texture_sheet_animation_module, force_over_lifetime_module, + limit_velocity_over_lifetime_module, + particle_feedback_simulation, sphere_billboard, stretched_billboard, diff --git a/packages/core/src/shaderlib/particle/limit_velocity_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/limit_velocity_over_lifetime_module.glsl new file mode 100644 index 0000000000..0e3fda0b0c --- /dev/null +++ b/packages/core/src/shaderlib/particle/limit_velocity_over_lifetime_module.glsl @@ -0,0 +1,116 @@ +#ifdef RENDERER_LVL_MODULE_ENABLED + uniform int renderer_LVLSpace; + uniform float renderer_LVLDampen; + + // Scalar limit + #ifndef RENDERER_LVL_SEPARATE_AXES + #ifdef RENDERER_LVL_LIMIT_CONSTANT_MODE + uniform float renderer_LVLLimitMaxConst; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + uniform float renderer_LVLLimitMinConst; + #endif + #endif + #ifdef RENDERER_LVL_LIMIT_CURVE_MODE + uniform vec2 renderer_LVLLimitMaxCurve[4]; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + uniform vec2 renderer_LVLLimitMinCurve[4]; + #endif + #endif + #endif + + // Per-axis limit + #ifdef RENDERER_LVL_SEPARATE_AXES + #ifdef RENDERER_LVL_LIMIT_CONSTANT_MODE + uniform vec3 renderer_LVLLimitMaxConstVector; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + uniform vec3 renderer_LVLLimitMinConstVector; + #endif + #endif + #ifdef RENDERER_LVL_LIMIT_CURVE_MODE + uniform vec2 renderer_LVLLimitXMaxCurve[4]; + uniform vec2 renderer_LVLLimitYMaxCurve[4]; + uniform vec2 renderer_LVLLimitZMaxCurve[4]; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + uniform vec2 renderer_LVLLimitXMinCurve[4]; + uniform vec2 renderer_LVLLimitYMinCurve[4]; + uniform vec2 renderer_LVLLimitZMinCurve[4]; + #endif + #endif + #endif + + // Drag curve + #ifdef RENDERER_LVL_DRAG_CURVE_MODE + uniform vec2 renderer_LVLDragMaxCurve[4]; + #ifdef RENDERER_LVL_DRAG_IS_RANDOM_TWO + uniform vec2 renderer_LVLDragMinCurve[4]; + #endif + #endif + + float evaluateLVLDrag(float normalizedAge, float dragRand) { + #ifdef RENDERER_LVL_DRAG_CURVE_MODE + float dragMax = evaluateParticleCurve(renderer_LVLDragMaxCurve, normalizedAge); + #ifdef RENDERER_LVL_DRAG_IS_RANDOM_TWO + float dragMin = evaluateParticleCurve(renderer_LVLDragMinCurve, normalizedAge); + return mix(dragMin, dragMax, dragRand); + #else + return dragMax; + #endif + #else + return mix(renderer_LVLDragConstant.x, renderer_LVLDragConstant.y, dragRand); + #endif + } + + vec3 applyLVLSpeedLimitTF(vec3 velocity, float normalizedAge, float limitRand, float effectiveDampen) { + #ifdef RENDERER_LVL_SEPARATE_AXES + vec3 limitValue; + #ifdef RENDERER_LVL_LIMIT_CONSTANT_MODE + limitValue = renderer_LVLLimitMaxConstVector; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + limitValue = mix(renderer_LVLLimitMinConstVector, limitValue, limitRand); + #endif + #endif + #ifdef RENDERER_LVL_LIMIT_CURVE_MODE + limitValue = vec3( + evaluateParticleCurve(renderer_LVLLimitXMaxCurve, normalizedAge), + evaluateParticleCurve(renderer_LVLLimitYMaxCurve, normalizedAge), + evaluateParticleCurve(renderer_LVLLimitZMaxCurve, normalizedAge) + ); + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + vec3 minLimitValue = vec3( + evaluateParticleCurve(renderer_LVLLimitXMinCurve, normalizedAge), + evaluateParticleCurve(renderer_LVLLimitYMinCurve, normalizedAge), + evaluateParticleCurve(renderer_LVLLimitZMinCurve, normalizedAge) + ); + limitValue = mix(minLimitValue, limitValue, limitRand); + #endif + #endif + + vec3 absVel = abs(velocity); + vec3 excess = max(absVel - limitValue, vec3(0.0)); + velocity = sign(velocity) * (absVel - excess * effectiveDampen); + #else + float limitValue; + #ifdef RENDERER_LVL_LIMIT_CONSTANT_MODE + limitValue = renderer_LVLLimitMaxConst; + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + limitValue = mix(renderer_LVLLimitMinConst, limitValue, limitRand); + #endif + #endif + #ifdef RENDERER_LVL_LIMIT_CURVE_MODE + limitValue = evaluateParticleCurve(renderer_LVLLimitMaxCurve, normalizedAge); + #ifdef RENDERER_LVL_LIMIT_IS_RANDOM_TWO + float minLimitValue = evaluateParticleCurve(renderer_LVLLimitMinCurve, normalizedAge); + limitValue = mix(minLimitValue, limitValue, limitRand); + #endif + #endif + + float speed = length(velocity); + if (speed > limitValue && speed > 0.0) { + float excess = speed - limitValue; + velocity = velocity * ((speed - excess * effectiveDampen) / speed); + } + #endif + return velocity; + } + +#endif diff --git a/packages/core/src/shaderlib/particle/particle_common.glsl b/packages/core/src/shaderlib/particle/particle_common.glsl index 7034d51479..ea26df257f 100644 --- a/packages/core/src/shaderlib/particle/particle_common.glsl +++ b/packages/core/src/shaderlib/particle/particle_common.glsl @@ -2,6 +2,10 @@ vec3 rotationByQuaternions(in vec3 v, in vec4 q) { return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); } +vec4 quaternionConjugate(in vec4 q) { + return vec4(-q.xyz, q.w); +} + vec3 rotationByEuler(in vec3 vector, in vec3 rot) { float halfRoll = rot.z * 0.5; float halfPitch = rot.x * 0.5; diff --git a/packages/core/src/shaderlib/particle/particle_feedback_simulation.glsl b/packages/core/src/shaderlib/particle/particle_feedback_simulation.glsl new file mode 100644 index 0000000000..51c1ce7d9f --- /dev/null +++ b/packages/core/src/shaderlib/particle/particle_feedback_simulation.glsl @@ -0,0 +1,243 @@ +// Transform Feedback update shader for particle simulation. +// Update order: VOL/FOL → Dampen → Drag → Position. +// Runs once per particle per frame (no rasterization). + +// Previous frame TF data +attribute vec3 a_FeedbackPosition; +attribute vec3 a_FeedbackVelocity; + +// Per-particle instance data +attribute vec4 a_ShapePositionStartLifeTime; +attribute vec4 a_DirectionTime; +attribute vec3 a_StartSize; +attribute float a_StartSpeed; +attribute vec4 a_Random0; +attribute vec4 a_Random1; +attribute vec3 a_SimulationWorldPosition; +attribute vec4 a_SimulationWorldRotation; +attribute vec4 a_Random2; + +// Uniforms +uniform float renderer_CurrentTime; +uniform float renderer_DeltaTime; +uniform vec3 renderer_Gravity; +uniform vec2 renderer_LVLDragConstant; +uniform vec3 renderer_WorldPosition; +uniform vec4 renderer_WorldRotation; +uniform int renderer_SimulationSpace; + +// TF outputs +varying vec3 v_FeedbackPosition; +varying vec3 v_FeedbackVelocity; + +#include +#include +#include +#include + +// Get VOL instantaneous velocity at normalizedAge +vec3 getVOLVelocity(float normalizedAge) { + vec3 vel = vec3(0.0); + #ifdef _VOL_MODULE_ENABLED + #ifdef RENDERER_VOL_CONSTANT_MODE + vel = renderer_VOLMaxConst; + #ifdef RENDERER_VOL_IS_RANDOM_TWO + vel = mix(renderer_VOLMinConst, vel, a_Random1.yzw); + #endif + #endif + #ifdef RENDERER_VOL_CURVE_MODE + vel = vec3( + evaluateParticleCurve(renderer_VOLMaxGradientX, normalizedAge), + evaluateParticleCurve(renderer_VOLMaxGradientY, normalizedAge), + evaluateParticleCurve(renderer_VOLMaxGradientZ, normalizedAge) + ); + #ifdef RENDERER_VOL_IS_RANDOM_TWO + vec3 minVel = vec3( + evaluateParticleCurve(renderer_VOLMinGradientX, normalizedAge), + evaluateParticleCurve(renderer_VOLMinGradientY, normalizedAge), + evaluateParticleCurve(renderer_VOLMinGradientZ, normalizedAge) + ); + vel = mix(minVel, vel, a_Random1.yzw); + #endif + #endif + #endif + return vel; +} + +// Get FOL instantaneous acceleration at normalizedAge +vec3 getFOLAcceleration(float normalizedAge) { + vec3 acc = vec3(0.0); + #ifdef _FOL_MODULE_ENABLED + #ifdef RENDERER_FOL_CONSTANT_MODE + acc = renderer_FOLMaxConst; + #ifdef RENDERER_FOL_IS_RANDOM_TWO + acc = mix(renderer_FOLMinConst, acc, vec3(a_Random2.x, a_Random2.y, a_Random2.z)); + #endif + #endif + #ifdef RENDERER_FOL_CURVE_MODE + acc = vec3( + evaluateParticleCurve(renderer_FOLMaxGradientX, normalizedAge), + evaluateParticleCurve(renderer_FOLMaxGradientY, normalizedAge), + evaluateParticleCurve(renderer_FOLMaxGradientZ, normalizedAge) + ); + #ifdef RENDERER_FOL_IS_RANDOM_TWO + vec3 minAcc = vec3( + evaluateParticleCurve(renderer_FOLMinGradientX, normalizedAge), + evaluateParticleCurve(renderer_FOLMinGradientY, normalizedAge), + evaluateParticleCurve(renderer_FOLMinGradientZ, normalizedAge) + ); + acc = mix(minAcc, acc, vec3(a_Random2.x, a_Random2.y, a_Random2.z)); + #endif + #endif + #endif + return acc; +} + +void main() { + float age = renderer_CurrentTime - a_DirectionTime.w; + float lifetime = a_ShapePositionStartLifeTime.w; + float normalizedAge = age / lifetime; + float dt = renderer_DeltaTime; + + // Dead particle: pass through unchanged + if (normalizedAge >= 1.0 || age < 0.0) { + v_FeedbackPosition = a_FeedbackPosition; + v_FeedbackVelocity = a_FeedbackVelocity; + gl_Position = vec4(0.0); + return; + } + + vec4 worldRotation; + if (renderer_SimulationSpace == 0) { + worldRotation = renderer_WorldRotation; + } else { + worldRotation = a_SimulationWorldRotation; + } + vec4 invWorldRotation = quaternionConjugate(worldRotation); + + // Read previous frame state (initialized by CPU on particle birth) + vec3 localVelocity = a_FeedbackVelocity; + + // ===================================================== + // Step 1: Apply velocity module deltas (VOL + FOL + Gravity) + // ===================================================== + + // Gravity (world space) + vec3 gravityDelta = renderer_Gravity * a_Random0.x * dt; + + // VOL instantaneous velocity (animated velocity, not persisted) + vec3 volLocal = vec3(0.0); + vec3 volWorld = vec3(0.0); + #ifdef _VOL_MODULE_ENABLED + vec3 vol = getVOLVelocity(normalizedAge); + if (renderer_VOLSpace == 0) { + volLocal = vol; + } else { + volWorld = vol; + } + #endif + + // FOL acceleration → velocity delta (always persisted, like gravity) + vec3 folDeltaLocal = vec3(0.0); + #ifdef _FOL_MODULE_ENABLED + vec3 folAcc = getFOLAcceleration(normalizedAge); + vec3 folVelDelta = folAcc * dt; + if (renderer_FOLSpace == 0) { + folDeltaLocal = folVelDelta; + } else { + // World FOL: convert to local and persist, same as gravity + folDeltaLocal = rotationByQuaternions(folVelDelta, invWorldRotation); + } + #endif + + // Gravity and FOL contribute to base velocity (persisted, subject to dampen/drag). + vec3 gravityLocal = rotationByQuaternions(gravityDelta, invWorldRotation); + localVelocity += folDeltaLocal + gravityLocal; + + // ===================================================== + // Step 2 & 3: Dampen (Limit Velocity) + Drag + // VOL must be projected into the LVL target space so that + // limit/drag see the full velocity regardless of VOL.space vs LVL.space. + // ===================================================== + #ifdef RENDERER_LVL_MODULE_ENABLED + // Precompute VOL in both spaces + vec3 volAsLocal = volLocal + rotationByQuaternions(volWorld, invWorldRotation); + vec3 volAsWorld = rotationByQuaternions(volLocal, worldRotation) + volWorld; + + float limitRand = a_Random2.w; + float dampen = renderer_LVLDampen; + // Frame-rate independent dampen (30fps as reference) + float effectiveDampen = 1.0 - pow(1.0 - dampen, dt * 30.0); + + if (renderer_LVLSpace == 0) { + // Local space: total = base + all VOL projected to local + vec3 totalLocal = localVelocity + volAsLocal; + vec3 dampenedTotal = applyLVLSpeedLimitTF(totalLocal, normalizedAge, limitRand, effectiveDampen); + localVelocity = dampenedTotal - volAsLocal; + } else { + // World space: total = rotated base + all VOL projected to world + vec3 totalWorld = rotationByQuaternions(localVelocity, worldRotation) + volAsWorld; + vec3 dampenedTotal = applyLVLSpeedLimitTF(totalWorld, normalizedAge, limitRand, effectiveDampen); + localVelocity = rotationByQuaternions(dampenedTotal - volAsWorld, invWorldRotation); + } + + // Drag: same space as dampen + { + float dragCoeff = evaluateLVLDrag(normalizedAge, a_Random2.w); + if (dragCoeff > 0.0) { + vec3 totalVel; + if (renderer_LVLSpace == 0) { + totalVel = localVelocity + volAsLocal; + } else { + totalVel = rotationByQuaternions(localVelocity, worldRotation) + volAsWorld; + } + float velMagSqr = dot(totalVel, totalVel); + float velMag = sqrt(velMagSqr); + + float drag = dragCoeff; + + #ifdef RENDERER_LVL_DRAG_MULTIPLY_SIZE + float maxDim = max(a_StartSize.x, max(a_StartSize.y, a_StartSize.z)); + float radius = maxDim * 0.5; + drag *= 3.14159265 * radius * radius; + #endif + + #ifdef RENDERER_LVL_DRAG_MULTIPLY_VELOCITY + drag *= velMagSqr; + #endif + + if (velMag > 0.0) { + float newVelMag = max(0.0, velMag - drag * dt); + vec3 draggedTotal = totalVel * (newVelMag / velMag); + if (renderer_LVLSpace == 0) { + localVelocity = draggedTotal - volAsLocal; + } else { + localVelocity = rotationByQuaternions(draggedTotal - volAsWorld, invWorldRotation); + } + } + } + } + #endif + + // ===================================================== + // Step 4: Integrate position in simulation space + // Local mode: position in local space, velocity rotated to local + // World mode: position in world space, velocity rotated to world + // ===================================================== + // FOL is now fully in localVelocity (both local and world-space FOL). + // Only VOL overlay needs to be added here. + vec3 totalVelocity; + if (renderer_SimulationSpace == 0) { + // Local: integrate in local space + totalVelocity = localVelocity + volLocal + + rotationByQuaternions(volWorld, invWorldRotation); + } else { + // World: integrate in world space + totalVelocity = rotationByQuaternions(localVelocity + volLocal, worldRotation) + volWorld; + } + vec3 position = a_FeedbackPosition + totalVelocity * dt; + + v_FeedbackPosition = position; + v_FeedbackVelocity = localVelocity; + gl_Position = vec4(0.0); +} diff --git a/packages/design/package.json b/packages/design/package.json index 15e344d70e..bb9334689e 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index 51fa4c828b..76afb0bfe9 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index fc21544658..4adb5e42ac 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 73e6e73be5..c3f93f7ce9 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/src/CollisionUtil.ts b/packages/math/src/CollisionUtil.ts index 893f4a3981..6cebe6d235 100644 --- a/packages/math/src/CollisionUtil.ts +++ b/packages/math/src/CollisionUtil.ts @@ -145,12 +145,16 @@ export class CollisionUtil { const { zeroTolerance } = MathUtil; const dir = Vector3.dot(normal, ray.direction); + const position = Vector3.dot(normal, ray.origin); // Parallel if (Math.abs(dir) < zeroTolerance) { + // Check if ray origin is on the plane + if (Math.abs(position + plane.distance) < zeroTolerance) { + return 0; + } return -1; } - const position = Vector3.dot(normal, ray.origin); let distance = (-plane.distance - position) / dir; if (distance < 0) { diff --git a/packages/physics-lite/package.json b/packages/physics-lite/package.json index 466970ff5c..013e4268cc 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.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index f57ec1d7e2..0fc6a36f49 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.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 614aa0e480..74baac132a 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.14", + "version": "2.0.0-alpha.15", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/rhi-webgl/src/GLBuffer.ts b/packages/rhi-webgl/src/GLBuffer.ts index beef4d2e78..86f0b9b1d0 100644 --- a/packages/rhi-webgl/src/GLBuffer.ts +++ b/packages/rhi-webgl/src/GLBuffer.ts @@ -3,10 +3,12 @@ import { WebGLGraphicDevice } from "./WebGLGraphicDevice"; import { WebGLExtension } from "./type"; export class GLBuffer implements IPlatformBuffer { + /** @internal */ + _glBuffer: WebGLBuffer; + private _gl: (WebGLRenderingContext & WebGLExtension) | WebGL2RenderingContext; private _glBindTarget: number; private _glBufferUsage: number; - private _glBuffer: WebGLBuffer; private _isWebGL2: boolean; constructor( @@ -20,7 +22,6 @@ export class GLBuffer implements IPlatformBuffer { const glBuffer = gl.createBuffer(); const glBufferUsage = this._getGLBufferUsage(gl, bufferUsage); const glBindTarget = type === BufferBindFlag.VertexBuffer ? gl.ARRAY_BUFFER : gl.ELEMENT_ARRAY_BUFFER; - this._gl = gl; this._glBuffer = glBuffer; this._glBufferUsage = glBufferUsage; @@ -90,6 +91,13 @@ export class GLBuffer implements IPlatformBuffer { } } + copyFromBuffer(srcBuffer: IPlatformBuffer, srcByteOffset: number, dstByteOffset: number, byteLength: number): void { + const gl = this._gl; + gl.bindBuffer(gl.COPY_READ_BUFFER, (srcBuffer)._glBuffer); + gl.bindBuffer(gl.COPY_WRITE_BUFFER, this._glBuffer); + gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, srcByteOffset, dstByteOffset, byteLength); + } + destroy(): void { this._gl.deleteBuffer(this._glBuffer); this._gl = null; diff --git a/packages/rhi-webgl/src/GLTransformFeedback.ts b/packages/rhi-webgl/src/GLTransformFeedback.ts new file mode 100644 index 0000000000..c64bd3520b --- /dev/null +++ b/packages/rhi-webgl/src/GLTransformFeedback.ts @@ -0,0 +1,53 @@ +import { IPlatformTransformFeedback, IPlatformBuffer } from "@galacean/engine-core"; +import { WebGLGraphicDevice } from "./WebGLGraphicDevice"; +import { GLBuffer } from "./GLBuffer"; + +/** + * @internal + * WebGL2 implementation of Transform Feedback. + */ +export class GLTransformFeedback implements IPlatformTransformFeedback { + private _gl: WebGL2RenderingContext; + private _glTransformFeedback: WebGLTransformFeedback; + + constructor(rhi: WebGLGraphicDevice) { + const gl = rhi.gl; + this._gl = gl; + this._glTransformFeedback = gl.createTransformFeedback(); + } + + bindBufferRange(index: number, buffer: IPlatformBuffer, byteOffset: number, byteSize: number): void { + const gl = this._gl; + gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, index, (buffer)._glBuffer, byteOffset, byteSize); + } + + unbindBuffer(index: number): void { + const gl = this._gl; + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, null); + } + + begin(primitiveMode: number): void { + this._gl.beginTransformFeedback(primitiveMode); + } + + end(): void { + this._gl.endTransformFeedback(); + } + + bind(): void { + const gl = this._gl; + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this._glTransformFeedback); + } + + unbind(): void { + const gl = this._gl; + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + } + + destroy(): void { + if (this._glTransformFeedback) { + this._gl.deleteTransformFeedback(this._glTransformFeedback); + this._glTransformFeedback = null; + } + } +} diff --git a/packages/rhi-webgl/src/GLTransformFeedbackPrimitive.ts b/packages/rhi-webgl/src/GLTransformFeedbackPrimitive.ts new file mode 100644 index 0000000000..26f6506675 --- /dev/null +++ b/packages/rhi-webgl/src/GLTransformFeedbackPrimitive.ts @@ -0,0 +1,110 @@ +import { IPlatformTransformFeedbackPrimitive, VertexElement, VertexBufferBinding } from "@galacean/engine-core"; +import { GLBuffer } from "./GLBuffer"; + +/** + * @internal + * WebGL2 implementation of Transform Feedback primitive. + * Maintains two VAOs (one per read direction), auto-rebuilds when program changes. + */ +export class GLTransformFeedbackPrimitive implements IPlatformTransformFeedbackPrimitive { + private _gl: WebGL2RenderingContext; + private _vaoA: WebGLVertexArrayObject; + private _vaoB: WebGLVertexArrayObject; + private _lastProgramId = -1; + + constructor(gl: WebGL2RenderingContext) { + this._gl = gl; + } + + updateVertexLayout( + program: any, + readBinding: VertexBufferBinding, + writeBinding: VertexBufferBinding, + feedbackElements: VertexElement[], + inputBinding: VertexBufferBinding, + inputElements: VertexElement[] + ): void { + if (program.id === this._lastProgramId) return; + + this._deleteVAOs(); + + const attribs = program.attributeLocation; + this._vaoA = this._createVAO(attribs, readBinding, feedbackElements, inputBinding, inputElements); + this._vaoB = this._createVAO(attribs, writeBinding, feedbackElements, inputBinding, inputElements); + this._lastProgramId = program.id; + + this._gl.bindVertexArray(null); + } + + bind(readIsA: boolean): void { + this._gl.bindVertexArray(readIsA ? this._vaoA : this._vaoB); + } + + unbind(): void { + this._gl.bindVertexArray(null); + } + + draw(mode: number, first: number, count: number): void { + this._gl.drawArrays(mode, first, count); + } + + invalidate(): void { + this._deleteVAOs(); + } + + destroy(): void { + this._deleteVAOs(); + } + + private _deleteVAOs(): void { + const gl = this._gl; + if (this._vaoA) { + gl.deleteVertexArray(this._vaoA); + this._vaoA = null; + } + if (this._vaoB) { + gl.deleteVertexArray(this._vaoB); + this._vaoB = null; + } + this._lastProgramId = -1; + } + + private _createVAO( + attribs: Record, + feedbackBinding: VertexBufferBinding, + feedbackElements: VertexElement[], + inputBinding: VertexBufferBinding, + inputElements: VertexElement[] + ): WebGLVertexArrayObject { + const gl = this._gl; + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + + // @ts-ignore: Access internal _platformBuffer across packages + gl.bindBuffer(gl.ARRAY_BUFFER, (feedbackBinding.buffer._platformBuffer)._glBuffer); + this._bindElements(gl, attribs, feedbackElements, feedbackBinding.stride); + + // @ts-ignore: Access internal _platformBuffer across packages + gl.bindBuffer(gl.ARRAY_BUFFER, (inputBinding.buffer._platformBuffer)._glBuffer); + this._bindElements(gl, attribs, inputElements, inputBinding.stride); + + gl.bindBuffer(gl.ARRAY_BUFFER, null); + return vao; + } + + private _bindElements( + gl: WebGL2RenderingContext, + attribs: Record, + elements: VertexElement[], + stride: number + ): void { + for (const element of elements) { + const loc = attribs[element.attribute]; + if (loc !== undefined && loc !== -1) { + const info = element._formatMetaInfo; + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer(loc, info.size, info.type, info.normalized, stride, element.offset); + } + } + } +} diff --git a/packages/rhi-webgl/src/WebGLGraphicDevice.ts b/packages/rhi-webgl/src/WebGLGraphicDevice.ts index 9609daa229..c937742f36 100644 --- a/packages/rhi-webgl/src/WebGLGraphicDevice.ts +++ b/packages/rhi-webgl/src/WebGLGraphicDevice.ts @@ -10,6 +10,8 @@ import { IPlatformRenderTarget, IPlatformTexture2D, IPlatformTextureCube, + IPlatformTransformFeedback, + IPlatformTransformFeedbackPrimitive, Logger, Mesh, Platform, @@ -34,6 +36,8 @@ import { GLTexture } from "./GLTexture"; import { GLTexture2D } from "./GLTexture2D"; import { GLTexture2DArray } from "./GLTexture2DArray"; import { GLTextureCube } from "./GLTextureCube"; +import { GLTransformFeedback } from "./GLTransformFeedback"; +import { GLTransformFeedbackPrimitive } from "./GLTransformFeedbackPrimitive"; import { WebCanvas } from "./WebCanvas"; import { WebGLExtension } from "./type"; @@ -268,6 +272,45 @@ export class WebGLGraphicDevice implements IHardwareRenderer { return new GLBuffer(this, type, byteLength, bufferUsage, data); } + createPlatformTransformFeedback(): IPlatformTransformFeedback { + return new GLTransformFeedback(this); + } + + createPlatformTransformFeedbackPrimitive(): IPlatformTransformFeedbackPrimitive { + return new GLTransformFeedbackPrimitive(this._gl); + } + + /** + * Enable GL_RASTERIZER_DISCARD (WebGL2 only). + */ + enableRasterizerDiscard(): void { + if (this._isWebGL2) { + const gl = this._gl; + gl.enable(gl.RASTERIZER_DISCARD); + } + } + + /** + * Disable GL_RASTERIZER_DISCARD (WebGL2 only). + */ + disableRasterizerDiscard(): void { + if (this._isWebGL2) { + const gl = this._gl; + gl.disable(gl.RASTERIZER_DISCARD); + } + } + + /** + * Invalidate the cached shader program state. + * Call this after using a custom program (e.g., Transform Feedback) outside the engine's pipeline. + */ + invalidateShaderProgramState(): void { + if (this._currentBindShaderProgram) { + this._gl.useProgram(null); + this._currentBindShaderProgram = null; + } + } + requireExtension(ext) { return this._extensions.requireExtension(ext); } diff --git a/packages/shader-lab/package.json b/packages/shader-lab/package.json index d692478a36..9bd54bffa1 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.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index d790785757..9edfbebf6c 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index b9f3afb49c..9e79cad810 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index e8b09e7a13..9e5b33c405 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -46,7 +46,6 @@ export class UICanvas extends Component implements IElement { @ignoreClone _canvasIndex: number = -1; /** @internal */ - @ignoreClone _rootCanvas: UICanvas; /** @internal */ @ignoreClone @@ -79,9 +78,7 @@ export class UICanvas extends Component implements IElement { @ignoreClone private _renderMode = CanvasRenderMode.WorldSpace; - @ignoreClone private _camera: Camera; - @ignoreClone private _cameraObserver: Camera; @assignmentClone private _resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation; @@ -413,12 +410,8 @@ export class UICanvas extends Component implements IElement { /** * @internal */ - _cloneTo(target: UICanvas, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: UICanvas): void { target.renderMode = this._renderMode; - const camera = this._camera; - if (camera) { - target.camera = CloneUtils.remapComponent(srcRoot, targetRoot, camera); - } } private _getRenderers(): UIRenderer[] { diff --git a/packages/ui/src/component/UIGroup.ts b/packages/ui/src/component/UIGroup.ts index 98b55d9bfa..b876e05ec4 100644 --- a/packages/ui/src/component/UIGroup.ts +++ b/packages/ui/src/component/UIGroup.ts @@ -11,10 +11,8 @@ export class UIGroup extends Component implements IGroupAble { @ignoreClone _indexInRootCanvas: number = -1; /** @internal */ - @ignoreClone _group: UIGroup; /** @internal */ - @ignoreClone _rootCanvas: UICanvas; /** @internal */ @ignoreClone diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 285df1d122..59a2fc434c 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -45,7 +45,6 @@ export class UIRenderer extends Renderer implements IGraphics { @deepClone raycastPadding: Vector4 = new Vector4(0, 0, 0, 0); /** @internal */ - @ignoreClone _rootCanvas: UICanvas; /** @internal */ @ignoreClone @@ -57,7 +56,6 @@ export class UIRenderer extends Renderer implements IGraphics { @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _group: UIGroup; /** @internal */ @ignoreClone diff --git a/packages/ui/src/component/UITransform.ts b/packages/ui/src/component/UITransform.ts index 699fd5652a..9144373472 100644 --- a/packages/ui/src/component/UITransform.ts +++ b/packages/ui/src/component/UITransform.ts @@ -255,18 +255,14 @@ export class UITransform extends Transform { } // @ts-ignore - override _cloneTo(target: UITransform, srcRoot: Entity, targetRoot: Entity): void { + override _cloneTo(target: UITransform): void { // @ts-ignore - super._cloneTo(target, srcRoot, targetRoot); - + super._cloneTo(target); const { _size: size, _pivot: pivot } = target; - // @ts-ignore size._onValueChanged = pivot._onValueChanged = null; - size.copyFrom(this._size); pivot.copyFrom(this._pivot); - // @ts-ignore size._onValueChanged = target._onSizeChanged; // @ts-ignore diff --git a/packages/ui/src/component/advanced/Button.ts b/packages/ui/src/component/advanced/Button.ts index 1862a11069..73bf817562 100644 --- a/packages/ui/src/component/advanced/Button.ts +++ b/packages/ui/src/component/advanced/Button.ts @@ -1,9 +1,9 @@ -import { Entity, ignoreClone, PointerEventData, Signal } from "@galacean/engine"; +import { deepClone, PointerEventData, Signal } from "@galacean/engine"; import { UIInteractive } from "../interactive/UIInteractive"; export class Button extends UIInteractive { /** Signal emitted when the button is clicked. */ - @ignoreClone + @deepClone readonly onClick = new Signal<[PointerEventData]>(); override onPointerClick(event: PointerEventData): void { @@ -16,13 +16,6 @@ export class Button extends UIInteractive { this.onClick.removeAll(); } - // @ts-ignore - override _cloneTo(target: Button, srcRoot: Entity, targetRoot: Entity): void { - super._cloneTo(target, srcRoot, targetRoot); - // @ts-ignore - this.onClick._cloneTo(target.onClick, srcRoot, targetRoot); - } - /** * Add a listening function for click. * @deprecated Use `onClick.on(listener, context)` instead. diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b415320251..b8ca6ffe55 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -153,9 +153,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { /** * @internal */ - _cloneTo(target: Image, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Image): void { // @ts-ignore - super._cloneTo(target, srcRoot, targetRoot); + super._cloneTo(target); target.sprite = this._sprite; target.drawMode = this._drawMode; } diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index d3371ea43f..98a16436a8 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -267,9 +267,9 @@ export class Text extends UIRenderer implements ITextRenderer { } // @ts-ignore - override _cloneTo(target: Text, srcRoot: Entity, targetRoot: Entity): void { + override _cloneTo(target: Text): void { // @ts-ignore - super._cloneTo(target, srcRoot, targetRoot); + super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; } diff --git a/packages/ui/src/component/interactive/UIInteractive.ts b/packages/ui/src/component/interactive/UIInteractive.ts index 83595f14a4..6811819585 100644 --- a/packages/ui/src/component/interactive/UIInteractive.ts +++ b/packages/ui/src/component/interactive/UIInteractive.ts @@ -1,4 +1,12 @@ -import { CloneUtils, Entity, EntityModifyFlags, Script, assignmentClone, ignoreClone } from "@galacean/engine"; +import { + CloneUtils, + Entity, + EntityModifyFlags, + Script, + assignmentClone, + deepClone, + ignoreClone +} from "@galacean/engine"; import { UIGroup } from "../.."; import { Utils } from "../../Utils"; import { IGroupAble } from "../../interface/IGroupAble"; @@ -11,7 +19,6 @@ import { Transition } from "./transition/Transition"; */ export class UIInteractive extends Script implements IGroupAble { /** @internal */ - @ignoreClone _rootCanvas: UICanvas; /** @internal */ @ignoreClone @@ -23,7 +30,6 @@ export class UIInteractive extends Script implements IGroupAble { @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _group: UIGroup; /** @internal */ @ignoreClone @@ -42,7 +48,7 @@ export class UIInteractive extends Script implements IGroupAble { @ignoreClone _globalInteractiveDirty: boolean = false; - @ignoreClone + @deepClone protected _transitions: Transition[] = []; @assignmentClone protected _interactive: boolean = true; @@ -164,24 +170,6 @@ export class UIInteractive extends Script implements IGroupAble { } } - // @ts-ignore - override _cloneTo(target: UIInteractive, srcRoot: Entity, targetRoot: Entity): void { - const transitions = this._transitions; - for (let i = 0, n = transitions.length; i < n; i++) { - const srcTransition = transitions[i]; - const dstTransition = new (transitions[i].constructor as new () => Transition)(); - dstTransition.normal = srcTransition.normal; - dstTransition.pressed = srcTransition.pressed; - dstTransition.hover = srcTransition.hover; - dstTransition.disabled = srcTransition.disabled; - const transitionTarget = srcTransition.target; - if (transitionTarget) { - dstTransition.target = CloneUtils.remapComponent(srcRoot, targetRoot, transitionTarget); - } - target.addTransition(dstTransition); - } - } - /** * @internal */ diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index ce0b9c4aa1..76f837b150 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.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index 831b885d2f..22ce03f335 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index 63b24a44ee..92ad132d37 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.15", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/CloneUtils.test.ts new file mode 100644 index 0000000000..7d7195f323 --- /dev/null +++ b/tests/src/core/CloneUtils.test.ts @@ -0,0 +1,876 @@ +import { + Entity, + MeshRenderer, + Script, + Signal, + assignmentClone, + deepClone, + ignoreClone +} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { describe, expect, it } from "vitest"; + +class TestScript extends Script { + targetEntity: Entity; + targetRenderer: MeshRenderer; + externalEntity: Entity; + externalRenderer: MeshRenderer; + deepChild: Entity; + selfRef: Entity; + speed: number; + name2: string; + flag: boolean; + data: object; +} + +/** Script with multiple entity/component refs pointing to different nodes */ +class MultiRefScript extends Script { + entityA: Entity; + entityB: Entity; + rendererA: MeshRenderer; + rendererB: MeshRenderer; +} + +/** Script where the same entity is referenced by multiple properties */ +class DuplicateRefScript extends Script { + ref1: Entity; + ref2: Entity; +} + +/** Script with null/undefined entity/component refs */ +class NullRefScript extends Script { + nullEntity: Entity = null; + undefinedEntity: Entity; + nullRenderer: MeshRenderer = null; + someNumber: number = 0; +} + +/** Script referencing a sibling entity (not parent/child, but sibling under clone root) */ +class SiblingRefScript extends Script { + sibling: Entity; + siblingRenderer: MeshRenderer; +} + +/** Script with a mix of decorated and undecorated entity refs */ +class DecoratedRefScript extends Script { + // Undecorated — should auto-remap via _remap + autoRemapEntity: Entity; + + // @assignmentClone — should still auto-remap since _remap takes priority + @assignmentClone + assignedEntity: Entity; + + // @ignoreClone — should still auto-remap since _remap takes priority + @ignoreClone + ignoredEntity: Entity; +} + +/** Script with a @deepClone array of entities */ +class ArrayRefScript extends Script { + @deepClone + entities: Entity[] = []; +} + +/** Script with Component self-reference */ +class SelfComponentRefScript extends Script { + selfScript: SelfComponentRefScript; +} + +/** Script referencing another Script on a different entity */ +class CrossScriptRefScript extends Script { + otherScript: TestScript; +} + +/** Script with a nested plain object containing entity refs */ +class NestedObjectScript extends Script { + @deepClone + config: { target: Entity; label: string } = { target: null, label: "" }; +} + +/** Script for testing multiple same-type components on one entity */ +class CounterScript extends Script { + value: number = 0; + partner: CounterScript; + targetEntity: Entity; +} + +/** Script that references a CounterScript */ +class CounterRefScript extends Script { + counter: CounterScript; +} + +/** Handler script used for Signal structured binding tests */ +class ClickHandler extends Script { + callCount = 0; + lastPrefix: string = ""; + + handleClick(): void { + this.callCount++; + } + + handleClickWithPrefix(prefix: string): void { + this.callCount++; + this.lastPrefix = prefix; + } +} + +/** Script with a Signal property */ +class SignalScript extends Script { + @deepClone + readonly onFire = new Signal<[number]>(); +} + +describe("Clone remap", async () => { + const engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + const scene = engine.sceneManager.activeScene; + engine.run(); + + describe("Basic Entity/Component remap", () => { + it("script undecorated Entity ref should remap to cloned entity", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(TestScript); + script.targetEntity = child; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedChild = clonedParent.children[0]; + + expect(clonedScript.targetEntity).not.eq(child); + expect(clonedScript.targetEntity).eq(clonedChild); + expect(clonedScript.targetEntity.name).eq("child"); + + rootEntity.destroy(); + }); + + it("script undecorated Component ref should remap to cloned component", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const meshRenderer = child.addComponent(MeshRenderer); + const script = parent.addComponent(TestScript); + script.targetRenderer = meshRenderer; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedChild = clonedParent.children[0]; + const clonedMeshRenderer = clonedChild.getComponent(MeshRenderer); + + expect(clonedScript.targetRenderer).not.eq(meshRenderer); + expect(clonedScript.targetRenderer).eq(clonedMeshRenderer); + + rootEntity.destroy(); + }); + + it("script ref to entity outside hierarchy should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(TestScript); + script.externalEntity = external; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.externalEntity).eq(external); + + rootEntity.destroy(); + }); + + it("script ref to component outside hierarchy should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalMR = external.addComponent(MeshRenderer); + const script = parent.addComponent(TestScript); + script.externalRenderer = externalMR; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.externalRenderer).eq(externalMR); + + rootEntity.destroy(); + }); + + it("deep hierarchy entity ref should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const grandchild = child.createChild("grandchild"); + const script = parent.addComponent(TestScript); + script.deepChild = grandchild; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedGrandchild = clonedParent.children[0].children[0]; + + expect(clonedScript.deepChild).not.eq(grandchild); + expect(clonedScript.deepChild).eq(clonedGrandchild); + expect(clonedScript.deepChild.name).eq("grandchild"); + + rootEntity.destroy(); + }); + + it("script ref to self entity (clone root) should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(TestScript); + script.selfRef = parent; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.selfRef).not.eq(parent); + expect(clonedScript.selfRef).eq(clonedParent); + + rootEntity.destroy(); + }); + + it("primitive and plain object props should not be affected", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(TestScript); + const obj = { x: 1 }; + script.speed = 42; + script.name2 = "test"; + script.flag = true; + script.data = obj; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.speed).eq(42); + expect(clonedScript.name2).eq("test"); + expect(clonedScript.flag).eq(true); + expect(clonedScript.data).eq(obj); + + rootEntity.destroy(); + }); + }); + + describe("Multiple and duplicate refs", () => { + it("multiple entity/component refs on same script all remap independently", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const mrA = childA.addComponent(MeshRenderer); + const mrB = childB.addComponent(MeshRenderer); + const script = parent.addComponent(MultiRefScript); + script.entityA = childA; + script.entityB = childB; + script.rendererA = mrA; + script.rendererB = mrB; + + const cloned = parent.clone(); + const cs = cloned.getComponent(MultiRefScript); + + expect(cs.entityA).not.eq(childA); + expect(cs.entityB).not.eq(childB); + expect(cs.entityA.name).eq("childA"); + expect(cs.entityB.name).eq("childB"); + expect(cs.entityA).eq(cloned.children[0]); + expect(cs.entityB).eq(cloned.children[1]); + expect(cs.rendererA).eq(cloned.children[0].getComponent(MeshRenderer)); + expect(cs.rendererB).eq(cloned.children[1].getComponent(MeshRenderer)); + + rootEntity.destroy(); + }); + + it("two properties referencing the same entity both remap to the same cloned entity", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DuplicateRefScript); + script.ref1 = child; + script.ref2 = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DuplicateRefScript); + + expect(cs.ref1).not.eq(child); + expect(cs.ref1).eq(cs.ref2); + expect(cs.ref1).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + }); + + describe("Null and undefined refs", () => { + it("null entity/component refs should not crash and remain null", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(NullRefScript); + + const cloned = parent.clone(); + const cs = cloned.getComponent(NullRefScript); + + expect(cs.nullEntity).eq(null); + expect(cs.nullRenderer).eq(null); + expect(cs.someNumber).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Sibling entity refs", () => { + it("ref to sibling entity under clone root should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const mrB = childB.addComponent(MeshRenderer); + const script = childA.addComponent(SiblingRefScript); + script.sibling = childB; + script.siblingRenderer = mrB; + + const cloned = parent.clone(); + const clonedChildA = cloned.children[0]; + const clonedChildB = cloned.children[1]; + const cs = clonedChildA.getComponent(SiblingRefScript); + + expect(cs.sibling).not.eq(childB); + expect(cs.sibling).eq(clonedChildB); + expect(cs.siblingRenderer).not.eq(mrB); + expect(cs.siblingRenderer).eq(clonedChildB.getComponent(MeshRenderer)); + + rootEntity.destroy(); + }); + }); + + describe("Clone decorator interaction with _remap", () => { + it("@assignmentClone entity ref still gets remapped via _remap priority", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.assignedEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.assignedEntity).not.eq(child); + expect(cs.assignedEntity).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("@ignoreClone entity ref still gets remapped via _remap priority", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.ignoredEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.ignoredEntity).not.eq(child); + expect(cs.ignoredEntity).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("undecorated entity ref remaps correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.autoRemapEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.autoRemapEntity).not.eq(child); + expect(cs.autoRemapEntity).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("@ignoreClone entity ref outside hierarchy stays original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(DecoratedRefScript); + script.ignoredEntity = external; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.ignoredEntity).eq(external); + + rootEntity.destroy(); + }); + }); + + describe("@deepClone array of entities", () => { + it("deep cloned entity array should remap internal refs", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script = parent.addComponent(ArrayRefScript); + script.entities = [childA, childB]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(2); + expect(cs.entities[0]).not.eq(childA); + expect(cs.entities[1]).not.eq(childB); + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(cloned.children[1]); + + rootEntity.destroy(); + }); + + it("deep cloned entity array with external ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(ArrayRefScript); + script.entities = [child, external]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(external); + + rootEntity.destroy(); + }); + + it("deep cloned empty entity array stays empty", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(ArrayRefScript); + script.entities = []; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Component self and cross references", () => { + it("script referencing itself should remap to cloned script", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SelfComponentRefScript); + script.selfScript = script; + + const cloned = parent.clone(); + const cs = cloned.getComponent(SelfComponentRefScript); + + expect(cs.selfScript).not.eq(script); + expect(cs.selfScript).eq(cs); + + rootEntity.destroy(); + }); + + it("script referencing another script on child entity should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const childScript = child.addComponent(TestScript); + const script = parent.addComponent(CrossScriptRefScript); + script.otherScript = childScript; + + const cloned = parent.clone(); + const cs = cloned.getComponent(CrossScriptRefScript); + const clonedChildScript = cloned.children[0].getComponent(TestScript); + + expect(cs.otherScript).not.eq(childScript); + expect(cs.otherScript).eq(clonedChildScript); + + rootEntity.destroy(); + }); + + it("script referencing external script should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalScript = external.addComponent(TestScript); + const script = parent.addComponent(CrossScriptRefScript); + script.otherScript = externalScript; + + const cloned = parent.clone(); + const cs = cloned.getComponent(CrossScriptRefScript); + + expect(cs.otherScript).eq(externalScript); + + rootEntity.destroy(); + }); + }); + + describe("Nested @deepClone object with entity refs", () => { + it("entity ref inside deep cloned plain object should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(NestedObjectScript); + script.config = { target: child, label: "hello" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedObjectScript); + + expect(cs.config).not.eq(script.config); + expect(cs.config.label).eq("hello"); + expect(cs.config.target).not.eq(child); + expect(cs.config.target).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("entity ref inside deep cloned object pointing outside keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(NestedObjectScript); + script.config = { target: external, label: "ext" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedObjectScript); + + expect(cs.config.target).eq(external); + expect(cs.config.label).eq("ext"); + + rootEntity.destroy(); + }); + }); + + describe("Signal clone with structured bindings", () => { + it("@deepClone Signal should not copy closure listeners", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SignalScript); + let called = false; + script.onFire.on(() => { called = true; }); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + + expect(cs.onFire).not.eq(script.onFire); + cs.onFire.invoke(1); + expect(called).eq(false); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should remap structured binding target to cloned hierarchy", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(handler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + expect(handler.callCount).eq(0); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should keep external structured binding target", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalHandler = external.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(externalHandler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + + cs.onFire.invoke(1); + expect(externalHandler.callCount).eq(1); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should remap structured binding with pre-resolved args", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(handler, "handleClickWithPrefix", "myPrefix"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + expect(clonedHandler.lastPrefix).eq("myPrefix"); + expect(handler.callCount).eq(0); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should preserve once flag on structured binding", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.once(handler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + cs.onFire.invoke(2); + expect(clonedHandler.callCount).eq(1); // once: removed after first call + + rootEntity.destroy(); + }); + }); + + describe("Clone hierarchy integrity", () => { + it("clone preserves children count and names", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + parent.createChild("a"); + parent.createChild("b"); + parent.createChild("c"); + + const cloned = parent.clone(); + expect(cloned.children.length).eq(3); + expect(cloned.children[0].name).eq("a"); + expect(cloned.children[1].name).eq("b"); + expect(cloned.children[2].name).eq("c"); + + rootEntity.destroy(); + }); + + it("clone of deeply nested hierarchy preserves structure", () => { + const rootEntity = scene.createRootEntity("root"); + const a = rootEntity.createChild("a"); + const b = a.createChild("b"); + const c = b.createChild("c"); + const d = c.createChild("d"); + + const cloned = a.clone(); + expect(cloned.children[0].name).eq("b"); + expect(cloned.children[0].children[0].name).eq("c"); + expect(cloned.children[0].children[0].children[0].name).eq("d"); + + rootEntity.destroy(); + }); + + it("script on child entity with ref to parent should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = child.addComponent(TestScript); + script.targetEntity = parent; + + const cloned = parent.clone(); + const clonedChild = cloned.children[0]; + const cs = clonedChild.getComponent(TestScript); + + expect(cs.targetEntity).not.eq(parent); + expect(cs.targetEntity).eq(cloned); + + rootEntity.destroy(); + }); + + it("multiple scripts on different entities with cross-refs all remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + + const scriptA = childA.addComponent(TestScript); + scriptA.targetEntity = childB; + + const scriptB = childB.addComponent(TestScript); + scriptB.targetEntity = childA; + + const cloned = parent.clone(); + const clonedA = cloned.children[0]; + const clonedB = cloned.children[1]; + const csA = clonedA.getComponent(TestScript); + const csB = clonedB.getComponent(TestScript); + + expect(csA.targetEntity).eq(clonedB); + expect(csB.targetEntity).eq(clonedA); + + rootEntity.destroy(); + }); + }); + + describe("Single entity with multiple same-type components", () => { + it("clone preserves multiple same-type components with correct state", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.value = 10; + script2.value = 20; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts.length).eq(2); + expect(clonedScripts[0].value).eq(10); + expect(clonedScripts[1].value).eq(20); + expect(clonedScripts[0]).not.eq(script1); + expect(clonedScripts[1]).not.eq(script2); + + rootEntity.destroy(); + }); + + it("ref to second component of same type should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + counter1.value = 1; + counter2.value = 2; + + const refScript = parent.addComponent(CounterRefScript); + refScript.counter = counter2; + + const cloned = parent.clone(); + const clonedRef = cloned.getComponent(CounterRefScript); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRef.counter).not.eq(counter2); + expect(clonedRef.counter).eq(clonedCounters[1]); + expect(clonedRef.counter.value).eq(2); + + rootEntity.destroy(); + }); + + it("ref to first component of same type should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + counter1.value = 100; + counter2.value = 200; + + const refScript = parent.addComponent(CounterRefScript); + refScript.counter = counter1; + + const cloned = parent.clone(); + const clonedRef = cloned.getComponent(CounterRefScript); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRef.counter).not.eq(counter1); + expect(clonedRef.counter).eq(clonedCounters[0]); + expect(clonedRef.counter.value).eq(100); + + rootEntity.destroy(); + }); + + it("cross-references between multiple same-type components on same entity", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.value = 1; + script2.value = 2; + script1.partner = script2; + script2.partner = script1; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].partner).eq(clonedScripts[1]); + expect(clonedScripts[1].partner).eq(clonedScripts[0]); + expect(clonedScripts[0].partner).not.eq(script2); + expect(clonedScripts[1].partner).not.eq(script1); + + rootEntity.destroy(); + }); + + it("self-reference among multiple same-type components remaps to correct clone", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.partner = script1; + script2.partner = script2; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].partner).eq(clonedScripts[0]); + expect(clonedScripts[1].partner).eq(clonedScripts[1]); + + rootEntity.destroy(); + }); + + it("multiple same-type components with entity refs all remap independently", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script1 = parent.addComponent(CounterScript); + const script2 = parent.addComponent(CounterScript); + script1.targetEntity = childA; + script2.targetEntity = childB; + + const cloned = parent.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].targetEntity).eq(cloned.children[0]); + expect(clonedScripts[1].targetEntity).eq(cloned.children[1]); + expect(clonedScripts[0].targetEntity.name).eq("childA"); + expect(clonedScripts[1].targetEntity.name).eq("childB"); + + rootEntity.destroy(); + }); + + it("@deepClone array referencing specific component among same-type siblings", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + const counter3 = child.addComponent(CounterScript); + counter1.value = 1; + counter2.value = 2; + counter3.value = 3; + + const arrayScript = parent.addComponent(ArrayRefScript); + // Note: ArrayRefScript uses Entity[], but we test component indexing + // via direct component references instead + const refScript1 = parent.addComponent(CounterRefScript); + const refScript2 = parent.addComponent(CounterRefScript); + refScript1.counter = counter1; + refScript2.counter = counter3; + + const cloned = parent.clone(); + const clonedRefs = cloned.getComponents(CounterRefScript, []); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRefs[0].counter).eq(clonedCounters[0]); + expect(clonedRefs[0].counter.value).eq(1); + expect(clonedRefs[1].counter).eq(clonedCounters[2]); + expect(clonedRefs[1].counter.value).eq(3); + + rootEntity.destroy(); + }); + }); +}); diff --git a/tests/src/core/particle/LimitVelocityOverLifetime.test.ts b/tests/src/core/particle/LimitVelocityOverLifetime.test.ts new file mode 100644 index 0000000000..980270541b --- /dev/null +++ b/tests/src/core/particle/LimitVelocityOverLifetime.test.ts @@ -0,0 +1,291 @@ +import { + ParticleRenderer, + ParticleMaterial, + Camera, + Entity, + ParticleCurveMode, + ParticleSimulationSpace, + Engine, + ParticleStopMode, + ParticleCompositeCurve, + ParticleCurve, + CurveKey +} from "@galacean/engine-core"; +import { Color, Vector3 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { LitePhysics } from "@galacean/engine-physics-lite"; +import { describe, beforeAll, beforeEach, expect, it } from "vitest"; + +describe("LimitVelocityOverLifetimeModule", function () { + let engine: Engine; + let particleRenderer: ParticleRenderer; + let entity: Entity; + + beforeAll(async function () { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), physics: new LitePhysics() }); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, -10); + cameraEntity.transform.lookAt(new Vector3()); + + entity = rootEntity.createChild("particle"); + particleRenderer = entity.addComponent(ParticleRenderer); + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + particleRenderer.setMaterial(material); + + engine.run(); + }); + + beforeEach(function () { + particleRenderer.generator.stop(true, ParticleStopMode.StopEmittingAndClear); + + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.enabled = false; + lvl.separateAxes = false; + lvl.dampen = 1; + lvl.limit = new ParticleCompositeCurve(1); + lvl.limitY = new ParticleCompositeCurve(1); + lvl.limitZ = new ParticleCompositeCurve(1); + lvl.drag = new ParticleCompositeCurve(0); + lvl.multiplyDragByParticleSize = false; + lvl.multiplyDragByParticleVelocity = false; + lvl.space = ParticleSimulationSpace.Local; + }); + + it("default values", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + expect(lvl.enabled).to.eq(false); + expect(lvl.separateAxes).to.eq(false); + expect(lvl.dampen).to.eq(1); + expect(lvl.space).to.eq(ParticleSimulationSpace.Local); + expect(lvl.multiplyDragByParticleSize).to.eq(false); + expect(lvl.multiplyDragByParticleVelocity).to.eq(false); + }); + + it("enabled property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + expect(lvl.enabled).to.eq(false); + lvl.enabled = true; + expect(lvl.enabled).to.eq(true); + lvl.enabled = false; + expect(lvl.enabled).to.eq(false); + }); + + it("separateAxes property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + expect(lvl.separateAxes).to.eq(false); + lvl.separateAxes = true; + expect(lvl.separateAxes).to.eq(true); + lvl.separateAxes = false; + expect(lvl.separateAxes).to.eq(false); + }); + + it("limit property (alias for limitX)", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const curve = new ParticleCompositeCurve(10); + lvl.limit = curve; + expect(lvl.limit).to.eq(curve); + expect(lvl.limitX).to.eq(curve); + }); + + it("limitX/Y/Z properties", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const curveX = new ParticleCompositeCurve(5); + const curveY = new ParticleCompositeCurve(10); + const curveZ = new ParticleCompositeCurve(15); + + lvl.limitX = curveX; + lvl.limitY = curveY; + lvl.limitZ = curveZ; + + expect(lvl.limitX).to.eq(curveX); + expect(lvl.limitY).to.eq(curveY); + expect(lvl.limitZ).to.eq(curveZ); + }); + + it("dampen property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.dampen = 0.5; + expect(lvl.dampen).to.eq(0.5); + lvl.dampen = 0; + expect(lvl.dampen).to.eq(0); + lvl.dampen = 1; + expect(lvl.dampen).to.eq(1); + }); + + it("drag property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const dragCurve = new ParticleCompositeCurve(2.5); + lvl.drag = dragCurve; + expect(lvl.drag).to.eq(dragCurve); + expect(lvl.drag.constant).to.eq(2.5); + }); + + it("drag with TwoConstants mode", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const dragCurve = new ParticleCompositeCurve(1, 5); + lvl.drag = dragCurve; + expect(lvl.drag.mode).to.eq(ParticleCurveMode.TwoConstants); + expect(lvl.drag.constantMin).to.eq(1); + expect(lvl.drag.constantMax).to.eq(5); + }); + + it("multiplyDragByParticleSize property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.multiplyDragByParticleSize = true; + expect(lvl.multiplyDragByParticleSize).to.eq(true); + lvl.multiplyDragByParticleSize = false; + expect(lvl.multiplyDragByParticleSize).to.eq(false); + }); + + it("multiplyDragByParticleVelocity property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.multiplyDragByParticleVelocity = true; + expect(lvl.multiplyDragByParticleVelocity).to.eq(true); + lvl.multiplyDragByParticleVelocity = false; + expect(lvl.multiplyDragByParticleVelocity).to.eq(false); + }); + + it("space property", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.space = ParticleSimulationSpace.World; + expect(lvl.space).to.eq(ParticleSimulationSpace.World); + lvl.space = ParticleSimulationSpace.Local; + expect(lvl.space).to.eq(ParticleSimulationSpace.Local); + }); + + it("limit with Constant mode", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.limit = new ParticleCompositeCurve(5); + expect(lvl.limit.mode).to.eq(ParticleCurveMode.Constant); + expect(lvl.limit.constant).to.eq(5); + }); + + it("limit with TwoConstants mode", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.limit = new ParticleCompositeCurve(2, 8); + expect(lvl.limit.mode).to.eq(ParticleCurveMode.TwoConstants); + expect(lvl.limit.constantMin).to.eq(2); + expect(lvl.limit.constantMax).to.eq(8); + }); + + it("limit with Curve mode", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const curve = new ParticleCurve(new CurveKey(0, 10), new CurveKey(1, 0)); + lvl.limit = new ParticleCompositeCurve(curve); + expect(lvl.limit.mode).to.eq(ParticleCurveMode.Curve); + }); + + it("limit with TwoCurves mode", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + const curveMin = new ParticleCurve(new CurveKey(0, 2), new CurveKey(1, 0)); + const curveMax = new ParticleCurve(new CurveKey(0, 10), new CurveKey(1, 5)); + lvl.limit = new ParticleCompositeCurve(curveMin, curveMax); + expect(lvl.limit.mode).to.eq(ParticleCurveMode.TwoCurves); + }); + + it("_isRandomMode returns false for Constant", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.limit = new ParticleCompositeCurve(5); + expect(lvl._isRandomMode()).to.eq(false); + }); + + it("_isRandomMode returns true for TwoConstants", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.limit = new ParticleCompositeCurve(2, 8); + expect(lvl._isRandomMode()).to.eq(true); + }); + + it("_isRandomMode with separateAxes", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.separateAxes = true; + lvl.limitX = new ParticleCompositeCurve(1, 5); + lvl.limitY = new ParticleCompositeCurve(1, 5); + lvl.limitZ = new ParticleCompositeCurve(1, 5); + expect(lvl._isRandomMode()).to.eq(true); + + // Mixed modes: not all random + lvl.limitZ = new ParticleCompositeCurve(5); + expect(lvl._isRandomMode()).to.eq(false); + }); + + it("enabling module triggers shader update without error", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.enabled = true; + lvl.limit = new ParticleCompositeCurve(5); + lvl.dampen = 0.8; + lvl.drag = new ParticleCompositeCurve(0.5); + + // Should not throw when updating shader data + particleRenderer.generator.play(); + expect(() => { + //@ts-ignore + engine._vSyncCount = Infinity; + //@ts-ignore + engine._time._lastSystemTime = 0; + let times = 0; + performance.now = function () { + times++; + return times * 100; + }; + for (let i = 0; i < 10; ++i) { + engine.update(); + } + }).to.not.throw(); + }); + + it("separateAxes with curve mode triggers shader update without error", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.enabled = true; + lvl.separateAxes = true; + lvl.limitX = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 10), new CurveKey(1, 2))); + lvl.limitY = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 8), new CurveKey(1, 1))); + lvl.limitZ = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 5), new CurveKey(1, 0))); + lvl.dampen = 0.5; + + particleRenderer.generator.play(); + expect(() => { + //@ts-ignore + engine._vSyncCount = Infinity; + //@ts-ignore + engine._time._lastSystemTime = 0; + let times = 0; + performance.now = function () { + times++; + return times * 100; + }; + for (let i = 0; i < 10; ++i) { + engine.update(); + } + }).to.not.throw(); + }); + + it("drag with curve mode triggers shader update without error", function () { + const lvl = particleRenderer.generator.limitVelocityOverLifetime; + lvl.enabled = true; + lvl.limit = new ParticleCompositeCurve(5); + lvl.drag = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 2))); + lvl.multiplyDragByParticleSize = true; + lvl.multiplyDragByParticleVelocity = true; + + particleRenderer.generator.play(); + expect(() => { + //@ts-ignore + engine._vSyncCount = Infinity; + //@ts-ignore + engine._time._lastSystemTime = 0; + let times = 0; + performance.now = function () { + times++; + return times * 100; + }; + for (let i = 0; i < 10; ++i) { + engine.update(); + } + }).to.not.throw(); + }); +});