Skip to content
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3594b58
feat: particle support limit velocity over lifetime
hhhhkrx Mar 16, 2026
80803df
feat: add Transform Feedback simulation for LimitVelocityOverLifetime
GuoLei1990 Mar 16, 2026
2193eb7
refactor: clean up TransformFeedbackPrimitive and Simulator
GuoLei1990 Mar 16, 2026
d023c18
fix: align TF shader velocity system with correct module interactions
GuoLei1990 Mar 17, 2026
d1b3bc2
refactor: simplify instance count assignment and streamline condition…
GuoLei1990 Mar 17, 2026
ba3cf33
fix: resolve GL conflict when modules dynamically enable during TF mode
GuoLei1990 Mar 17, 2026
e19d3d5
style: format ParticleGenerator
GuoLei1990 Mar 17, 2026
fcc73e5
refactor: use MeshTopology enum and improve TransformFeedback API
GuoLei1990 Mar 17, 2026
2024ae1
refactor: clean architecture for TransformFeedbackPrimitive
GuoLei1990 Mar 17, 2026
b913778
fix: unbind TF buffer after TF object unbind to clear global binding …
GuoLei1990 Mar 17, 2026
86d670f
refactor: polish TransformFeedbackPrimitive API and naming
GuoLei1990 Mar 17, 2026
33cc9dc
refactor: clean up LimitVelocityOverLifetimeModule
GuoLei1990 Mar 17, 2026
b5ab642
fix: rename u_DragConstant to renderer_LVLDragConstant for naming con…
GuoLei1990 Mar 17, 2026
30141c4
refactor: pass VertexBufferBinding to platform layer instead of raw b…
GuoLei1990 Mar 17, 2026
e1e8bcd
fix: clear stale TF buffer binding before bindBufferRange
GuoLei1990 Mar 17, 2026
efd53e1
fix: ensure VAO A/B always correspond to binding A/B
GuoLei1990 Mar 17, 2026
fc66911
fix: guard limit velocity module enablement on WebGL1
hhhhkrx Mar 17, 2026
c2d491b
fix: use full VOL instantaneous value instead of delta
GuoLei1990 Mar 17, 2026
639e893
fix: correct TF render path velocity split for stretched billboard
hhhhkrx Mar 17, 2026
86caf6a
refactor: move buffer layout to ParticleBufferUtils, rename TF to Fee…
GuoLei1990 Mar 17, 2026
98fc9f6
refactor: extract TransformFeedbackSimulator as reusable base
GuoLei1990 Mar 17, 2026
41755f2
fix: store TF position in simulation space instead of baked world coords
GuoLei1990 Mar 17, 2026
7bbe5a9
fix: persist world-space FOL into base velocity like gravity
hhhhkrx Mar 17, 2026
5795450
refactor: rename _useTFMode to _useTransformFeedback, clean up comments
GuoLei1990 Mar 17, 2026
784f01a
perf: cache simulator reference in writeParticleData
GuoLei1990 Mar 17, 2026
c9c418f
fix: project VOL into LVL target space for dampen and drag
hhhhkrx Mar 17, 2026
ef83408
refactor: remove dead drag code from non-TF render path
GuoLei1990 Mar 17, 2026
4cc87be
perf: reuse cached invWorldRotation in position integration
GuoLei1990 Mar 17, 2026
486e497
refactor: clean up TF naming in ParticleGenerator
GuoLei1990 Mar 17, 2026
dc1f4e1
refactor: rename volVelocity to instantVOLVelocity for clarity
hhhhkrx Mar 17, 2026
fdaf404
refactor: move TF pass after _updateShaderData to avoid duplicate uni…
GuoLei1990 Mar 17, 2026
5c9d35f
fix: guard drag curve min with RENDERER_LVL_DRAG_IS_RANDOM_TWO macro
hhhhkrx Mar 17, 2026
c382b20
refactor: simplify ParticleGenerator feedback code
GuoLei1990 Mar 17, 2026
14e406a
refactor: rename abbreviated variables in LVL shader for clarity
hhhhkrx Mar 17, 2026
554e5d8
refactor: simplify _addFeedbackParticle with Vector3 API
GuoLei1990 Mar 17, 2026
7a207d8
refactor: merge feedback/non-feedback instance buffer upload paths
GuoLei1990 Mar 17, 2026
993c6ce
style: format ParticleGenerator
GuoLei1990 Mar 17, 2026
63fd990
refactor: rename particle_transform_feedback_update to particle_feedb…
GuoLei1990 Mar 17, 2026
24d4600
fix: make e2e case deterministic with fixed random seed
GuoLei1990 Mar 17, 2026
0802398
test: update e2e screenshots for particle cases
GuoLei1990 Mar 17, 2026
dc064eb
fix: move a_Random2 declaration after FOL include to fix missing attr…
GuoLei1990 Mar 17, 2026
ee67e54
fix: use CPU-side macros for a_Random2 conditional declaration
GuoLei1990 Mar 17, 2026
429db08
perf: reuse instance VertexBufferBinding instead of creating per frame
GuoLei1990 Mar 17, 2026
cadf867
fix: use a_Random2.w instead of a_Random0.x for drag random factor
hhhhkrx Mar 18, 2026
cc1ab6b
fix: fix ray and plane when ray origin is on the plane and parallel (…
singlecoder Mar 16, 2026
3455cd6
chore: release v2.0.0-alpha.15
cptbtptpbcptdtptp Mar 17, 2026
7d9f2df
Fix compont props clone bug (#2926)
cptbtptpbcptdtptp Mar 18, 2026
47d64aa
fix: preserve feedback buffer data on resize via GPU buffer copy
GuoLei1990 Mar 18, 2026
0ffa4cf
fix: e2e texture
hhhhkrx Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions e2e/case/particleRenderer-limitVelocity.ts
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);

// Limit velocity over lifetime
limitVelocityOverLifetime.enabled = true;
limitVelocityOverLifetime.separateAxes = true;
Comment on lines +122 to +123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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/case

Repository: galacean/engine

Length of output: 2685


Fail fast when WebGL2 is unavailable so this test actually exercises limit-velocity.

The module's enabled setter silently returns without enabling on WebGL1 (it checks isWebGL2 and returns early). Add an explicit WebGL2 capability guard before line 122 to ensure the test fails or skips if WebGL2 is not available, rather than silently passing without covering the intended transform-feedback behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/case/particleRenderer-limitVelocity.ts` around lines 122 - 123, Before
enabling limitVelocityOverLifetime, add an explicit WebGL2 capability guard that
checks the renderer/context for WebGL2 (e.g. renderer.capabilities.isWebGL2 or
gl instanceof WebGL2RenderingContext) and fail or skip the test if WebGL2 is not
available; specifically, insert the guard just before the lines that set
limitVelocityOverLifetime.enabled and .separateAxes so the test does not
silently no-op on WebGL1—use a clear early exit such as throwing an
Error("WebGL2 required for limit-velocity test") or invoking the test runner's
skip mechanism.

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;
}
6 changes: 6 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ export const E2E_CONFIG = {
threshold: 0,
diffPercentage: 0.1630209
},
limitVelocityOverLifetime: {
category: "Particle",
caseFileName: "particleRenderer-limitVelocity",
threshold: 0,
diffPercentage: 0.15
},
textureSheetAnimation: {
category: "Particle",
caseFileName: "particleRenderer-textureSheetAnimation",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/graphic/Buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/graphic/TransformFeedback.ts
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();
}
}
143 changes: 143 additions & 0 deletions packages/core/src/graphic/TransformFeedbackPrimitive.ts
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();
}

/**
* 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();
}
}
Loading
Loading