-
-
Notifications
You must be signed in to change notification settings - Fork 397
particle support limit velocity over lifetime #2925
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 38 commits
3594b58
80803df
2193eb7
d023c18
d1b3bc2
ba3cf33
e19d3d5
fcc73e5
2024ae1
b913778
86d670f
33cc9dc
b5ab642
30141c4
e1e8bcd
efd53e1
fc66911
c2d491b
639e893
86caf6a
98fc9f6
41755f2
7bbe5a9
5795450
784f01a
c9c418f
ef83408
4cc87be
486e497
dc1f4e1
fdaf404
5c9d35f
c382b20
14e406a
554e5d8
7a207d8
993c6ce
63fd990
24d4600
0802398
dc064eb
ee67e54
429db08
cadf867
cc1ab6b
3455cd6
7d9f2df
47d64aa
0ffa4cf
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,148 @@ | ||
| /** | ||
| * @title Particle Limit Velocity Over Lifetime | ||
| * @category Particle | ||
| */ | ||
| import { | ||
| AssetType, | ||
| BlendMode, | ||
| Burst, | ||
| Camera, | ||
| Color, | ||
| Engine, | ||
| Entity, | ||
| SphereShape, | ||
| Logger, | ||
| ParticleCompositeCurve, | ||
| ParticleCurveMode, | ||
| ParticleGradientMode, | ||
| ParticleMaterial, | ||
| ParticleRenderer, | ||
| ParticleSimulationSpace, | ||
| PostProcess, | ||
| BloomEffect, | ||
| TonemappingEffect, | ||
| Texture2D, | ||
| WebGLEngine | ||
| } from "@galacean/engine"; | ||
| import { initScreenshot, updateForE2E } from "./.mockForE2E"; | ||
|
|
||
| // Create engine | ||
| WebGLEngine.create({ | ||
| canvas: "canvas" | ||
| }).then((engine) => { | ||
| Logger.enable(); | ||
| engine.canvas.resizeByClientSize(); | ||
|
|
||
| const scene = engine.sceneManager.activeScene; | ||
| const rootEntity = scene.createRootEntity(); | ||
| scene.background.solidColor = new Color(0, 0, 0, 1); | ||
|
|
||
| // Camera | ||
| const cameraEntity = rootEntity.createChild("camera"); | ||
| cameraEntity.transform.setPosition(2, 1.43, 30); | ||
| cameraEntity.transform.setRotation(0, 0, 0); | ||
| 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.run(); | ||
|
|
||
| engine.resourceManager | ||
| .load({ | ||
| url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original", | ||
| type: AssetType.Texture2D | ||
| }) | ||
| .then((texture) => { | ||
| createScalarLimitParticle(engine, rootEntity, <Texture2D>texture); | ||
| }); | ||
| }); | ||
|
|
||
| function createScalarLimitParticle(engine: Engine, rootEntity: Entity, texture: Texture2D): void { | ||
| const particleEntity = rootEntity.createChild("ScalarLimit"); | ||
| particleEntity.transform.setPosition(2.006557, 1.43, 12.35); | ||
|
|
||
| const particleRenderer = particleEntity.addComponent(ParticleRenderer); | ||
|
|
||
| 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 generator = particleRenderer.generator; | ||
| generator.useAutoRandomSeed = false; | ||
|
|
||
| const { main, emission, limitVelocityOverLifetime, colorOverLifetime, velocityOverLifetime } = generator; | ||
|
|
||
| // Main module | ||
| 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: fade in then fade out | ||
| 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); | ||
|
|
||
| // Velocity over lifetime (delayed activation) | ||
| setTimeout(() => { | ||
| velocityOverLifetime.enabled = true; | ||
| velocityOverLifetime.velocityX.constant = 1; | ||
| velocityOverLifetime.velocityY.constant = 20; | ||
| velocityOverLifetime.velocityZ.constant = 1; | ||
| console.log("s"); | ||
| }, 3000); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Limit velocity over lifetime | ||
| limitVelocityOverLifetime.enabled = true; | ||
| limitVelocityOverLifetime.separateAxes = true; | ||
|
Comment on lines
+122
to
+123
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify module behavior: enabling is gated by WebGL2.
module_file="$(fd 'LimitVelocityOverLifetimeModule.ts$' | head -n1)"
case_file="$(fd 'particleRenderer-limitVelocity.ts$' | head -n1)"
echo "Module file: ${module_file}"
echo "Case file: ${case_file}"
rg -n -C3 'override set enabled|isWebGL2|_setTransformFeedback' "$module_file"
rg -n -C3 'limitVelocityOverLifetime\.enabled|isWebGL2' "$case_file"
# Optional: inspect similar E2E patterns for capability gating.
rg -n --type=ts -C2 'isWebGL2|TransformFeedback|limitVelocityOverLifetime' e2e/caseRepository: galacean/engine Length of output: 2685 Fail fast when WebGL2 is unavailable so this test actually exercises limit-velocity. The module's 🤖 Prompt for AI Agents |
||
| limitVelocityOverLifetime.limitX = new ParticleCompositeCurve(1); | ||
| limitVelocityOverLifetime.limitY = new ParticleCompositeCurve(1); | ||
| limitVelocityOverLifetime.limitZ = new ParticleCompositeCurve(0); | ||
| // limitVelocityOverLifetime.limit = new ParticleCompositeCurve(1); | ||
| limitVelocityOverLifetime.space = ParticleSimulationSpace.World; | ||
| limitVelocityOverLifetime.dampen = 0.25; | ||
| limitVelocityOverLifetime.drag = new ParticleCompositeCurve(0.0); | ||
| limitVelocityOverLifetime.multiplyDragByParticleSize = true; | ||
| limitVelocityOverLifetime.multiplyDragByParticleVelocity = true; | ||
|
|
||
| // limitVelocityOverLifetime.enabled = true; | ||
| // limitVelocityOverLifetime.separateAxes = false; | ||
| // limitVelocityOverLifetime.limit = new ParticleCompositeCurve(0); | ||
| // limitVelocityOverLifetime.dampen = 1; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { Engine } from "../Engine"; | ||
| 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 { | ||
| /** @internal */ | ||
| _platformPrimitive: IPlatformTransformFeedbackPrimitive; | ||
|
|
||
| private _engine: Engine; | ||
| 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) { | ||
| this._engine = engine; | ||
| this._byteStride = byteStride; | ||
| this._transformFeedback = new TransformFeedback(engine); | ||
| this._transformFeedback.isGCIgnored = true; | ||
| this._platformPrimitive = engine._hardwareRenderer.createPlatformTransformFeedbackPrimitive(); | ||
| } | ||
|
|
||
| /** | ||
| * Resize read and write buffers. | ||
| * @param vertexCount - Number of vertices to allocate | ||
| */ | ||
| resize(vertexCount: number): void { | ||
| this._bindingA?.buffer.destroy(); | ||
| this._bindingB?.buffer.destroy(); | ||
|
|
||
| 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(); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| destroy(): void { | ||
| this._platformPrimitive?.destroy(); | ||
| this._bindingA?.buffer.destroy(); | ||
| this._bindingB?.buffer.destroy(); | ||
| this._transformFeedback?.destroy(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.