-
-
Notifications
You must be signed in to change notification settings - Fork 401
feat(particle): add orbital radial velocity over lifetime #3049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/2.0
Are you sure you want to change the base?
Changes from 29 commits
f65694a
a5a081e
28f8b29
127c994
e2fbf0e
9573247
29b279d
b7d6eec
141aacf
a4e9e0e
a741ef6
3277c2e
ca53c9d
5a09858
23cbe79
45e97ed
9dd762a
196cbff
fbeecc8
ea164c4
5867ea6
0530fd1
65cc91b
4e2d79f
7352457
fcf3717
1395f99
d5f09e9
8bcfc64
829b679
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /** | ||
| * @title Particle Velocity Orbital Constant | ||
| * @category Particle | ||
| */ | ||
| import { | ||
| AssetType, | ||
| BlendMode, | ||
| Camera, | ||
| Color, | ||
| ConeShape, | ||
| Engine, | ||
| Entity, | ||
| ParticleCompositeCurve, | ||
| ParticleMaterial, | ||
| ParticleRenderer, | ||
| ParticleSimulationSpace, | ||
| Texture2D, | ||
| Vector3, | ||
| WebGLEngine, | ||
| WebGLMode | ||
| } from "@galacean/engine"; | ||
| import { initScreenshot, updateForE2E } from "./.mockForE2E"; | ||
|
|
||
| WebGLEngine.create({ | ||
| canvas: "canvas", | ||
| graphicDeviceOptions: { webGLMode: WebGLMode.WebGL2 } | ||
| }).then((engine) => { | ||
| engine.canvas.resizeByClientSize(); | ||
|
|
||
| const scene = engine.sceneManager.activeScene; | ||
| const rootEntity = scene.createRootEntity(); | ||
| scene.background.solidColor = new Color(0.01, 0.01, 0.012, 1); | ||
|
|
||
| const cameraEntity = rootEntity.createChild("camera"); | ||
| cameraEntity.transform.setPosition(30.8, 40.8, -49.3); | ||
| cameraEntity.transform.lookAt(new Vector3(5.8, 7.7, 7.1)); | ||
| const camera = cameraEntity.addComponent(Camera); | ||
| camera.fieldOfView = 38; | ||
|
|
||
| engine.resourceManager | ||
| .load<Texture2D>({ | ||
| url: "https://mdn.alipayobjects.com/huamei_9ahbho/afts/img/A*OiP_RLwuFqAAAAAAQBAAAAgAegDwAQ/original", | ||
| type: AssetType.Texture | ||
| }) | ||
| .then((texture) => { | ||
| createOrbitalConstantParticle(engine, rootEntity, texture); | ||
|
|
||
| updateForE2E(engine, 160, 31); | ||
| initScreenshot(engine, camera); | ||
| }); | ||
| }); | ||
|
|
||
| function createOrbitalConstantParticle(engine: Engine, rootEntity: Entity, texture: Texture2D): void { | ||
| const particleEntity = new Entity(engine, "VelocityOrbitalConstant"); | ||
|
|
||
| const particleRenderer = particleEntity.addComponent(ParticleRenderer); | ||
| const generator = particleRenderer.generator; | ||
| generator.randomSeed = 0; | ||
|
|
||
| const material = new ParticleMaterial(engine); | ||
| material.baseColor = new Color(0.35, 0.8, 1.0, 0.72); | ||
| material.baseTexture = texture; | ||
| material.blendMode = BlendMode.Additive; | ||
| particleRenderer.setMaterial(material); | ||
|
|
||
| const { main, emission, velocityOverLifetime } = generator; | ||
|
|
||
| main.duration = 5; | ||
| main.isLoop = true; | ||
| main.startLifetime.constant = 5; | ||
| main.startSpeed.constant = 5; | ||
| main.startSize.constant = 0.7; | ||
| main.gravityModifier.constant = 0; | ||
| main.simulationSpace = ParticleSimulationSpace.Local; | ||
| main.maxParticles = 1000; | ||
|
|
||
| emission.rateOverTime.constant = 10; | ||
| const shape = new ConeShape(); | ||
| shape.radius = 1; | ||
| shape.angle = 25; | ||
| shape.randomDirectionAmount = 0; | ||
| shape.rotation.set(90, 0, 0); | ||
| emission.shape = shape; | ||
|
|
||
| velocityOverLifetime.enabled = true; | ||
| velocityOverLifetime.space = ParticleSimulationSpace.Local; | ||
| velocityOverLifetime.velocityX = new ParticleCompositeCurve(1); | ||
| velocityOverLifetime.velocityY = new ParticleCompositeCurve(10); | ||
| velocityOverLifetime.velocityZ = new ParticleCompositeCurve(1); | ||
| velocityOverLifetime.orbitalX = new ParticleCompositeCurve(1); | ||
| velocityOverLifetime.orbitalY = new ParticleCompositeCurve(1); | ||
| velocityOverLifetime.orbitalZ = new ParticleCompositeCurve(1); | ||
| velocityOverLifetime.offset = new Vector3(2, 0, 0); | ||
| velocityOverLifetime.radial = new ParticleCompositeCurve(5); | ||
|
|
||
| rootEntity.addChild(particleEntity); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ export class ParticleGenerator { | |
| private static _tempVector30 = new Vector3(); | ||
| private static _tempVector31 = new Vector3(); | ||
| private static _tempVector32 = new Vector3(); | ||
| private static _tempVector33 = new Vector3(); | ||
| private static _tempMat = new Matrix(); | ||
| private static _tempColor = new Color(); | ||
| private static _tempQuat0 = new Quaternion(); | ||
|
|
@@ -680,17 +681,16 @@ export class ParticleGenerator { | |
| * @internal | ||
| */ | ||
| _setTransformFeedback(): void { | ||
| const needed = | ||
| const needed = !!( | ||
| this._renderer.engine._hardwareRenderer.isWebGL2 && | ||
| (this.limitVelocityOverLifetime.enabled || | ||
| this.noise.enabled || | ||
| this.subEmitters._hasSubEmitterOfType(ParticleSubEmitterType.Death)); | ||
| (this.limitVelocityOverLifetime?.enabled || | ||
| this.noise?.enabled || | ||
| this.velocityOverLifetime?._needTransformFeedback() || | ||
| this.subEmitters?._hasSubEmitterOfType(ParticleSubEmitterType.Death)) | ||
| ); | ||
| if (needed === this._useTransformFeedback) return; | ||
| this._useTransformFeedback = needed; | ||
|
|
||
| // Switching TF mode invalidates all active particle state: feedback buffers and instance | ||
| // buffer layout are incompatible between the two paths. Clear rather than show a one-frame | ||
| // jump; new particles will fill in naturally from the next emit cycle. | ||
| this._clearActiveParticles(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Docs] The comment that used to sit above this line ("Switching TF mode invalidates all active particle state: feedback buffers and instance buffer layout are incompatible between the two paths. Clear rather than show a one-frame jump...") was deleted, but it's still accurate and explains a non-obvious invariant — without it, a future reader could "optimize away" this clear and reintroduce the buffer-incompatibility bug it prevents. Please restore it. |
||
|
|
||
| if (needed) { | ||
|
|
@@ -785,7 +785,12 @@ export class ParticleGenerator { | |
| renderer._setDirtyFlagFalse(ParticleUpdateFlags.TransformVolume); | ||
| } | ||
|
|
||
| this._addGravityToBounds(maxLifetime, transformedBounds, bounds); | ||
| if (this._useOrbitalBounds()) { | ||
| bounds.min.copyFrom(transformedBounds.min); | ||
| bounds.max.copyFrom(transformedBounds.max); | ||
| } else { | ||
| this._addGravityToBounds(maxLifetime, transformedBounds, bounds); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -816,7 +821,9 @@ export class ParticleGenerator { | |
| } | ||
|
|
||
| const maxLifetime = this.main.startLifetime._getMax(); | ||
| this._addGravityToBounds(maxLifetime, bounds, bounds); | ||
| if (!this._useOrbitalBounds()) { | ||
| this._addGravityToBounds(maxLifetime, bounds, bounds); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1018,12 +1025,7 @@ export class ParticleGenerator { | |
|
|
||
| // Velocity random | ||
| const velocityOverLifetime = this.velocityOverLifetime; | ||
| if ( | ||
| velocityOverLifetime.enabled && | ||
| velocityOverLifetime.velocityX.mode === ParticleCurveMode.TwoConstants && | ||
| velocityOverLifetime.velocityY.mode === ParticleCurveMode.TwoConstants && | ||
| velocityOverLifetime.velocityZ.mode === ParticleCurveMode.TwoConstants | ||
| ) { | ||
| if (velocityOverLifetime.enabled && velocityOverLifetime._isRandomMode()) { | ||
| const rand = velocityOverLifetime._velocityRand; | ||
| instanceVertices[offset + 24] = rand.random(); | ||
| instanceVertices[offset + 25] = rand.random(); | ||
|
|
@@ -1628,6 +1630,7 @@ export class ParticleGenerator { | |
| _tempVector22: velMinMaxZ, | ||
| _tempVector30: worldOffsetMin, | ||
| _tempVector31: worldOffsetMax, | ||
| _tempVector32: noiseBoundsExtents, | ||
| _tempMat: rotateMat | ||
| } = ParticleGenerator; | ||
| worldOffsetMin.set(0, 0, 0); | ||
|
|
@@ -1701,30 +1704,115 @@ export class ParticleGenerator { | |
| } | ||
| } | ||
|
|
||
| const { noise } = this; | ||
| this._getNoiseBoundsExtents(maxLifetime, noiseBoundsExtents); | ||
|
|
||
| const needTransformFeedback = velocityOverLifetime._needTransformFeedback(); | ||
| const orbitalActive = needTransformFeedback && velocityOverLifetime._isOrbitalActive(); | ||
| if (needTransformFeedback) { | ||
| const offset = velocityOverLifetime.offset; | ||
| let radialReach = 0; | ||
| if (velocityOverLifetime._isRadialActive()) { | ||
| this._getExtremeValueFromZero(velocityOverLifetime.radial, velMinMaxX); | ||
| radialReach = Math.max(Math.abs(velMinMaxX.x), Math.abs(velMinMaxX.y)) * maxLifetime; | ||
| } | ||
| if (orbitalActive) { | ||
| const dx = Math.max(Math.abs(min.x - offset.x), Math.abs(max.x - offset.x)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Dedup] These three lines + the |
||
| const dy = Math.max(Math.abs(min.y - offset.y), Math.abs(max.y - offset.y)); | ||
| const dz = Math.max(Math.abs(min.z - offset.z), Math.abs(max.z - offset.z)); | ||
| const worldReach = this._getRangeReach(worldOffsetMin, worldOffsetMax); | ||
| const noiseReach = this._getVectorReach(noiseBoundsExtents); | ||
| const gravityReach = this._getGravityBoundsReach(maxLifetime); | ||
| const reach = Math.sqrt(dx * dx + dy * dy + dz * dz) + worldReach + noiseReach + gravityReach + radialReach; | ||
| min.set( | ||
| Math.min(min.x, offset.x - reach), | ||
| Math.min(min.y, offset.y - reach), | ||
| Math.min(min.z, offset.z - reach) | ||
| ); | ||
| max.set( | ||
| Math.max(max.x, offset.x + reach), | ||
| Math.max(max.y, offset.y + reach), | ||
| Math.max(max.z, offset.z + reach) | ||
| ); | ||
| } else if (radialReach > 0) { | ||
| min.set(min.x - radialReach, min.y - radialReach, min.z - radialReach); | ||
| max.set(max.x + radialReach, max.y + radialReach, max.z + radialReach); | ||
| } | ||
| } | ||
|
|
||
| out.transform(rotateMat); | ||
| min.add(worldOffsetMin); | ||
| max.add(worldOffsetMax); | ||
| if (!orbitalActive) { | ||
| min.add(worldOffsetMin); | ||
| max.add(worldOffsetMax); | ||
|
|
||
| // Noise module impact: noise output is normalized to [-1, 1], | ||
| // max displacement = |strength_max| | ||
| const { noise } = this; | ||
| if (noise.enabled) { | ||
| let noiseMaxX: number, noiseMaxY: number, noiseMaxZ: number; | ||
| if (noise.separateAxes) { | ||
| noiseMaxX = Math.abs(noise.strengthX._getMax()); | ||
| noiseMaxY = Math.abs(noise.strengthY._getMax()); | ||
| noiseMaxZ = Math.abs(noise.strengthZ._getMax()); | ||
| } else { | ||
| noiseMaxX = noiseMaxY = noiseMaxZ = Math.abs(noise.strengthX._getMax()); | ||
| if (noise.enabled) { | ||
| min.set(min.x - noiseBoundsExtents.x, min.y - noiseBoundsExtents.y, min.z - noiseBoundsExtents.z); | ||
| max.set(max.x + noiseBoundsExtents.x, max.y + noiseBoundsExtents.y, max.z + noiseBoundsExtents.z); | ||
| } | ||
| min.set(min.x - noiseMaxX, min.y - noiseMaxY, min.z - noiseMaxZ); | ||
| max.set(max.x + noiseMaxX, max.y + noiseMaxY, max.z + noiseMaxZ); | ||
| } | ||
|
|
||
| min.add(worldPosition); | ||
| max.add(worldPosition); | ||
| } | ||
|
|
||
| private _useOrbitalBounds(): boolean { | ||
| const { velocityOverLifetime } = this; | ||
| return velocityOverLifetime._needTransformFeedback() && velocityOverLifetime._isOrbitalActive(); | ||
| } | ||
|
|
||
| private _getNoiseBoundsExtents(maxLifetime: number, out: Vector3): void { | ||
| const { noise } = this; | ||
| if (!noise.enabled) { | ||
| out.set(0, 0, 0); | ||
| return; | ||
| } | ||
|
|
||
| let noiseMaxX: number, noiseMaxY: number, noiseMaxZ: number; | ||
| if (noise.separateAxes) { | ||
| noiseMaxX = this._getCurveMagnitudeFromZero(noise.strengthX); | ||
| noiseMaxY = this._getCurveMagnitudeFromZero(noise.strengthY); | ||
| noiseMaxZ = this._getCurveMagnitudeFromZero(noise.strengthZ); | ||
| } else { | ||
| noiseMaxX = noiseMaxY = noiseMaxZ = this._getCurveMagnitudeFromZero(noise.strengthX); | ||
| } | ||
| out.set(noiseMaxX * maxLifetime, noiseMaxY * maxLifetime, noiseMaxZ * maxLifetime); | ||
| } | ||
|
|
||
| private _getGravityBoundsReach(maxLifetime: number): number { | ||
| const modifierMinMax = ParticleGenerator._tempVector20; | ||
| this._getExtremeValueFromZero(this.main.gravityModifier, modifierMinMax); | ||
|
|
||
| const coefficient = 0.5 * maxLifetime * maxLifetime; | ||
| const minGravityEffect = modifierMinMax.x * coefficient; | ||
| const maxGravityEffect = modifierMinMax.y * coefficient; | ||
| const { x, y, z } = this._renderer.scene.physics.gravity; | ||
|
|
||
| const gravityBoundsExtents = ParticleGenerator._tempVector33; | ||
| gravityBoundsExtents.set( | ||
| Math.max(Math.abs(x * minGravityEffect), Math.abs(x * maxGravityEffect)), | ||
| Math.max(Math.abs(y * minGravityEffect), Math.abs(y * maxGravityEffect)), | ||
| Math.max(Math.abs(z * minGravityEffect), Math.abs(z * maxGravityEffect)) | ||
| ); | ||
| return this._getVectorReach(gravityBoundsExtents); | ||
| } | ||
|
|
||
| private _getRangeReach(min: Vector3, max: Vector3): number { | ||
| const x = Math.max(Math.abs(min.x), Math.abs(max.x)); | ||
| const y = Math.max(Math.abs(min.y), Math.abs(max.y)); | ||
| const z = Math.max(Math.abs(min.z), Math.abs(max.z)); | ||
| return Math.sqrt(x * x + y * y + z * z); | ||
| } | ||
|
|
||
| private _getVectorReach(value: Vector3): number { | ||
| return Math.sqrt(value.x * value.x + value.y * value.y + value.z * value.z); | ||
| } | ||
|
|
||
| private _getCurveMagnitudeFromZero(curve: ParticleCompositeCurve): number { | ||
| const minMax = ParticleGenerator._tempVector20; | ||
| this._getExtremeValueFromZero(curve, minMax); | ||
| return Math.max(Math.abs(minMax.x), Math.abs(minMax.y)); | ||
| } | ||
|
|
||
| private _addGravityToBounds(maxLifetime: number, origin: BoundingBox, out: BoundingBox): void { | ||
| const { min: originMin, max: originMax } = origin; | ||
| const modifierMinMax = ParticleGenerator._tempVector20; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Nit] Unrelated
let→constlint fix that slipped into this feature PR — harmless, but ideally kept out of the diff.