Skip to content

feat(particle): add orbital radial velocity over lifetime#3049

Merged
GuoLei1990 merged 37 commits into
galacean:dev/2.0from
hhhhkrx:feat/particle-vol-orbital-radial
Jul 10, 2026
Merged

feat(particle): add orbital radial velocity over lifetime#3049
GuoLei1990 merged 37 commits into
galacean:dev/2.0from
hhhhkrx:feat/particle-vol-orbital-radial

Conversation

@hhhhkrx

@hhhhkrx hhhhkrx commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds orbital and radial support to the Particle Velocity over Lifetime module, including transform-feedback simulation for position-dependent velocity terms.

Details

  • Adds orbitalX, orbitalY, orbitalZ, radial, and offset properties to VelocityOverLifetimeModule.
  • Enables transform feedback when orbital or radial velocity is active, because these terms depend on the particle's current position.
  • Uploads constant and curve shader data for orbital/radial modes and keeps the Velocity over Lifetime macro split so linear-only paths remain guarded separately.
  • Applies orbital/radial in the transform-feedback shader by integrating base velocity first, then rotating/growing the relative position around the configured offset.
  • Expands particle bounds conservatively when orbital/radial terms are active.
  • Adds unit coverage for property defaults, active detection, TF switching, shader data upload, curve/constant switching, clone behavior, and disabled-module behavior.
  • Adds an e2e case with fixed random seed and Unity-like Velocity over Lifetime settings.

Validation

  • vitest tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts --run
  • node packages/shader-compiler/bundler/cli.js packages/shader/src/Shaders packages/shader/compiledShaders --clean --emit-index --emit-sources
  • BUILD_TYPE=MODULE NODE_ENV=release rollup -c
  • BUILD_TYPE=UMD NODE_ENV=release rollup -c
  • Local Playwright visual check for particleRenderer-velocity-orbital-constant completed without page errors.

Summary by CodeRabbit

  • New Features
    • Added orbital (X/Y/Z) and radial velocity support to particle velocity-over-lifetime, including an orbit-center offset.
    • Added WebGL2 transform-feedback rendering for both constant and curve-based orbital/radial modes.
  • Bug Fixes
    • Improved transformed-bounds calculations for orbital/radial motion to better account for the orbit-center offset.
  • Tests
    • Expanded unit tests covering activation, transform-feedback requirements, shader macro/uniform selection, and cloning behavior.
    • Added an E2E particle case (velocityOrbitalConstant) with screenshot verification and updated E2E configuration.

@hhhhkrx hhhhkrx requested a review from GuoLei1990 June 24, 2026 03:43
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds orbital and radial particle velocity support, updates transform-feedback and bounds handling, and extends unit and E2E coverage for the new particle behavior.

Changes

Orbital and radial velocity over lifetime

Layer / File(s) Summary
Module state and shader upload
packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
Adds orbital and radial properties, default initialization, transform-feedback dirty handling, and shader uploads for orbital/radial modes, uniforms, and macros.
Generator transform feedback and bounds
packages/core/src/particle/ParticleGenerator.ts
Updates transform-feedback gating, random velocity setup, and transformed bounds expansion for orbital offset and radial reach.
Module tests for orbital and radial behavior
tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts
Covers defaults, active-state detection, offset copying, transform-feedback gating, shader mode selection, cloning, and dirty-bounds behavior after offset changes.
E2E orbital constant case and config
e2e/case/particleRenderer-velocity-orbital-constant.ts, e2e/config.ts
Adds a WebGL2 E2E particle case and registers it in the E2E config.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Suggested labels

enhancement, shader

Suggested reviewers

  • GuoLei1990
  • cptbtptpbcptdtptp

Poem

🐇 I hop जहां the particles twirl,
Orbiting softly in a glowing whirl.
Radial breezes guide their sway,
And a screenshot seals the display.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding orbital and radial velocity support to particle velocity over lifetime.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hhhhkrx hhhhkrx self-assigned this Jun 24, 2026
@hhhhkrx hhhhkrx added effect Effect related functions particle labels Jun 24, 2026
@hhhhkrx hhhhkrx added this to Particle Jun 24, 2026
@hhhhkrx hhhhkrx added this to the 2.0 milestone Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.72093% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.48%. Comparing base (c7f89db) to head (2d94f63).
⚠️ Report is 2 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
...case/particleRenderer-velocity-orbital-constant.ts 0.00% 64 Missing and 1 partial ⚠️
packages/core/src/particle/ParticleGenerator.ts 91.34% 9 Missing ⚠️
e2e/config.ts 0.00% 6 Missing ⚠️
...ore/src/particle/modules/ParticleCompositeCurve.ts 91.48% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3049      +/-   ##
===========================================
+ Coverage    79.36%   79.48%   +0.11%     
===========================================
  Files          903      904       +1     
  Lines       100628   101104     +476     
  Branches     11256    11355      +99     
===========================================
+ Hits         79866    80360     +494     
+ Misses       20578    20560      -18     
  Partials       184      184              
Flag Coverage Δ
unittests 79.48% <83.72%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@augmentcode

augmentcode Bot commented Jun 24, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Extends the Particle VelocityOverLifetimeModule with orbital (XYZ) and radial velocity terms that depend on particle position, and wires these terms into the WebGL2 transform-feedback simulation path.

Changes:

  • Adds orbitalX, orbitalY, orbitalZ, radial, and offset properties to velocity-over-lifetime.
  • Introduces shader macros/properties and uploads constant/curve data for orbital and radial modes.
  • Updates ParticleGenerator._setTransformFeedback() to enable TF when orbital/radial terms are active (WebGL2-only).
  • Applies conservative particle bounds expansion when orbital/radial are active to avoid culling.
  • Adds a new e2e case (particleRenderer-velocity-orbital-constant) and registers it in e2e/config.ts.
  • Adds unit tests covering defaults, active detection, TF switching, shader data upload, and clone behavior.

Technical Notes: Orbital/radial are implemented only on the transform-feedback path (WebGL2), while keeping the existing linear-only velocity path guarded separately to avoid unnecessary TF.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

/**
* The center of orbit for orbital/radial velocity, in the system's local space.
*/
get offset(): Vector3 {

@augmentcode augmentcode Bot Jun 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/core/src/particle/modules/VelocityOverLifetimeModule.ts:206 (get offset()): since this returns the internal Vector3, in-place mutations like vol.offset.set(...) won’t trigger _onGeneratorParamsChanged(), so the shader may keep a stale offset until something else dirties the generator.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}

private _isCurveMode(curve: ParticleCompositeCurve): boolean {
return curve.mode === ParticleCurveMode.Curve || curve.mode === ParticleCurveMode.TwoCurves;

@augmentcode augmentcode Bot Jun 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/core/src/particle/modules/VelocityOverLifetimeModule.ts:413 (_isCurveMode): treating ParticleCurveMode.TwoCurves as “curve mode” here looks inconsistent with the lack of any random-min upload/macro for orbital/radial, so TwoCurves (and TwoConstants) likely behave as “max only” rather than true random range.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts (1)

162-165: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Prefer tolerance checks for shader float assertions.

These assertions compare decimal shader values with exact equality. Since shader data commonly round-trips through float32, this can cause intermittent failures on different environments.

Suggested fix
-    expect(orbital.x).to.eq(0.77);
-    expect(orbital.y).to.eq(1.02);
-    expect(orbital.z).to.eq(0.94);
+    expect(orbital.x).to.be.closeTo(0.77, 1e-6);
+    expect(orbital.y).to.be.closeTo(1.02, 1e-6);
+    expect(orbital.z).to.be.closeTo(0.94, 1e-6);
@@
-    expect(orbital.x).to.eq(0.77);
-    expect(orbital.y).to.eq(1.02);
-    expect(orbital.z).to.eq(0.94);
+    expect(orbital.x).to.be.closeTo(0.77, 1e-6);
+    expect(orbital.y).to.be.closeTo(1.02, 1e-6);
+    expect(orbital.z).to.be.closeTo(0.94, 1e-6);

Also applies to: 186-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts` around lines 162
- 165, The assertions in the VelocityOverLifetimeOrbital.test.ts file use exact
equality checks for floating point shader values (orbital.x, orbital.y,
orbital.z, and the particleRenderer.shaderData.getFloat() call), which can fail
intermittently due to float32 precision variations across environments. Replace
the exact equality assertions with tolerance-based assertions that allow for
small floating point differences. Apply this change to all the assertions
mentioned, including those at lines 186-188, using a small epsilon value
appropriate for float32 comparisons.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/particle/modules/VelocityOverLifetimeModule.ts`:
- Around line 206-216: The offset getter in VelocityOverLifetimeModule returns
the internal mutable Vector3 object directly, allowing callers to mutate it in
place via direct property assignment (e.g., velocityOverLifetime.offset.x = 2)
which bypasses the setter and never triggers _onGeneratorParamsChanged(). To fix
this, the offset Vector3 returned from the getter must be wrapped in a proxy or
use another mechanism to intercept property mutations on x, y, and z properties,
ensuring that any direct modification to those properties still triggers the
_onGeneratorParamsChanged() call to keep orbital/radial bounds and uniforms in
sync.

In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1708-1720: The orbital reach calculation in the
velocityOverLifetime._isOrbitalActive() block does not account for world-space
velocity or force offsets (worldOffsetMin and worldOffsetMax), which can cause
particle bounds to miss the swept direction and cull visible particles. Include
the world-space offset values in the reach calculation by factoring the distance
from the orbital offset to the world-space offset bounds into the reach value
before applying min.set and max.set operations. This ensures the orbital reach
encompasses both the rotational movement around renderer_VOLOffset and any
world-space offset translation.

In `@tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts`:
- Around line 25-44: The WebGLEngine instance is created in the beforeAll hook
but never disposed, causing GPU/WebGL resource leaks. Add an afterAll hook after
the existing beforeAll hook that calls the appropriate disposal method on the
engine instance (typically engine.destroy() or a similar cleanup method) to
properly release resources and prevent destabilization of subsequent test files.
This ensures the engine lifecycle is properly managed with matching setup and
teardown.

---

Nitpick comments:
In `@tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts`:
- Around line 162-165: The assertions in the VelocityOverLifetimeOrbital.test.ts
file use exact equality checks for floating point shader values (orbital.x,
orbital.y, orbital.z, and the particleRenderer.shaderData.getFloat() call),
which can fail intermittently due to float32 precision variations across
environments. Replace the exact equality assertions with tolerance-based
assertions that allow for small floating point differences. Apply this change to
all the assertions mentioned, including those at lines 186-188, using a small
epsilon value appropriate for float32 comparisons.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e0c28792-c579-40d2-a7e7-a3961a027c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 39b4b08 and bc6f703.

⛔ Files ignored due to path filters (5)
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
  • packages/shader/src/ShaderLibrary/Particle/Module/VelocityOverLifetime.glsl is excluded by !**/*.glsl
  • packages/shader/src/ShaderLibrary/Particle/ParticleFeedback.glsl is excluded by !**/*.glsl
  • packages/shader/src/ShaderLibrary/Particle/ParticleVert.glsl is excluded by !**/*.glsl
  • packages/shader/src/Shaders/Effect/ParticleFeedback.shader is excluded by !**/*.shader
📒 Files selected for processing (5)
  • e2e/case/particleRenderer-velocity-orbital-constant.ts
  • e2e/config.ts
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
  • tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts

Comment thread packages/core/src/particle/modules/VelocityOverLifetimeModule.ts Outdated
Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
Comment thread tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc6f7030d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +206 to +208
get offset(): Vector3 {
return this._offset;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate bounds when offset is mutated in place

When callers use the usual mutable-vector pattern (velocityOverLifetime.offset.set(...) or assigning components), this getter returns the live Vector3 but no _onValueChanged handler is registered, so the setter and _onGeneratorParamsChanged() are bypassed. The shader can still read the changed offset, but the particle bounds stay based on the old orbit center and can cull live particles; other particle vector properties such as shape transforms hook _onValueChanged for this reason.

Useful? React with 👍 / 👎.

Comment on lines +1716 to +1719
const dx = Math.max(Math.abs(min.x - offset.x), Math.abs(max.x - offset.x));
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 reach = Math.sqrt(dx * dx + dy * dy + dz * dz) + radialReach;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include world-space offsets in orbital bounds

When orbital velocity is combined with world-space VOL/FOL, the feedback shader converts those world-space deltas into simulation space before rotating around the offset, but this reach calculation happens before worldOffsetMin/Max are merged later. For example, an X-only world-space velocity can be swept into Z by the orbital rotation while the bounds only receive the X offset afterward, so particles can be incorrectly culled; fold the accumulated world offsets into the pre-orbit radius before computing reach.

Useful? React with 👍 / 👎.

GuoLei1990

This comment was marked as outdated.

@hhhhkrx hhhhkrx force-pushed the feat/particle-vol-orbital-radial branch 2 times, most recently from 0abcaf9 to 4d2f689 Compare June 24, 2026 04:06
@hhhhkrx hhhhkrx force-pushed the feat/particle-vol-orbital-radial branch from 4d2f689 to f65694a Compare June 24, 2026 04:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 714-716: After calling _migrateActiveParticlesToFeedback() in the
conditional block where previousUseTransformFeedback is false, the GPU instance
buffer still contains the old non-TF compacted layout and needs to be
synchronized with the new feedback slot organization. Add a call to mark the
active particle range as dirty or perform an in-place instance buffer upload
immediately after _migrateActiveParticlesToFeedback() completes to ensure
instance attributes match the reorganized feedback slots.
- Around line 1913-1918: The orbital reach calculation in the _isOrbitalActive()
block computes the bounding sphere for orbital particles without accounting for
gravity displacement that gets applied before orbital rotation. This causes
visible particles to be culled when they move outside the calculated sphere
after gravity is applied. Modify the reach calculation (where Math.sqrt(dx * dx
+ dy * dy + dz * dz) + radialReach is computed) to include the maximum possible
gravity displacement in the distance calculation, ensuring the bounding sphere
encompasses particles after gravity displacement but before orbital velocity
rotation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2229cd18-f79f-4d3e-a62c-59447d39414c

📥 Commits

Reviewing files that changed from the base of the PR and between bc6f703 and 0abcaf9.

⛔ Files ignored due to path filters (5)
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
  • packages/shader/src/ShaderLibrary/Particle/Module/VelocityOverLifetime.glsl is excluded by !**/*.glsl
  • packages/shader/src/ShaderLibrary/Particle/ParticleFeedback.glsl is excluded by !**/*.glsl
  • packages/shader/src/ShaderLibrary/Particle/ParticleVert.glsl is excluded by !**/*.glsl
  • packages/shader/src/Shaders/Effect/ParticleFeedback.shader is excluded by !**/*.shader
📒 Files selected for processing (5)
  • e2e/case/particleRenderer-velocity-orbital-constant.ts
  • e2e/config.ts
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/modules/VelocityOverLifetimeModule.ts
  • tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • e2e/config.ts
  • e2e/case/particleRenderer-velocity-orbital-constant.ts
  • tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts
  • packages/core/src/particle/modules/VelocityOverLifetimeModule.ts

Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the orbital/radial implementation end to end (TS + GLSL + TF simulation + bounds math). Overall the architecture looks solid: exact rotationByEuler integration in the TF shader instead of a linear ω×r step, conservative bounds composition is self-consistent (gravity/noise/world reach are folded into the orbital reach, and the outer _addGravityToBounds skip correctly avoids double-counting), the macro split (_VOL_LINEAR_MODULE_ENABLED) checks out at every usage site, clone wiring survives entity.clone(), and the deleted ParticleFeedback.glsl was confirmed dead code (never #included).

Found 1 confirmed bug (empirically reproduced with a passing vitest repro in real chromium WebGL2 — see the inline comment on ParticleCompositeCurve.ts), 1 latent-hazard pattern, and a handful of dedup/doc suggestions. Details inline.

}
}
return min;
out.set(min, max);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Bug — confirmed with a running repro] For a Curve/TwoCurves-mode composite whose curve is unassigned or has zero keys, min/max stay undefined here, so _isZero() returns false (undefined === 0) and the curve is classified as active.

Repro (single line, natural "switch mode first, author curve later" order):

vol.enabled = true;
vol.radial.mode = ParticleCurveMode.Curve; // curveMax is still undefined

Consequences, all verified by running the engine in chromium:

  1. _isRadialActive()true → on WebGL2 the generator flips into transform feedback and _clearActiveParticles() kills every live particle — for a curve that evaluates to 0 everywhere.
  2. _updateShaderData then hits radial.curveMax._getTypeArray()TypeError thrown every frame (this crash path doesn't even need WebGL2). Same crash for all-three-orbital-axes Curve mode.

Suggested fix — treat an unauthored curve as zero, matching the ?? 0 convention already used in _evaluateCumulative:

out.set(min ?? 0, max ?? 0);

which fixes _isZero, _getMax, and the NaN-prone _getExtremeValueFromZero consumers in one place.

vitest repro (4/4 assertions pass on this branch, chromium WebGL2)
it("mechanism: curve mode with unassigned curveMax is classified non-zero", () => {
  const vol = particleRenderer.generator.velocityOverLifetime;
  vol.enabled = true;
  expect((vol.radial as any)._isZero()).to.eq(true);
  vol.radial.mode = ParticleCurveMode.Curve;
  expect(vol.radial.curveMax).to.eq(undefined);
  expect((vol.radial as any)._isZero()).to.eq(false); // BUG
  expect(vol._isRadialActive()).to.eq(true);          // BUG
});

it("consequence A: transform feedback flips on and live particles are cleared", () => {
  expect((particleRenderer.generator as any)._useTransformFeedback).to.eq(isWebGL2);
});

it("consequence B: _updateShaderData throws TypeError on undefined curveMax", () => {
  expect(() => {
    particleRenderer.generator._updateShaderData(particleRenderer.shaderData);
  }).to.throw(TypeError);
});

it("control: assigning Constant mode afterwards recovers", () => {
  const vol = particleRenderer.generator.velocityOverLifetime;
  vol.radial.mode = ParticleCurveMode.Constant;
  expect(vol._isRadialActive()).to.eq(false);
  expect(() => {
    particleRenderer.generator._updateShaderData(particleRenderer.shaderData);
  }).to.not.throw();
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified fixed in 8bcfc64 ✅ — rebuilt this branch and ran the suite in chromium WebGL2: the new "unauthored orbital/radial curves stay inactive" regression test covers the exact repro chain (no spurious TF flip, no TypeError), 30/30 passing. Thanks for also adopting the ?? 0 form — it fixes _getMax/_getExtremeValueFromZero for empty curves in the same stroke.

const maxGravityEffect = modifierMinMax.y * coefficient;
const { x, y, z } = this._renderer.scene.physics.gravity;

const gravityBoundsExtents = ParticleGenerator._tempVector32;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Latent hazard] _getGravityBoundsReach grabs ParticleGenerator._tempVector32 as its own scratch — but the caller _calculateTransformedBounds has that same static aliased as noiseBoundsExtents (L1632), writes it at L1707, and reads it at L1723 and L1748.

It happens to be safe today only by accident of ordering/branch shape: the L1723 read executes one line before this function clobbers the vector at L1724, and the L1748 read sits in the mutually-exclusive !orbitalActive branch. The two adjacent lines at L1723-1724 look freely reorderable; swapping them (or supporting orbital+noise in one branch later) silently makes noiseReach read gravity data. No comment or assertion documents the constraint.

Suggestion: give this helper its own temp (_tempVector33) or take an out: Vector3 parameter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified fixed in 8bcfc64 ✅ — _getGravityBoundsReach now uses the dedicated _tempVector33 (confirmed single user via grep), so the noiseBoundsExtents aliasing hazard is gone. Bounds tests (16/16, incl. the orbital gravity-reach case) pass on the updated branch.

#ifdef RENDERER_VOL_ORBITAL_CONSTANT_MODE
vec3 orbital = renderer_VOLOrbitalMaxConst;
#ifdef RENDERER_VOL_ORBITAL_IS_RANDOM_TWO
orbital = mix(renderer_VOLOrbitalMinConst, orbital, attributes.a_Random1.yzw);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Worth a comment in code or PR description] Orbital (and radial, via .y) random-two mode reuses the exact random channel a_Random1.yzw that linear VOL's random-two mode consumes. When a user enables random mode on both linear and orbital/radial simultaneously, each particle's lerp factors are identical — particles that draw a fast linear velocity always also orbit fastest (Unity draws these independently per property).

I traced the instance layout: all 42 float slots are currently assigned, so independent draws would require growing the per-particle layout — the sharing looks like a reasonable trade-off (SOL and Noise already share a_Random0.z), but it's worth documenting the intent here so it doesn't read as an oversight.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Resolved in 8bcfc64 ✅ — the comment documenting the shared random channel (and the instance-layout rationale) is exactly what was needed here.

vec3 getVOLVelocity(Attributes attributes, float normalizedAge) {
vec3 vel = vec3(0.0);
#ifdef _VOL_MODULE_ENABLED
#ifdef _VOL_LINEAR_MODULE_ENABLED

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Dedup] getVOLVelocity is now body-for-body identical to the new evaluateVOLVelocity in VelocityOverLifetime.glsl (which this file already #includes at L48, and the single call site at L142 sits inside the same _VOL_LINEAR_MODULE_ENABLED guard). The local copy can be deleted and the call site switched to evaluateVOLVelocity(attr, normalizedAge) — one less pair of functions to keep in sync on the next VOL change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified fixed in 8bcfc64 ✅ — local getVOLVelocity removed and the call site switched to the shared evaluateVOLVelocity; shader precompile passes on the updated branch.

const orbitalActive = this._isOrbitalActive();
const radialActive = this._isRadialActive();

if (orbitalActive) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Dedup] This orbital 3-axis curve/constant detection + upload block is a structural copy of the linear velocityX/Y/Z block above in the same method (~45 lines, only property/macro names differ). LimitVelocityOverLifetimeModule already factors the same shape into _uploadScalarSpeed/_uploadSeparateAxisSpeeds returning { modeMacro, randomMacro } — the same extraction here would keep the two copies from drifting.


vec3 rel;
if (renderer_SimulationSpace == 0) {
rel = attr.a_FeedbackPosition - renderer_VOLOffset;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Dedup] This rel computation (position relative to orbit center, with the local/world simulation-space branching) is duplicated near-identically in ParticleFeedback.shader (L261-266). The two copies live in files that don't share the function, so a future fix to the offset space conversion (sign/rotation) has to land in both, risking silent divergence between the rendered stretched-billboard velocity and the simulated position. Both files already include VelocityOverLifetime.glsl — a small shared helper taking the position as a parameter would remove the risk.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up with a concrete, verified shape for this — a small helper in VelocityOverLifetime.glsl (next to evaluateVOLOrbital/evaluateVOLRadial, inside the orbital/radial guard):

// Position relative to the orbit center, in the system's local space.
vec3 computeVOLRelativePosition(vec3 position, vec3 simulationWorldPosition, vec4 invWorldRotation) {
    if (renderer_SimulationSpace == 0) {
        return position - renderer_VOLOffset;
    } else {
        return rotationByQuaternions(position - simulationWorldPosition, invWorldRotation) - renderer_VOLOffset;
    }
}

Call sites collapse to:

// ParticleVert.glsl
vec3 rel = computeVOLRelativePosition(attr.a_FeedbackPosition, attr.a_SimulationWorldPosition, invWorldRotation);

// ParticleFeedback.shader
vec3 rel = computeVOLRelativePosition(position, attr.a_SimulationWorldPosition, invWorldRotation);

renderer_SimulationSpace is declared before the #include in both host shaders (ParticleVert.glsl L37, ParticleFeedback.shader L18), so the helper resolves fine. Verified locally: shader precompile passes and all particle unit tests (162/162) stay green with this applied.

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));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Dedup] These three lines + the Math.sqrt(dx*dx + dy*dy + dz*dz) below re-implement exactly what the new _getRangeReach(min, max) helper (L1798, added in this same PR and called two lines later for worldReach) already does — just with an offset shift. Shifting min/max by offset into spare temps and calling _getRangeReach would drop the duplicate.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.


const loaders = ResourceManager._loaders;
for (let key in loaders) {
for (const key in loaders) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Nit] Unrelated letconst lint fix that slipped into this feature PR — harmless, but ideally kept out of the diff.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 d5f09e951 线性推进一个 commit 到 8bcfc647cgh api compare d5f09e951...8bcfc647c 返回 status: ahead, ahead_by: 1, behind_by: 08bcfc647c 父 = d5f09e951071f84be6fa583c8c43000cc0712ee0,确认非 force-push 回退)。唯一改动是 fix: handle empty particle curves,5 文件:ParticleCompositeCurve.ts(+1/-1)+ ParticleGenerator.ts(+2/-1)+ VelocityOverLifetime.glsl(+1/-0)+ ParticleFeedback.shader(+1/-30)+ 测试(+20)。三类改动:??0 修一个真实的 P0 级崩溃 + NaN-bounds latent bug(含 base dev/2.0 就存在的)② feedback shader 删本地 getVOLVelocity 收口到共享 evaluateVOLVelocity(纯去重,byte 等价)③ _tempVector33 消除一处 latent aliasing 隐患。逐路径核对全部正确,新测试反向证伪有效,CI 全绿。无 P0/P1,无新问题。--comment LGTM。

✅ 改动 1 —— ParticleCompositeCurve._getKeyMinMax 空曲线 undefined → 0(逐路径核对 = 真崩溃 + NaN-bounds 双修复)

out.set(min, max)out.set(min ?? 0, max ?? 0)。看似只是"处理空曲线",实为一个实质行为变化:Curve/TwoCurves 模式但 curveMax/curveMin 对象未创建(undefined)的曲线,从"被判为 active"翻转为"判为 inactive"。逐路径核对这是两个真实 bug 的正确修复,非 cosmetic:

  1. 修 P0 级 TypeError 崩溃(_updateShaderData 每帧路径) —— set mode 只设 this._mode不惰性创建 _curveMax(已读 :31-34 确认)。故 new ParticleCompositeCurve(0)(默认 constant,_curveMax=undefined)→ mode = CurvecurveMax 仍是 undefined——这是真实的编辑器/用户路径。

    • 修复前_getKeyMinMax(undefined?.keys, count=0) 返回 out.set(undefined, undefined)_isZero() = undefined===0 && undefined===0 = false_isRadialActive() = !false = true → 进入 _updateShaderDataradialActive 块(VelocityOverLifetimeModule.ts:391-393)→ radial.curveMax._getTypeArray() 命中 undefined._getTypeArray() = TypeError 崩溃。orbital 同理(:342-355 orbitalX.curveMax._getTypeArray())。
    • 修复后out.set(0,0)_isZero() = true_isRadialActive()/_isOrbitalActive() = falseradialActive/orbitalActive 块被跳过 → 不触碰 undefined._getTypeArray() → 无崩溃。语义正确:未创建曲线对象的 Curve 模式曲线在 shader 里求值为 0,判为 inactive 是对的。✅
  2. 修 NaN-bounds latent bug(含 base dev/2.0 就存在的) —— _getMinMax_getExtremeValueFromZeroParticleGenerator.ts:1852-1855)喂 bounds:

    curve._getMinMax(out);
    out.x = Math.min(0, out.x);   // 修复前 Math.min(0, undefined) = NaN
    out.y = Math.max(0, out.y);   // 修复前 Math.max(0, undefined) = NaN

    修复前,任何 Curve/TwoCurves 模式但 curve 对象未创建的 size/velocity/force 曲线喂 bounds 会产出 NaN → 污染整个包围盒。我核了 base dev/2.0:旧 _getMaxKeyValue/_getMinKeyValue 空 keys 同样返回 undefinedorigin/dev/2.0:ParticleCompositeCurve.ts:271-295),_getExtremeValueFromZero 同样 Math.min(0, undefined)——这条 NaN-bounds 是 base 就存在的 latent bug,本 commit 一并修掉。??0 让空曲线贡献 0 extent,保守安全。✅

  3. 只改空曲线路径,authored 曲线 byte 不变 —— ?? 0 仅在 min/maxundefined(无 keys)时生效。有 keys 的曲线(全零 zeroCurve 或非零)走 min = max = keys[0].value 循环,?? 0 不触发,与修复前逐字节相同。无误伤 active 曲线。✅

✅ 改动 2 —— ParticleFeedback.shader 删本地 getVOLVelocity 改用共享 evaluateVOLVelocity(逐路径核对真去重)

删掉 feedback shader 里本地的 getVOLVelocity(30 行),调用点改用 d5f09e951 新增的共享 module evaluateVOLVelocity——与前几轮 getVOLOrbital/getVOLRadial 收口是同一模式(feedback shader 是该 VOL 速度 evaluator 最后的私有副本,ParticleVert.glsl 早已用共享版)。

逐路径核对(不只信 commit message,核 byte 等价 + 共享函数在 scope + 宏门一致 + 零残留 + e2e 反向证伪)

  1. byte 等价 —— 删除的 getVOLVelocity 与 module evaluateVOLVelocityVelocityOverLifetime.glsl:36-60)逐行对照:CONSTANT 分支 renderer_VOLMaxConst + mix(renderer_VOLMinConst, ·, a_Random1.yzw)、CURVE 分支 evaluateParticleCurve(renderer_VOLMaxGradientX/Y/Z) + mix(minVelocity, ·, a_Random1.yzw)——逐字相同(仅局部变量名 velvelocityminVelminVelocity)。✅

  2. 共享函数在 scope —— feedback shader #include "ShaderLibrary/Particle/Module/VelocityOverLifetime.glsl":48)。evaluateVOLVelocity 定义在 module 的 #ifdef _VOL_LINEAR_MODULE_ENABLED(module :13 开、:96 闭)内,call-site vec3 vol = evaluateVOLVelocity(...) 也在 #ifdef _VOL_LINEAR_MODULE_ENABLED(feedback :111-119)内——定义门与调用门逐字一致。✅

  3. 旧版无守卫 vs 新版有守卫——安全 —— 旧 getVOLVelocity#if 守卫(vec3 vel = vec3(0.0) 初始化);新 evaluateVOLVelocity 仅在 _VOL_LINEAR_MODULE_ENABLED 下定义。因唯一 call-site 同样被 _VOL_LINEAR_MODULE_ENABLED 守卫,函数不存在时永不被调用;且 _VOL_LINEAR_MODULE_ENABLED 蕴含 CONSTANT 或 CURVE 二选一(module :4),故 velocity 两分支必有一路赋值,无未初始化读。✅

  4. 零残留 —— git grep getVOLVelocity 8bcfc647c -- packages/shader/** = NONE。真删除无悬挂。✅

✅ 改动 3 —— _tempVector33 消除 gravity/noise 共享 scratch 的 latent aliasing

_getGravityBoundsReachgravityBoundsExtents_tempVector32 改用新增的 _tempVector33_tempVector32_calculateTransformedBounds:1633)绑定 noiseBoundsExtents

逐路径核对(这条正是我 13:33 那轮"temp-vector 别名安全"里评估过的 latent 隐患)

  • orbital 分支:noiseReach = _getVectorReach(noiseBoundsExtents)_getGravityBoundsReach 调用前一行已 snapshot 进 number(:1720-1722),故此前 _tempVector32 复用未污染 noiseReach——我 13:33 判"latent 但当前未触发"是对的。
  • !orbitalActive 分支读 noiseBoundsExtents:1748-1749),但它与 orbital 分支互斥,_getGravityBoundsReach 只在 orbitalActive 时调用——读 noise 的分支永不在 gravity reach 计算后运行。
  • 故这是正确的防御性硬化(消除一处未来重构易踩的共享 scratch 耦合),非当前可观测 bug 的修复。零行为变化、CI 全绿。✅

CI

tip 8bcfc647c 全绿:build(3 OS)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。

注释合规

新增一行 // Orbital/radial random modes share linear VOL random channels to keep the particle instance layout unchanged.——单行 //、无末尾句号 ✓,是准确的 why 注释(记录 orbital 用 a_Random1.yzw/radial 用 a_Random1.y 刻意复用 linear VOL 随机槽以保持实例布局不变的设计约定,非复述代码),已对照实际 a_Random1 用法核实准确。删除的 30 行是函数体,无注释违规。fix: handle empty particle curves commit message 准确。无新增违规。

新增测试反向证伪有效

velocity over lifetime unauthored orbital/radial curves stay inactivevol.radial.mode = CurvecurveMax === undefined)→ 断言 _isZero() === true / _isRadialActive() === false / _needTransformFeedback() === false / _updateShaderData(...) to.not.throw()修复前反向证伪_getKeyMinMax 返回 undefined_isZero() = false_isRadialActive() = true → 断言 fail,且 _updateShaderData 会命中 undefined._getTypeArray() 抛 TypeErrorto.not.throw 直接 fail)。故这是真回归网——同时守住 _isZero 语义翻转 + 崩溃路径。orbital 三轴 Curve 分支同样覆盖。

剩余开放项(均非阻塞,历轮已记,源码本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 未触及 vert rel/视觉速度重建。
  • [P3] offset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • getVOLOrbital/getVOLRadial feedback shader 去重 —— 1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max 命名 —— 5a09858e

本 commit 是一次干净的 bug 修复 + 去重:??0 同时修掉一个 P0 级崩溃(未创建曲线对象的 Curve 模式曲线因 _isActive 误判为 true 而在 _updateShaderData 命中 undefined._getTypeArray() 抛 TypeError)+ 一个 NaN-bounds latent bug(Math.min(0, undefined)=NaN,含 base dev/2.0 就存在的),语义翻转正确(空曲线 → inactive);feedback shader 收口到共享 evaluateVOLVelocity(byte 等价、宏门一致、零残留);_tempVector33 消除一处 latent aliasing 隐患(防御性硬化)。逐路径核对全部正确,新测试反向证伪同时守住语义翻转 + 崩溃路径,CI 4/4 全绿。无 P0/P1/新问题。LGTM。

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One more small structural suggestion on the shader side (verified: the change below compiles through the shader precompiler and passes all 13 particle test files, 162/162, on this branch).

#define _VOL_LINEAR_MODULE_ENABLED
#endif

#if defined(_VOL_LINEAR_MODULE_ENABLED) || defined(RENDERER_VOL_ORBITAL_CONSTANT_MODE) || defined(RENDERER_VOL_ORBITAL_CURVE_MODE) || defined(RENDERER_VOL_RADIAL_CONSTANT_MODE) || defined(RENDERER_VOL_RADIAL_CURVE_MODE)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Dedup] The 4-condition orbital/radial guard defined(RENDERER_VOL_ORBITAL_CONSTANT_MODE) || ... || defined(RENDERER_VOL_RADIAL_CURVE_MODE) is repeated 4 times across 3 files (twice here, ParticleVert.glsl L197, ParticleFeedback.shader L210). Following the _VOL_LINEAR_MODULE_ENABLED convention this PR already established, a composite macro collapses them all and removes the risk of one site drifting when a new mode macro is added later:

#if defined(RENDERER_VOL_ORBITAL_CONSTANT_MODE) || defined(RENDERER_VOL_ORBITAL_CURVE_MODE) || defined(RENDERER_VOL_RADIAL_CONSTANT_MODE) || defined(RENDERER_VOL_RADIAL_CURVE_MODE)
    #define _VOL_ORBITAL_RADIAL_MODULE_ENABLED
#endif

#if defined(_VOL_LINEAR_MODULE_ENABLED) || defined(_VOL_ORBITAL_RADIAL_MODULE_ENABLED)
    #define _VOL_MODULE_ENABLED
#endif

The other three sites then become plain #ifdef _VOL_ORBITAL_RADIAL_MODULE_ENABLED. Both consumers include this file before use, so visibility is fine. I applied exactly this locally: shader precompile passes and all particle unit tests (162/162) stay green.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 8bcfc647c 线性推进一个 commit 到 829b679dgh api compare 8bcfc647c...829b679d 返回 status: ahead, ahead_by: 1, behind_by: 0829b679d 父 = 8bcfc647c11b7a3149835e77e4d11b67a4a4a740,确认非 force-push 回退)。唯一改动是 refactor: share orbital radial vol shader guard,3 个 shader 文件、净 +6/-2:VelocityOverLifetime.glsl(+6/-2)+ ParticleVert.glsl(+1/-1)+ ParticleFeedback.shader(+1/-1)。纯宏守卫收口,零行为变化——把重复 3 次的 4 项 #if defined(...ORBITAL...) || ...RADIAL... inline 谓词提取成单个 _VOL_ORBITAL_RADIAL_MODULE_ENABLED 宏(在 VelocityOverLifetime.glsl 定义一次),另两个文件改引用它。逐路径核对:宏展开逐字等价、定义先于所有使用点(include-order 契约保持)、消费范围收窄正确(只收 4 项组合谓词,2 项 orbital-only/radial-only 选择谓词未误伤)、零残留、e2e 反向证伪。无 P0/P1,无新问题。--comment LGTM。

✅ 本轮唯一改动 —— orbital/radial 组合守卫收口到 _VOL_ORBITAL_RADIAL_MODULE_ENABLED(逐路径核对宏展开逐字等价 + include-order 契约 + 收窄范围正确 + 零残留 + e2e 反向证伪)

829b679d 把在 3 个文件里逐字重复的组合谓词 defined(RENDERER_VOL_ORBITAL_CONSTANT_MODE) || defined(RENDERER_VOL_ORBITAL_CURVE_MODE) || defined(RENDERER_VOL_RADIAL_CONSTANT_MODE) || defined(RENDERER_VOL_RADIAL_CURVE_MODE) 提取成一个宏,在 VelocityOverLifetime.glsl:8-10 定义,另两处改 #ifdef _VOL_ORBITAL_RADIAL_MODULE_ENABLED。这是消除双源真相的合理收口——组合谓词原本 3 副本,改一处漏一处会 desync,现单一 source of truth,与既有 _VOL_LINEAR_MODULE_ENABLED(同一模块文件里 RENDERER_VOL_CONSTANT_MODE || RENDERER_VOL_CURVE_MODE 的收口宏)完全同构对称。

逐路径核对(不只信 commit message,核宏展开等价 + _VOL_MODULE_ENABLED gate 不变 + include-order 契约 + 收窄边界 + 零残留 + e2e 反向证伪)

  1. _VOL_MODULE_ENABLED 派生逻辑逐字等价 ——

    • BASE(8bcfc647c):_VOL_MODULE_ENABLED = _VOL_LINEAR_MODULE_ENABLED || ORBITAL_CONST || ORBITAL_CURVE || RADIAL_CONST || RADIAL_CURVE(单行 5 项 OR)。
    • NEW:先 _VOL_ORBITAL_RADIAL_MODULE_ENABLED = ORBITAL_CONST || ORBITAL_CURVE || RADIAL_CONST || RADIAL_CURVE,再 _VOL_MODULE_ENABLED = _VOL_LINEAR_MODULE_ENABLED || _VOL_ORBITAL_RADIAL_MODULE_ENABLED
    • 代入展开 = _VOL_LINEAR_MODULE_ENABLED || (ORBITAL_CONST || ORBITAL_CURVE || RADIAL_CONST || RADIAL_CURVE)——|| 结合律下与 BASE 逐字相同布尔、相同操作数。✅
  2. 3 个使用点各自逐字等价 —— renderer_VOLOffset 声明(VelocityOverLifetime.glsl:103)、ParticleVert.glsl:197visualSimulationVelocity 重建入口)、ParticleFeedback.shader:210linearVelocity 初始化入口)三处 BASE 都是同一 4 项谓词,NEW 全改成 #ifdef _VOL_ORBITAL_RADIAL_MODULE_ENABLED,展开即原谓词。✅

  3. include-order 契约保持(跨文件宏依赖的关键) —— 新宏在 VelocityOverLifetime.glsl:8 定义,被 ParticleVert.glsl:197 / ParticleFeedback.shader:210 引用;两文件均在引用点之前 #include ".../Module/VelocityOverLifetime.glsl"(vert :103 先于 :197、feedback :48 先于 :210)。且这两文件早已在同一模块 include 之后使用 _VOL_LINEAR_MODULE_ENABLED(vert :117/:182、feedback :112/:212)——即新宏沿用了已确立的同一 include-order 契约,不是新引入的脆弱依赖。✅

  4. 收窄范围正确——只收 4 项组合谓词,未误伤 2 项选择谓词 —— 全 shader tree 扫描:完整 4 项 inline 谓词现仅剩 VelocityOverLifetime.glsl:8 一处(即宏定义站),3 处旧副本全清。同时 ParticleVert.glsl:213/220ParticleFeedback.shader:239/246VelocityOverLifetime.glsl:137/1622 项 orbital-only(ORBITAL_CONST||ORBITAL_CURVE)/ radial-only(RADIAL_CONST||RADIAL_CURVE 选择谓词未动——这些是在 orbital vs radial 子块间做选择,与「orbital 或 radial 任一 active」的组合门语义不同,用 4 项宏替换它们会是错误的过度收口。作者正确区分了这两类谓词。✅

  5. 零残留 —— 全 packages/shader/src/** .glsl/.shader 逐文件扫描,旧 4 项组合谓词零残留(仅剩宏定义站),无悬挂副本。✅

  6. e2e 实跑反向证伪 —— tip 829b679d 上 e2e 4/4 全绿。这是决定性证据:velocityOrbitalConstant case 覆盖 orbital+radial+linear VOL 全模式,若宏收口误改了任一变体的编译门(漏掉某 mode、错配 renderer_VOLOffset uniform 声明门、或 include-order 契约破裂使宏在使用点未定义)→ 编译出的 shader 变体不同 → 基线图偏移 → e2e 红。实测 4/4 全绿 ⇒ 守卫收口产出的 shader 变体端到端逐字节等价。✅

CI

tip 829b679d 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。

注释合规

本 commit 零注释改动——VelocityOverLifetime.glsl:102// Orbital / Radial (transform-feedback only). Center of orbit in system-local space. 是被移动宏所在块的未改动既有注释,非本次新增/修改。新增的是宏定义(#if/#define/#endif),无注释。refactor: share orbital radial vol shader guard commit message 准确。无新增违规。

剩余开放项(均非阻塞,历轮已记,源码本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 只把 ParticleVert.glsl:197 的守卫从 inline 谓词改成宏,未触及 vert rel/视觉速度重建逻辑,P2 性质不变。
  • [P3] offset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ??0 空曲线崩溃 + NaN-bounds —— 8bcfc647c
  • _tempVector33 aliasing 硬化 —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • getVOLVelocity/getVOLOrbital/getVOLRadial feedback shader 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max 命名 —— 5a09858e

本 commit 是一次干净的宏守卫收口:把 3 个 shader 文件里逐字重复的 4 项 orbital/radial 组合谓词提取成单个 _VOL_ORBITAL_RADIAL_MODULE_ENABLED 宏(消除双源真相,与既有 _VOL_LINEAR_MODULE_ENABLED 同构对称)。逐路径核对 _VOL_MODULE_ENABLED 派生逐字等价(|| 结合律)、3 处使用点等价、include-order 契约沿用既有 _VOL_LINEAR_MODULE_ENABLED 同一模式、收窄范围正确(未误伤 2 项 orbital-only/radial-only 选择谓词)、零残留、e2e 4/4 反向证伪行为不变。无 P0/P1/新问题。LGTM。

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

Non-blocking performance / gating suggestion:

Orbital/radial VOL is a WebGL2 + transform feedback feature, but some of its side effects are currently gated by slightly different predicates. _setTransformFeedback() and bounds use _needTransformFeedback(), while _updateShaderData() still checks _isOrbitalActive() / _isRadialActive() directly and can enable orbital/radial macros + upload uniforms even when TF cannot run, for example on WebGL1. Emission random also uses enabled && _isRandomMode(), which can do orbital/radial random work for a feature that will not be active.

It may be worth centralizing this into one predicate, for example _isOrbitalRadialRuntimeEnabled() / _needTransformFeedback(), and using it consistently for:

  • TF selection
  • orbital/radial shader macros + uniforms
  • orbital/radial random channels during emission
  • bounds

This would keep WebGL1 / non-TF cases from generating unused shader variants or doing per-frame shaderData/random work for a feature that cannot affect the result.

Related small shader cleanup: in ParticleVert.glsl, the orbital/radial visual velocity calculation is only consumed by RENDERER_MODE_STRETCHED_BILLBOARD, but it is currently emitted in all TF render variants. The compiler may DCE it for normal billboard/mesh variants, but explicitly wrapping that block with #ifdef RENDERER_MODE_STRETCHED_BILLBOARD would reduce driver reliance and keep non-stretched variants smaller.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 829b679d 线性推进一个 commit 到 fdb377b39gh api compare 829b679d...fdb377b39 返回 status: ahead, ahead_by: 1, behind_by: 0fdb377b39 父 = 829b679d43a071c5ecc22ef08f393276c63cceed,确认非 force-push 回退)。唯一改动是 perf: gate orbital radial vol runtime work,2 文件:VelocityOverLifetimeModule.ts(+10/-5)+ ParticleVert.glsl(+33/-34)。两类改动:① TS 侧用 _needTransformFeedback() gate 掉 orbital/radial 的 shader-data 上传 + 随机槽写入(WebGL1 / orbital·radial 均 inactive 时跳过,纯 perf,正确)② shader 侧把 orbital/radial visual velocity 重建包进 RENDERER_MODE_STRETCHED_BILLBOARD(只有 stretched billboard 才消费 visualLocalVelocity,其余模式白算——方向正确)。但改动 ② 在把旧的 #else 分支删掉、改依赖顶部默认初始化时,引入了一个 stretched-billboard 视觉速度丢失 linear VOL 贡献的正确性回退(P1),且恰好静默撤销了 141aacf35 fix(particle): align stretched billboard to orbital velocity 的 linear-only 分支。故 --request-changes

问题

[P1] ParticleVert.glsl:177-241 — stretched-billboard + linear-VOL(无 orbital/radial)TF 路径下,visualLocalVelocity 丢失 linear VOL 速度贡献(静默撤销 141aacf35 的 fix)

改动 ② 把 orbital/radial visual velocity 重建包进 #ifdef RENDERER_MODE_STRETCHED_BILLBOARD 是对的(只有 stretched billboard 在 :277 消费 visualLocalVelocity/visualWorldVelocity),但同时删掉了旧的 #else 分支,改为完全依赖顶部默认初始化。旧代码(829b679d):

// :176-177  init(在 _VOL_LINEAR_MODULE_ENABLED 修改 localVelocity 之前)
vec3 visualLocalVelocity = localVelocity;   // = a_FeedbackVelocity
vec3 visualWorldVelocity = worldVelocity;   // = vec3(0.0)
...
#ifdef _VOL_LINEAR_MODULE_ENABLED
    localVelocity += instantVOLVelocity;    // localVelocity 现在含 linear VOL
    ...
#endif
#ifdef _VOL_ORBITAL_RADIAL_MODULE_ENABLED
    ... visualLocalVelocity = visualSimulationVelocity + orbitalRadialVelocity;  // visualSimulationVelocity += currentLinearVelocity
#else
    visualLocalVelocity = localVelocity;    // ← 在 linear VOL 加完之后重读,含 linear VOL
    visualWorldVelocity = worldVelocity;
#endif

新代码删掉 #else,所以 stretched-billboard + linear-VOL + orbital/radial 时,visualLocalVelocity 保留 :177 的初值 = a_FeedbackVelocity在 linear VOL += instantVOLVelocity 之前捕获),linear VOL 的速度贡献被丢掉。

这是正确性回退,非等价重构,理由三条(均已读源码核实,非推断)

  1. a_FeedbackVelocity 不含 linear VOL —— 不是双算,是真丢失。 feedback shader(ParticleFeedback.shader:105/268)里 localVelocity = attr.a_FeedbackVelocity,linear VOL 只算进 volLocal/volWorld 用于位置积分v_FeedbackVelocity = localVelocity 原样透传上一帧的 feedback 速度,从不把 linear VOL 写进 v_FeedbackVelocity。所以 vert 端必须把 instantVOLVelocity 加回来才是真实瞬时速度——旧 #elsevisualLocalVelocity = localVelocity(= a_FeedbackVelocity + instantVOLVelocity)正是干这个,新代码把它丢了。

  2. 保留的 orbital 分支自己也加 linear VOL —— 证明设计意图就是「stretch 速度含 linear VOL」。 orbital/radial 分支 :213 明确 visualSimulationVelocity += currentLinearVelocity。新代码在 orbital 路径honor 这个语义,却在 linear-only 路径丢了它,前后不自洽。

  3. 非 TF 路径(:239-241)也含 VOL,作为参照。 非 TF 分支 visualLocalVelocity = localVelocity 是在 computeParticlePosition:239,inout 累积了 start+VOL+FOL)之后捕获的,含 VOL。新 TF linear-only 路径与它不一致。

触发条件(完全可达、且是常见配置):WebGL2 + RENDERER_MODE_STRETCHED_BILLBOARD + linear VOL 启用 + 无 orbital/radial + TF 被 noise/limitVelocity/death-subEmitter 任一触发(ParticleGenerator._setTransformFeedback:684-690isWebGL2 && (limit || noise || VOL._needTransformFeedback() || deathSubEmitter)——linear-only VOL 自己不开 TF,但 noise/limit 会开)。即:WebGL2 上「拖尾/火花」类常见效果 = stretched billboard + linear VOL + noise,stretch 朝向/长度会静默丢掉 linear VOL 分量。

为什么 e2e 4/4 绿漏过particleRenderer-velocity-orbital-constant.ts默认 billboard(未设 renderMode:277 消费点被编译掉)且 orbital active(走 orbital 分支非 #else)——两头都覆盖不到这个 linear-only stretched-billboard 回退。这正是历轮 [P2]「stretched-billboard vert 路径无测试覆盖」放大的后果。

Git 历史透镜证据git log -S "visualLocalVelocity = localVelocity" 指向 141aacf35 fix(particle): align stretched billboard to orbital velocity——该 fix: commit 正是故意引入这个 #else 分支(linear-only 也要把 stretch 速度对齐到真实速度)。本 commit 删掉它 = 静默撤销该 fix 的 linear-only 分支,无 commit body 说明依据。

修复:把 :177-178visualLocalVelocity/visualWorldVelocity 初始化下移到 _VOL_LINEAR_MODULE_ENABLED 块(:193之后,让默认值捕获含 linear VOL 的 localVelocity/worldVelocity(orbital 块随后照常 overwrite),或直接恢复 #else 分支。同时建议补一条 stretched billboard + linear VOL + noise(无 orbital/radial) 的 e2e 把这条契约 pin 住——这个回退能静默漏过的根因就是它无覆盖。

✅ 改动 1(TS gating)—— 逐路径核对正确

_updateShaderDataorbitalActive/radialActive 现在 = needTransformFeedback && _isOrbitalActive()/_isRadialActive()。因整块在 if (this.enabled) 内,_needTransformFeedback() 退化为 isWebGL2 && (_isOrbitalActive() || _isRadialActive())。逐路径核:

  • WebGL2 + orbital/radial 任一 active:needTransformFeedback=true → 与旧行为逐字等价。✅
  • WebGL1:needTransformFeedback=false → 跳过 orbital/radial 曲线数组上传 + 不设 orbital/radial 宏,且 _enableMacro(…, null) 正确 disable 旧宏。WebGL1 无 TF,orbital/radial 本就跑不了,这是正确的 perf 收窄。✅

_isRandomMode() 早返回 isLinearRandomMode(当 !_needTransformFeedback()):唯一消费者 ParticleGenerator.ts:1028 门控随机槽 24/25/26(a_Random1.yzw)写入。核对边界——orbital 在 TwoConstants(0,0)_isZero()=true → inactive 但 mode===TwoConstants)时,旧代码会因 isOrbitalRandomMode=true 写 3 个随机值、新代码不写:但此配置下 orbital 宏不设、shader 不消费这些槽,结果不变_velocityRand 是 VOL 专属子种子(grep 确认无跨模块共享),不写只影响自己的未被消费的槽,无 RNG 跨模块污染。✅

✅ 改动 2 的 perf 意图 —— 正确

把 orbital/radial visual velocity 重建包进 RENDERER_MODE_STRETCHED_BILLBOARD 方向正确:grep 确认 visualLocalVelocity/visualWorldVelocity 唯一消费点在 :277#ifdef RENDERER_MODE_STRETCHED_BILLBOARD 内),其余 billboard/mesh 模式该重建被 DCE 白算。只是删 #else 的落地方式引入了上面的 P1。

CI

tip fdb377b39 全绿:build(3 OS)/e2e(4/4 全 success)/codecov(project + patch)/lint/labeler。但 e2e 覆盖不到 P1 config(默认 billboard + orbital active,见上)。

注释合规

本 commit 零注释改动。TS 侧新增 3 行逻辑无注释。commit message perf: gate orbital radial vol runtime work 准确描述了 perf 意图,但未提改动 ② 顺带删除了 141aacf35#else——属可追溯性缺口(P1 修复时应在 commit body 说明)。

剩余开放项(非阻塞,历轮已记,本轮未触及)

  • [P3] offset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

改动的 perf 意图(TS gating + shader 侧只在 stretched billboard 才算 visual velocity)方向都对,TS 侧逐路径核对等价/安全。但 shader 侧删 #else 改依赖顶部默认初始化时,因初始化在 linear VOL += 之前捕获,导致 stretched billboard + linear VOL + noise(无 orbital/radial)TF 路径的 stretch 速度丢失 linear VOL 分量(P1,静默撤销 141aacf35 fix 的 linear-only 分支,e2e 用默认 billboard + orbital active 漏过)。建议把 visualLocalVelocity/visualWorldVelocity 初始化下移到 linear VOL 块之后(或恢复 #else)+ 补 stretched-billboard linear-only e2e 后再合。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 fdb377b39 线性推进一个 commit 到 e911a1dc8gh api compare fdb377b39...e911a1dc8 返回 status: ahead, ahead_by: 1, behind_by: 0e911a1dc8 父 = fdb377b398eeabad012235a690d571a3f1f1fdda,确认非 force-push 回退)。唯一改动是 fix: preserve linear vol stretched velocity——ParticleVert.glsl 单文件 +3/-2,正是我上轮 [P1] 所指的那个文件、那个落点。逐路径核对:修复正确,且落点与我上轮建议完全一致。解除我历史 CHANGES_REQUESTED 门控,approve。

✅ [原 P1] stretched-billboard + linear-VOL(无 orbital/radial)丢失 linear VOL 速度贡献 —— 已修复(落点精准)

e911a1dc8visualLocalVelocity/visualWorldVelocity 的初始化从 _VOL_LINEAR_MODULE_ENABLED之前(旧 :176-177)下移到该块之后(新 :192-193),正是我上轮建议的两个方案之一:

// 前(fdb377b39,init 在 linear VOL 修改 localVelocity 之前捕获)
localVelocity = attr.a_FeedbackVelocity;
worldVelocity = vec3(0.0);
vec3 visualLocalVelocity = localVelocity;   // ← 捕获的是 a_FeedbackVelocity,不含 linear VOL
vec3 visualWorldVelocity = worldVelocity;
...
#ifdef _VOL_LINEAR_MODULE_ENABLED
    localVelocity += instantVOLVelocity;    // linear VOL 加进 localVelocity(但 visual 已在此之前捕获)
#endif

// 后(e911a1dc8,init 下移到 linear VOL 块之后)
#ifdef _VOL_LINEAR_MODULE_ENABLED
    localVelocity += instantVOLVelocity;    // localVelocity 现在含 linear VOL
    ...
#endif
vec3 visualLocalVelocity = localVelocity;   // ← 捕获含 linear VOL 的值
vec3 visualWorldVelocity = worldVelocity;

逐路径核对(不只信 commit message,核四条路径 + 作用域 + 编译门 + P1 载荷前提 + CI)

  1. [P1 修复路径] stretched + linear-VOL(VOLSpace==0) + 无 orbital/radial —— 新 init 在 localVelocity += instantVOLVelocity 之后捕获 → visualLocalVelocity = a_FeedbackVelocity + instantVOLVelocity(含 linear VOL),无 orbital 块不 overwrite → linear VOL 分量被正确保留。上轮丢失的正是这条,已修。✅

  2. [对称路径] stretched + linear-VOL(VOLSpace!=0, world) + 无 orbital/radial —— 新 init 捕获 worldVelocity = instantVOLVelocityvisualWorldVelocity 含 world VOL,无 orbital 不 overwrite → world VOL 保留。✅

  3. [orbital 路径不受影响] stretched + orbital/radial(任意 linear 状态) —— :227-231 无条件 overwrite visualLocalVelocity/visualWorldVelocityvisualSimulationVelocity + orbitalRadialVelocity,且 visualSimulationVelocity 独立于 init(来自 a_FeedbackVelocity + currentLinearVelocity,非 localVelocity),故新 init 值对 orbital 路径是死值,行为与 base 逐字节相同,无回归。✅

  4. [非 TF 路径 bit-identical] —— #else 分支的 visualLocalVelocity = localVelocity:241-242)未动,逐字节不变。✅

作用域 / 编译门核对(GLSL 正确性关键)

  • 新 init(:195-196)在 _VOL_LINEAR_MODULE_ENABLED#endif:193之后#ifdef RENDERER_MODE_STRETCHED_BILLBOARD:198之前——即在 TF 分支顶层作用域声明,与 #else 分支的声明(:241-242)同一 brace 作用域深度。消费点 :278 在 TF/non-TF #if/#else/#endif:243)闭合之后,两分支都声明了该变量,故所有宏组合下 visualLocalVelocity 恰好声明一次且在 scope。✅
  • _VOL_LINEAR_MODULE_ENABLED 未定义时,localVelocity:176 无条件赋 a_FeedbackVelocity)在 :195 已有定义,visualLocalVelocity = localVelocity 读到有效值。✅
  • 全文件 #if/#endif = 36/36 平衡,region 嵌套良构(移动两行未破坏预处理平衡)。✅

P1 载荷前提复核(feedback shader 不把 linear VOL 写进 v_FeedbackVelocity —— 我逐行核了 tip 的 ParticleFeedback.shaderlocalVelocity = a_FeedbackVelocity:105)→ linear VOL 只算进 volLocal/volWorld:113-119不加进 localVelocity)→ localVelocity += folDeltaLocal + gravityLocal:133,只加 FOL+gravity)→ LVL 块(:136-189)把 volAsLocal 临时加入求 damping 后又减出:145 totalLocal = localVelocity + volAsLocal:147 localVelocity = dampenedTotal - volAsLocal)→ v_FeedbackVelocity = localVelocity:268仍不含 linear VOL。故 vert 端必须自己把 instantVOLVelocity 加回——新 init 位置正是干这个。P1 的载荷前提成立,修复对症。✅

CI

tip e911a1dc8 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success,已等 1/4 queued 分片跑完转绿)/codecov(project + patch)/lint/labeler。注:e2e 用默认 billboard + orbital active,覆盖不到本次修的 linear-only stretched-billboard 路径(见下 P2),故 e2e 绿既不能证也不能否这条 fix——修复的正确性由上面逐路径 shader 分析(by construction)确立,非靠 e2e。

注释合规

本 commit 零注释改动(纯代码移动两行声明)。fix: preserve linear vol stretched velocity commit message 准确描述了修复,也把 P1 修复正确标为 fix:。无新增违规。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 仍未加测试(正是让上轮 P1 静默漏过的那道缺口)。本次修复经逐路径 shader 分析 by construction 正确,但这条 feedback↔vert / linear-VOL-stretch 契约仍未被任何测试 pin 住。不阻塞(历轮已反复记,硬去重不再展开),但建议合并前或紧接着补一条 stretched billboard + linear VOL + noise(无 orbital/radial) 的 e2e,把这条契约钉死——否则未来任一端改动仍可能静默 desync。
  • [P3] offset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ [本轮] stretched-billboard + linear-VOL 丢失 linear VOL 速度贡献 —— e911a1dc8 init 下移到 linear VOL 块之后,逐路径核对 + 作用域/编译门/载荷前提全核,已闭环。
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本 commit 精准修掉了我上轮的阻塞项 [P1]:把 visualLocalVelocity/visualWorldVelocity 初始化从 linear VOL 块之前下移到之后,使 stretched-billboard + linear-VOL(无 orbital/radial)路径的 stretch 速度正确含 linear VOL 分量。逐路径核对四条路径行为(P1 路径修复 / world-space 对称 / orbital 路径死值不回归 / 非 TF bit-identical)、作用域与编译门(两分支同 scope 声明恰一次、#if/#endif 36/36 平衡)、P1 载荷前提(feedback shader v_FeedbackVelocity 确不含 linear VOL)全部核实,落点与我上轮建议完全一致,CI 4/4 全绿。无 P0/P1/新问题。解除我历史 CHANGES_REQUESTED 门控,approve。唯一开放项是这条契约仍无测试守护(P2,不阻塞,历轮已记)。

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-up review @ e911a1d

All findings from the previous rounds are verified fixed. This pass went after behavior semantics and the public API surface — no blocking bugs found. Things I verified and am NOT flagging: the TF integration decomposition (orbitVelocity vs externalVelocity) is algebraically equivalent to the old path in both simulation spaces; every macro combination that makes a shader read a_Random1 has a matching CPU write via _isRandomMode(); the orbital bounds fold (world/noise/gravity/radial reaches collapsed into a rotation-invariant scalar sphere) neither double-counts nor drops gravity in either bounds caller; module construction order makes the new optional chaining in _setTransformFeedback necessary and sufficient; clone keeps the _offset change listener because CloneManager deep-copies into the existing instance via copyFrom.

Inline, most-important first: 2 semantics decisions to confirm (world-space orbit center, LimitVelocity not constraining orbital/radial), 1 API naming window that closes at merge (offsetorbitalOffset), 2 behavior changes that deserve a line in the PR description (linear TwoCurves random fix, noise bounds ×lifetime), 1 editor-facing UX edge (zero-crossing clears particles), 1 test-placement note.

One non-inline note: the conservative orbital bounds are large by design — the new gravity+orbital test pins ±125 where directional gravity alone would be far tighter, since the scalar-reach sphere turns directional gravity into all-directions reach. Correct and safe, just worth keeping culling efficiency in mind if orbital becomes a common authoring pattern.

Also, the PR description's validation command references tests/src/core/particle/VelocityOverLifetimeOrbital.test.ts, which no longer exists on this branch — please update it alongside the test-placement comment below.

if (renderer_SimulationSpace == 0) {
rel = position - renderer_VOLOffset;
} else {
rel = rotationByQuaternions(position - attr.a_SimulationWorldPosition, invWorldRotation) - renderer_VOLOffset;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Design — please confirm] In world simulation space the orbit center is attr.a_SimulationWorldPosition — the emitter's position at the particle's birth. With a moving emitter, each particle orbits its own birth-frame center (plus the birth-rotation-transformed offset), so the trail becomes a family of rings pinned to past emitter positions. This is internally consistent with how the engine already treats world-sim (noise's noiseBasePos uses the same anchor), but the PR aims for Unity-like behavior, and my understanding is Unity orbits the system's current center in world space (moving the emitter drags the orbits along) — worth verifying against a Unity side-by-side before this ships. Whichever semantics is intended, please state it in the offset jsdoc; "in the system's local space" currently doesn't tell a world-sim user which transform snapshot applies.

baseVelocity += computeNoiseVelocity(attr, noiseBasePos, normalizedAge);
#endif

#ifdef _VOL_ORBITAL_RADIAL_MODULE_ENABLED

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Behavior gap vs Unity — worth documenting] The LVL dampen/drag steps above run before this block and only see base + linear VOL velocity, so the orbital tangential velocity and radial velocity are never speed-limited. Unity's Limit Velocity over Lifetime clamps the total velocity, orbital included. The deviation is defensible (limiting a positional rotation is awkward in this integration scheme), but it should be a deliberate, documented choice — a short comment here and/or a @remarks line on the orbital properties would stop it reading as an oversight later.

/**
* The center of orbit for orbital/radial velocity, in the system's local space.
*/
get offset(): Vector3 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Naming — window closes at merge] offset only affects orbital/radial and is meaningless for the linear velocityX/Y/Z on this same module, so the bare name reads ambiguous on a module called VelocityOverLifetime. Unity names this orbitalOffsetX/Y/Z; orbitalOffset here would be self-describing. Renaming after release is a breaking change, so this is the only cheap moment to decide. (Minor, same block: the trailing explicit _onGeneratorParamsChanged() in the setter is redundant — copyFrom already fires the _onValueChanged listener wired in the constructor, and the explicit call also fires on self-assignment where nothing changed. Safe to drop.)

velocityOverLifetime.velocityY.mode === ParticleCurveMode.TwoConstants &&
velocityOverLifetime.velocityZ.mode === ParticleCurveMode.TwoConstants
) {
if (velocityOverLifetime.enabled && velocityOverLifetime._isRandomMode()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Behavior change — please call out in PR description] The old condition only wrote a_Random1.yzw when all three linear curves were TwoConstants, while RENDERER_VOL_IS_RANDOM_TWO was already set for the all-TwoCurves case too — so linear TwoCurves random mode previously sampled zeros from the never-written slots and every particle rendered at the min curve. _isRandomMode() fixing that is correct, but existing content using linear TwoCurves VOL will visibly change after this PR (it becomes actually randomized). Deserves an explicit line in the description/changelog so the rendering diff is traceable to this fix rather than to the orbital feature.

} else {
noiseMaxX = noiseMaxY = noiseMaxZ = this._getCurveMagnitudeFromZero(noise.strengthX);
}
out.set(noiseMaxX * maxLifetime, noiseMaxY * maxLifetime, noiseMaxZ * maxLifetime);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Behavior change — please call out in PR description] Noise bounds used to be |strength_max| with no lifetime factor; since TF noise contributes velocity, × maxLifetime is the correct displacement upper bound and the old formula under-estimated. Right fix — but existing scenes with noise get much larger culling bounds after this PR (the new "Noise" bounds test pins ±11.414 where the old formula gave ±3.414 for the same setup). Same ask as the random-write fix: one line in the description declaring the bundled fix.

this._onCompositeCurveChange(lastValue, value);
lastValue?._unRegisterOnValueChanged(this._onTransformFeedbackDirty);
value?._registerOnValueChanged(this._onTransformFeedbackDirty);
this._generator._setTransformFeedback();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[UX edge — editor team should know] TF activation is value-based (_isZero() over the whole curve range), so animating or slider-dragging an orbital constant through exactly 0 toggles TF off and back on, and each toggle runs _clearActiveParticles() — all live particles vanish twice mid-drag. It's a narrow trigger (the value must land exactly on zero-range) and probably acceptable, but since editor sliders make it reachable, a @remarks note on the orbital/radial properties ("switching between zero and non-zero restarts the simulation") would set expectations.

}).to.not.throw();
});

it("velocity over lifetime orbital/radial default values", function () {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Tests] These ~210 lines are VelocityOverLifetimeModule tests (an earlier revision of this branch had them in a dedicated VelocityOverLifetimeOrbital.test.ts); landing them inside the LimitVelocityOverLifetime suite makes them hard to discover, and the suite-level beforeEach now resets VOL orbital state for LVL tests that never touch it. Suggest moving them to tests/src/core/particle/VelocityOverLifetime.test.ts (no VOL test file exists yet — arguably overdue). Nit while moving: the 0x7/0x4 magic numbers in the dirty-flag test would read better with a comment pointing at ParticleUpdateFlags.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 e911a1dc8 线性推进一个 commit 到 2956d0b71gh api compare e911a1dc8...2956d0b71 返回 status: ahead, ahead_by: 1, behind_by: 02956d0b71 父 = e911a1dc8bc7b7b491f36630679c24f75a6748ef,确认非 force-push 回退)。唯一改动是 refactor(particle): clarify orbital radial API,7 文件:VelocityOverLifetimeModule.ts(+14/-8)+ ParticleGenerator.ts(+10/-10)+ ParticleFeedback.shader(+2/-1)+ e2e case(+1/-1)+ ParticleBoundingBox.test.ts(+1/-1)+ 测试搬迁LimitVelocityOverLifetime.test.ts -222 → 新 VelocityOverLifetime.test.ts +267)。四类改动:① API 重命名 offsetcenterOffset(含 setter/getter + 全部 in-PR 调用点)② setter 去掉冗余的显式 _onGeneratorParamsChanged() ③ 三处 JSDoc + shader 注释澄清 ④ 把误放在 LVL 测试文件里的 orbital/radial 测试搬到专属 VOL 测试文件。逐路径核对全部正确,历史 a5a081e34 回归网完整保留并重命名,CI 4/4 全绿。无 P0/P1/新问题。--comment LGTM。

✅ 改动 1 —— offsetcenterOffset 重命名(逐路径核对:pre-release API 无外部消费者 + 全 in-PR 调用点已迁移)

gh api .../VelocityOverLifetimeModule.ts?ref=dev/2.0 确认:offset/orbital*/radial/centerOffset getter 在 base dev/2.0 零存在——整个 orbital/radial VOL 特性是本 PR(feat/particle-vol-orbital-radial净新增、从未发版。故 offsetcenterOffsetpre-release API 清晰化,非 breaking change,无外部消费者可破坏。全部 in-PR 调用点(e2e case :93 + ParticleBoundingBox.test.ts:112 + 新测试)本 commit 同步改完,CI 4/4 e2e 全绿反向证伪重命名未漏调用点(漏则编译失败)。命名本身是净改进——centerOffset 显式表达「相对系统原点的中心偏移」,比裸 offset 少歧义。✅

✅ 改动 2 —— setter 去掉显式 _onGeneratorParamsChanged()(逐路径核对:冗余去重,非 a5a081e34 回退)

// 前(e911a1dc8)
set offset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
  this._generator._renderer._onGeneratorParamsChanged();   // ← 本次删掉
}
// 后(2956d0b71)
set centerOffset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
}

这是冗余去重,非正确性回退,逐路径核对(不只信 commit message,核 copyFrom 副作用 + 构造期 wiring + 幂等 dirty + git 历史透镜)

  1. copyFrom 自己就 fire dirty —— Vector3.copyFrompackages/math/src/Vector3.ts:556-562)末行 this._onValueChanged?.()。构造期 wiring this._offset._onValueChanged = () => this._generator._renderer._onGeneratorParamsChanged():266本 PR 保留)把 _offset 的值变化路由到 dirty。故 reassignment 路径centerOffset = new Vector3(...)value !== offsetcopyFrom_onValueChanged → dirty)仍然脏 bounds——经 callback,非经被删的显式调用。✅

  2. 被删的显式调用是双派发的第二源 —— 旧 setter 在 reassignment 时 dirty 两次(copyFrom→callback + 显式调用)。_onGeneratorParamsChangedParticleRenderer.ts:306-308)是纯幂等 |= 三 dirty bit,故双派发无害、去掉一个无行为变化。唯一差异:旧 setter 在 value === offset(同实例自赋)时仍显式 dirty,新 setter 不 dirty——但同实例自赋值本就未变,不 dirty 是正确的(旧的是在 no-op 上过度 fire)。✅

  3. git 历史透镜——不是回退 a5a081e34 —— 我核了 a5a081e34 fix(particle): dirty bounds when velocity offset changes:它只加了两样东西——(a) 构造期 wiring _offset._onValueChanged = ...(本 PR 保留 :266)+ (b) "offset component changes dirty bounds after clone" 测试(本 PR 保留并重命名 为 centerOffset)。a5a081e34 从未触碰 setter 的显式调用(那行 pre-date 该 fix)。故删它不撤销 a5a081e34——该 fix 的真实机制(constructor wiring + 回归测试)完整保留。无矛盾。✅

  4. in-place 路径(历史 P1 关注点)仍被测试守护 —— 新测试 centerOffset component changes dirty bounds after cloneVelocityOverLifetime.test.ts:246)做 centerOffset.x = 1set x_onValueChanged)+ centerOffset.set(2,0,0).set_onValueChanged)在 cloned module 上,断言 _isContainDirtyFlag(GeneratorVolume) === true。这条走的正是 _offset._onValueChanged 构造期 wiring 路径(非被删的 setter 显式调用),revert wiring 即红——a5a081e34 的回归网原封守住。✅

注:setter reassignment 的 dirty 行为无直接断言(clone preserves ... centerOffset 只断言 value 拷贝非 dirty),但如上 by construction 证明(copyFrom_onValueChanged 是 math 包 invariant + 构造 wiring 未动),且 in-place 路径(历史 P1 真正踩过的坑)有测试。不阻塞。

✅ 改动 3 —— JSDoc + shader 注释澄清(逐条打开代码验非 aspirational)

三处新增/改写的行为描述,逐条对照代码核实全部准确,无 aspirational:

  1. "Switching orbital/radial values between zero and non-zero restarts the simulation" —— 准确。orbital/radial 曲线值变化 → _onTransformFeedbackDirty:96,经 _onOrbitalRadialChange:498 注册)→ _setTransformFeedback()。值跨零翻转 → _isOrbitalActive/_isRadialActive(读 _isZero())翻转 → _needTransformFeedback() 结果翻转 → _setTransformFeedbackParticleGenerator.ts:683)里 needed !== _useTransformFeedback → 调 this._clearActiveParticles() 重启仿真。✅

  2. "LimitVelocityOverLifetime does not clamp orbital/radial motion"(JSDoc + shader 注释)—— 准确。shader(ParticleFeedback.shader)里 LVL 块(RENDERER_LVL_MODULE_ENABLED)clamp 的是 localVelocity(base+FOL/gravity)与 volLocal/volWorld(linear VOL);orbital/radial 块(_VOL_ORBITAL_RADIAL_MODULE_ENABLED,另起于 :211)用未经 LVL clampvolLocal/volWorld 重算 linearVelocity,orbital 旋转(rotationByEuler)+ radial 生长对 rel位置积分,完全在 LVL 之外。故 orbital/radial motion 确实不被 LVL clamp。✅

  3. centerOffset "in world simulation space, this local offset is transformed by the simulation transform captured when each particle is born" —— 准确。shader :237(world 空间分支)renderer_VOLOffsetworldRotation + attr.a_SimulationWorldPosition(每粒子出生帧仿真变换,烘进 attribute)变换。✅

JSDoc 格式合规:多行 /** */ 块、@remarks block tag、句尾句号。setter 不重复 JSDoc(getter 覆盖 accessor pair,符合项目惯例)。

✅ 改动 4 —— 测试搬迁(逐条核对:搬对位置 + 回归网无丢失)

8 个 velocity over lifetime orbital/radial ... 测试从 LimitVelocityOverLifetime.test.ts(它们本就误放在这里)搬到新专属 VelocityOverLifetime.test.ts。对照 base vs tip 的 it() 标题清单:LVL 测试文件精确剩下 21 个真 LVL 测试(速度/dampen/drag/space 等),8 个 orbital/radial 全部搬走且都在新文件出现(含关键的 clone preserves orbital/radial/centerOffset + centerOffset component changes dirty bounds after clone)。搬迁同步改了 offsetcenterOffset。是合理的测试归位(orbital/radial 属 VOL 概念,不属 LVL),非覆盖丢失。✅

CI

tip 2956d0b71 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡;本次重命名 offsetcenterOffset 使「静态中心点」语义更清晰,部分收敛该讨论,但类型本身未变。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ [本轮相关] offset in-place 分量改脏 bounds —— a5a081e34 的构造期 wiring + 回归测试本 PR 完整保留并重命名为 centerOffset,本次删 setter 显式调用不撤销该 fix。
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本 commit 是一次干净的 pre-release API 清晰化:offsetcenterOffset 重命名(净新增特性,无外部消费者,全 in-PR 调用点已迁移)+ setter 去冗余显式 dirty(copyFrom_onValueChanged 已覆盖,非 a5a081e34 回退——该 fix 的构造 wiring + 回归测试完整保留)+ 三处行为描述澄清(逐条对照 shader/TS 验准确非 aspirational)+ 误放的 orbital/radial 测试归位到专属 VOL 测试文件(回归网无丢失)。逐路径核对全部正确,git 历史透镜确认无矛盾,CI 4/4 全绿。无 P0/P1/新问题。LGTM。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 e911a1dc8 线性推进一个 commit 到 2956d0b71gh api compare e911a1dc8...2956d0b71 返回 status: ahead, ahead_by: 1, behind_by: 02956d0b71 父 = e911a1dc8bc7b7b491f36630679c24f75a6748ef,确认非 force-push 回退)。唯一改动是 refactor(particle): clarify orbital radial API,7 文件:VelocityOverLifetimeModule.ts(+14/-8)+ ParticleGenerator.ts(+10/-10)+ ParticleFeedback.shader(+2/-1)+ e2e case(+1/-1)+ ParticleBoundingBox.test.ts(+1/-1)+ 测试搬迁LimitVelocityOverLifetime.test.ts -222 → 新 VelocityOverLifetime.test.ts +267)。四类改动:① API 重命名 offsetcenterOffset(含 setter/getter + 全部 in-PR 调用点)② setter 去掉冗余的显式 _onGeneratorParamsChanged() ③ 三处 JSDoc + shader 注释澄清 ④ 把误放在 LVL 测试文件里的 orbital/radial 测试搬到专属 VOL 测试文件。逐路径核对全部正确,历史 a5a081e34 回归网完整保留并重命名,CI 4/4 全绿。无 P0/P1/新问题。--comment LGTM。

✅ 改动 1 —— offsetcenterOffset 重命名(逐路径核对:pre-release API 无外部消费者 + 全 in-PR 调用点已迁移)

gh api .../VelocityOverLifetimeModule.ts?ref=dev/2.0 确认:offset/orbital*/radial/centerOffset getter 在 base dev/2.0 零存在——整个 orbital/radial VOL 特性是本 PR(feat/particle-vol-orbital-radial净新增、从未发版。故 offsetcenterOffsetpre-release API 清晰化,非 breaking change,无外部消费者可破坏。全部 in-PR 调用点(e2e case :93 + ParticleBoundingBox.test.ts:112 + 新测试)本 commit 同步改完,CI 4/4 e2e 全绿反向证伪重命名未漏调用点(漏则编译失败)。命名本身是净改进——centerOffset 显式表达「相对系统原点的中心偏移」,比裸 offset 少歧义。✅

✅ 改动 2 —— setter 去掉显式 _onGeneratorParamsChanged()(逐路径核对:冗余去重,非 a5a081e34 回退)

// 前(e911a1dc8)
set offset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
  this._generator._renderer._onGeneratorParamsChanged();   // ← 本次删掉
}
// 后(2956d0b71)
set centerOffset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
}

这是冗余去重,非正确性回退,逐路径核对(不只信 commit message,核 copyFrom 副作用 + 构造期 wiring + 幂等 dirty + git 历史透镜)

  1. copyFrom 自己就 fire dirty —— Vector3.copyFrompackages/math/src/Vector3.ts:556-562)末行 this._onValueChanged?.()。构造期 wiring this._offset._onValueChanged = () => this._generator._renderer._onGeneratorParamsChanged():266本 PR 保留)把 _offset 的值变化路由到 dirty。故 reassignment 路径centerOffset = new Vector3(...)value !== offsetcopyFrom_onValueChanged → dirty)仍然脏 bounds——经 callback,非经被删的显式调用。✅

  2. 被删的显式调用是双派发的第二源 —— 旧 setter 在 reassignment 时 dirty 两次(copyFrom→callback + 显式调用)。_onGeneratorParamsChangedParticleRenderer.ts:306-308)是纯幂等 |= 三 dirty bit,故双派发无害、去掉一个无行为变化。唯一差异:旧 setter 在 value === offset(同实例自赋)时仍显式 dirty,新 setter 不 dirty——但同实例自赋值本就未变,不 dirty 是正确的(旧的是在 no-op 上过度 fire)。✅

  3. git 历史透镜——不是回退 a5a081e34 —— 我核了 a5a081e34 fix(particle): dirty bounds when velocity offset changes:它只加了两样东西——(a) 构造期 wiring _offset._onValueChanged = ...(本 PR 保留 :266)+ (b) "offset component changes dirty bounds after clone" 测试(本 PR 保留并重命名 为 centerOffset)。a5a081e34 从未触碰 setter 的显式调用(那行 pre-date 该 fix)。故删它不撤销 a5a081e34——该 fix 的真实机制(constructor wiring + 回归测试)完整保留。无矛盾。✅

  4. in-place 路径(历史 P1 关注点)仍被测试守护 —— 新测试 centerOffset component changes dirty bounds after cloneVelocityOverLifetime.test.ts:246)做 centerOffset.x = 1set x_onValueChanged)+ centerOffset.set(2,0,0).set_onValueChanged)在 cloned module 上,断言 _isContainDirtyFlag(GeneratorVolume) === true。这条走的正是 _offset._onValueChanged 构造期 wiring 路径(非被删的 setter 显式调用),revert wiring 即红——a5a081e34 的回归网原封守住。✅

注:setter reassignment 的 dirty 行为无直接断言(clone preserves ... centerOffset 只断言 value 拷贝非 dirty),但如上 by construction 证明(copyFrom_onValueChanged 是 math 包 invariant + 构造 wiring 未动),且 in-place 路径(历史 P1 真正踩过的坑)有测试。不阻塞。

✅ 改动 3 —— JSDoc + shader 注释澄清(逐条打开代码验非 aspirational)

三处新增/改写的行为描述,逐条对照代码核实全部准确,无 aspirational:

  1. "Switching orbital/radial values between zero and non-zero restarts the simulation" —— 准确。orbital/radial 曲线值变化 → _onTransformFeedbackDirty:96,经 _onOrbitalRadialChange:498 注册)→ _setTransformFeedback()。值跨零翻转 → _isOrbitalActive/_isRadialActive(读 _isZero())翻转 → _needTransformFeedback() 结果翻转 → _setTransformFeedbackParticleGenerator.ts:683)里 needed !== _useTransformFeedback → 调 this._clearActiveParticles() 重启仿真。✅

  2. "LimitVelocityOverLifetime does not clamp orbital/radial motion"(JSDoc + shader 注释)—— 准确。shader(ParticleFeedback.shader)里 LVL 块(RENDERER_LVL_MODULE_ENABLED)clamp 的是 localVelocity(base+FOL/gravity)与 volLocal/volWorld(linear VOL);orbital/radial 块(_VOL_ORBITAL_RADIAL_MODULE_ENABLED,另起于 :211)用未经 LVL clampvolLocal/volWorld 重算 linearVelocity,orbital 旋转(rotationByEuler)+ radial 生长对 rel位置积分,完全在 LVL 之外。故 orbital/radial motion 确实不被 LVL clamp。✅

  3. centerOffset "in world simulation space, this local offset is transformed by the simulation transform captured when each particle is born" —— 准确。shader :237(world 空间分支)renderer_VOLOffsetworldRotation + attr.a_SimulationWorldPosition(每粒子出生帧仿真变换,烘进 attribute)变换。✅

JSDoc 格式合规:多行 /** */ 块、@remarks block tag、句尾句号。setter 不重复 JSDoc(getter 覆盖 accessor pair,符合项目惯例)。

✅ 改动 4 —— 测试搬迁(逐条核对:搬对位置 + 回归网无丢失)

8 个 velocity over lifetime orbital/radial ... 测试从 LimitVelocityOverLifetime.test.ts(它们本就误放在这里)搬到新专属 VelocityOverLifetime.test.ts。对照 base vs tip 的 it() 标题清单:LVL 测试文件精确剩下 21 个真 LVL 测试(速度/dampen/drag/space 等),8 个 orbital/radial 全部搬走且都在新文件出现(含关键的 clone preserves orbital/radial/centerOffset + centerOffset component changes dirty bounds after clone)。搬迁同步改了 offsetcenterOffset。是合理的测试归位(orbital/radial 属 VOL 概念,不属 LVL),非覆盖丢失。✅

CI

tip 2956d0b71 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡;本次重命名 offsetcenterOffset 使「静态中心点」语义更清晰,部分收敛该讨论,但类型本身未变。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ [本轮相关] offset in-place 分量改脏 bounds —— a5a081e34 的构造期 wiring + 回归测试本 PR 完整保留并重命名为 centerOffset,本次删 setter 显式调用不撤销该 fix。
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本 commit 是一次干净的 pre-release API 清晰化:offsetcenterOffset 重命名(净新增特性,无外部消费者,全 in-PR 调用点已迁移)+ setter 去冗余显式 dirty(copyFrom_onValueChanged 已覆盖,非 a5a081e34 回退——该 fix 的构造 wiring + 回归测试完整保留)+ 三处行为描述澄清(逐条对照 shader/TS 验准确非 aspirational)+ 误放的 orbital/radial 测试归位到专属 VOL 测试文件(回归网无丢失)。逐路径核对全部正确,git 历史透镜确认无矛盾,CI 4/4 全绿。无 P0/P1/新问题。LGTM。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 e911a1dc8 线性推进一个 commit 到 2956d0b71gh api compare e911a1dc8...2956d0b71 返回 status: ahead, ahead_by: 1, behind_by: 02956d0b71 父 = e911a1dc8bc7b7b491f36630679c24f75a6748ef,确认非 force-push 回退)。唯一改动是 refactor(particle): clarify orbital radial API,7 文件:VelocityOverLifetimeModule.ts(+14/-8)+ ParticleGenerator.ts(+10/-10)+ ParticleFeedback.shader(+2/-1)+ e2e case(+1/-1)+ ParticleBoundingBox.test.ts(+1/-1)+ 测试搬迁LimitVelocityOverLifetime.test.ts -222 → 新 VelocityOverLifetime.test.ts +267)。四类改动:① API 重命名 offsetcenterOffset(含 setter/getter + 全部 in-PR 调用点)② setter 去掉冗余的显式 _onGeneratorParamsChanged() ③ 三处 JSDoc + shader 注释澄清 ④ 把误放在 LVL 测试文件里的 orbital/radial 测试搬到专属 VOL 测试文件。逐路径核对全部正确,历史 a5a081e34 回归网完整保留并重命名,CI 4/4 全绿。无 P0/P1/新问题。--comment LGTM。

✅ 改动 1 —— offsetcenterOffset 重命名(逐路径核对:pre-release API 无外部消费者 + 全 in-PR 调用点已迁移)

gh api .../VelocityOverLifetimeModule.ts?ref=dev/2.0 确认:offset/orbital*/radial/centerOffset getter 在 base dev/2.0 零存在——整个 orbital/radial VOL 特性是本 PR(feat/particle-vol-orbital-radial净新增、从未发版。故 offsetcenterOffsetpre-release API 清晰化,非 breaking change,无外部消费者可破坏。全部 in-PR 调用点(e2e case :93 + ParticleBoundingBox.test.ts:112 + 新测试)本 commit 同步改完,CI 4/4 e2e 全绿反向证伪重命名未漏调用点(漏则编译失败)。命名本身是净改进——centerOffset 显式表达「相对系统原点的中心偏移」,比裸 offset 少歧义。✅

✅ 改动 2 —— setter 去掉显式 _onGeneratorParamsChanged()(逐路径核对:冗余去重,非 a5a081e34 回退)

// 前(e911a1dc8)
set offset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
  this._generator._renderer._onGeneratorParamsChanged();   // ← 本次删掉
}
// 后(2956d0b71)
set centerOffset(value: Vector3) {
  const offset = this._offset;
  if (value !== offset) { offset.copyFrom(value); }
}

这是冗余去重,非正确性回退,逐路径核对(不只信 commit message,核 copyFrom 副作用 + 构造期 wiring + 幂等 dirty + git 历史透镜)

  1. copyFrom 自己就 fire dirty —— Vector3.copyFrompackages/math/src/Vector3.ts:556-562)末行 this._onValueChanged?.()。构造期 wiring this._offset._onValueChanged = () => this._generator._renderer._onGeneratorParamsChanged():266本 PR 保留)把 _offset 的值变化路由到 dirty。故 reassignment 路径centerOffset = new Vector3(...)value !== offsetcopyFrom_onValueChanged → dirty)仍然脏 bounds——经 callback,非经被删的显式调用。✅

  2. 被删的显式调用是双派发的第二源 —— 旧 setter 在 reassignment 时 dirty 两次(copyFrom→callback + 显式调用)。_onGeneratorParamsChangedParticleRenderer.ts:306-308)是纯幂等 |= 三 dirty bit,故双派发无害、去掉一个无行为变化。唯一差异:旧 setter 在 value === offset(同实例自赋)时仍显式 dirty,新 setter 不 dirty——但同实例自赋值本就未变,不 dirty 是正确的(旧的是在 no-op 上过度 fire)。✅

  3. git 历史透镜——不是回退 a5a081e34 —— 我核了 a5a081e34 fix(particle): dirty bounds when velocity offset changes:它只加了两样东西——(a) 构造期 wiring _offset._onValueChanged = ...(本 PR 保留 :266)+ (b) "offset component changes dirty bounds after clone" 测试(本 PR 保留并重命名 为 centerOffset)。a5a081e34 从未触碰 setter 的显式调用(那行 pre-date 该 fix)。故删它不撤销 a5a081e34——该 fix 的真实机制(constructor wiring + 回归测试)完整保留。无矛盾。✅

  4. in-place 路径(历史 P1 关注点)仍被测试守护 —— 新测试 centerOffset component changes dirty bounds after cloneVelocityOverLifetime.test.ts:246)做 centerOffset.x = 1set x_onValueChanged)+ centerOffset.set(2,0,0).set_onValueChanged)在 cloned module 上,断言 _isContainDirtyFlag(GeneratorVolume) === true。这条走的正是 _offset._onValueChanged 构造期 wiring 路径(非被删的 setter 显式调用),revert wiring 即红——a5a081e34 的回归网原封守住。✅

注:setter reassignment 的 dirty 行为无直接断言(clone preserves ... centerOffset 只断言 value 拷贝非 dirty),但如上 by construction 证明(copyFrom_onValueChanged 是 math 包 invariant + 构造 wiring 未动),且 in-place 路径(历史 P1 真正踩过的坑)有测试。不阻塞。

✅ 改动 3 —— JSDoc + shader 注释澄清(逐条打开代码验非 aspirational)

三处新增/改写的行为描述,逐条对照代码核实全部准确,无 aspirational:

  1. "Switching orbital/radial values between zero and non-zero restarts the simulation" —— 准确。orbital/radial 曲线值变化 → _onTransformFeedbackDirty:96,经 _onOrbitalRadialChange:498 注册)→ _setTransformFeedback()。值跨零翻转 → _isOrbitalActive/_isRadialActive(读 _isZero())翻转 → _needTransformFeedback() 结果翻转 → _setTransformFeedbackParticleGenerator.ts:683)里 needed !== _useTransformFeedback → 调 this._clearActiveParticles() 重启仿真。✅

  2. "LimitVelocityOverLifetime does not clamp orbital/radial motion"(JSDoc + shader 注释)—— 准确。shader(ParticleFeedback.shader)里 LVL 块(RENDERER_LVL_MODULE_ENABLED)clamp 的是 localVelocity(base+FOL/gravity)与 volLocal/volWorld(linear VOL);orbital/radial 块(_VOL_ORBITAL_RADIAL_MODULE_ENABLED,另起于 :211)用未经 LVL clampvolLocal/volWorld 重算 linearVelocity,orbital 旋转(rotationByEuler)+ radial 生长对 rel位置积分,完全在 LVL 之外。故 orbital/radial motion 确实不被 LVL clamp。✅

  3. centerOffset "in world simulation space, this local offset is transformed by the simulation transform captured when each particle is born" —— 准确。shader :237(world 空间分支)renderer_VOLOffsetworldRotation + attr.a_SimulationWorldPosition(每粒子出生帧仿真变换,烘进 attribute)变换。✅

JSDoc 格式合规:多行 /** */ 块、@remarks block tag、句尾句号。setter 不重复 JSDoc(getter 覆盖 accessor pair,符合项目惯例)。

✅ 改动 4 —— 测试搬迁(逐条核对:搬对位置 + 回归网无丢失)

8 个 velocity over lifetime orbital/radial ... 测试从 LimitVelocityOverLifetime.test.ts(它们本就误放在这里)搬到新专属 VelocityOverLifetime.test.ts。对照 base vs tip 的 it() 标题清单:LVL 测试文件精确剩下 21 个真 LVL 测试(速度/dampen/drag/space 等),8 个 orbital/radial 全部搬走且都在新文件出现(含关键的 clone preserves orbital/radial/centerOffset + centerOffset component changes dirty bounds after clone)。搬迁同步改了 offsetcenterOffset。是合理的测试归位(orbital/radial 属 VOL 概念,不属 LVL),非覆盖丢失。✅

CI

tip 2956d0b71 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡;本次重命名 offsetcenterOffset 使「静态中心点」语义更清晰,部分收敛该讨论,但类型本身未变。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ [本轮相关] offset in-place 分量改脏 bounds —— a5a081e34 的构造期 wiring + 回归测试本 PR 完整保留并重命名为 centerOffset,本次删 setter 显式调用不撤销该 fix。
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本 commit 是一次干净的 pre-release API 清晰化:offsetcenterOffset 重命名(净新增特性,无外部消费者,全 in-PR 调用点已迁移)+ setter 去冗余显式 dirty(copyFrom_onValueChanged 已覆盖,非 a5a081e34 回退——该 fix 的构造 wiring + 回归测试完整保留)+ 三处行为描述澄清(逐条对照 shader/TS 验准确非 aspirational)+ 误放的 orbital/radial 测试归位到专属 VOL 测试文件(回归网无丢失)。逐路径核对全部正确,git 历史透镜确认无矛盾,CI 4/4 全绿。无 P0/P1/新问题。LGTM。

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the three commits after 829b679. Module build/shader precompile and all 14 particle test files (162/162) pass. One uncovered runtime crash is reproducible on the current head, plus one correction to the new public API remarks.

);
shaderData.setFloatArray(
VelocityOverLifetimeModule._orbitalMaxCurveYProperty,
orbitalY.curveMax._getTypeArray()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1 — runtime crash, reproduced on current head] The unauthored-curve fix covers the all-zero case, but this upload path still assumes every companion axis owns a curve object once the orbital group is active. A natural partial-authoring state crashes:

vol.enabled = true;
vol.orbitalX = new ParticleCompositeCurve(
  new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 2))
);
vol.orbitalY.mode = ParticleCurveMode.Curve; // curveMax is undefined
vol.orbitalZ.mode = ParticleCurveMode.Curve; // curveMax is undefined
generator._updateShaderData(shaderData);     // TypeError: undefined._getTypeArray

_isOrbitalActive() is true because X is non-zero, and all three modes select this curve branch, so Y throws here. The same issue applies to missing curveMin/curveMax in TwoCurves; an explicitly empty companion ParticleCurve also uploads an invalid all-zero key array whose shader evaluator has no matching segment after age 0.

The submitted regression test only makes all three axes unauthored, so the group remains inactive and never reaches this block. Please upload a valid zero curve (for example keys at 0 and 1 with value 0) for missing/empty companion axes, including both min and max arrays.

/**
* Orbital velocity (radians/second) around the x axis of the system.
* @remarks Requires WebGL2 and transform feedback. Switching orbital/radial values between zero and non-zero
* restarts the simulation. LimitVelocityOverLifetime does not clamp orbital/radial motion.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Docs — correction to my earlier slider note] This wording is broader than the implementation. _setTransformFeedback() clears particles only when the generator aggregate _useTransformFeedback value toggles. If orbitalX is already active, changing radial from zero to non-zero leaves TF enabled and returns before _clearActiveParticles(); likewise, if Noise/LVL already keeps TF enabled, activating the first orbital value does not restart anything.

Suggested wording: “Changing orbital/radial values may clear active particles when the change toggles the generator between transform-feedback and non-transform-feedback modes.” The same correction applies to the repeated remarks on Y, Z, and radial.

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 2956d0b71 线性推进一个 commit 到 682154818gh api compare 2956d0b71...682154818 返回 status: ahead, ahead_by: 1, behind_by: 0682154818 父 = 2956d0b71,确认非 force-push 回退)。唯一改动是 docs(particle): clarify transform feedback restart——VelocityOverLifetimeModule.ts 单文件 +11/-7,纯 JSDoc @remarks 改写,4 个 getter(orbitalX/Y/Z + radial)里把「Switching orbital/radial values between zero and non-zero restarts the simulation」改写为「Changing orbital/radial values may clear active particles when the change toggles the generator between transform-feedback and non-transform-feedback modes」。零产物/逻辑改动。逐条对照代码:这是一次准确性修正——把旧措辞的过度断言收紧到真实触发条件。CI 全绿。无 P0/P1/新问题。--comment LGTM。

✅ 本轮唯一改动 —— orbital/radial @remarks 改写(逐路径核对:把过度断言收紧到真实条件,是净改进非漂移)

我上轮把旧措辞标为「准确」,本轮把它改得更准确——逐路径 trace 机制核实新措辞是真实条件,旧措辞是 over-claim:

触发链路(端到端 trace)

  • orbital/radial 曲线值变化 → _onValueChanged_onTransformFeedbackDirtyVelocityOverLifetimeModule.ts:96,经 _onOrbitalRadialChange:499-503 注册/转发)→ _setTransformFeedback()
  • _setTransformFeedbackParticleGenerator.ts:683-694):needed = isWebGL2 && (limit || noise || VOL._needTransformFeedback() || deathSubEmitter):691 if (needed === this._useTransformFeedback) return; 早返回——TF 模式未翻转时直接 return,不 clear;只有 needed !== _useTransformFeedback(TF 模式真翻转)才 :694 _clearActiveParticles()

新旧措辞对照

  1. 旧措辞("switching zero↔non-zero restarts")是 over-claim —— 它断言 orbital/radial 跨零翻转必然 restart。但若 noise 或 limitVelocity 同时启用_useTransformFeedbacktrue,orbital/radial 跨零翻转时 needed === _useTransformFeedback(都是 true)→ :691 早返回 → 不 clear。故旧措辞在「其他 TF 源已开启」时是错的。
  2. 新措辞("may clear ... when the change toggles TF/non-TF modes")准确 —— may + 明确条件「toggles TF/non-TF mode」,恰好对应 needed !== _useTransformFeedback 这个真实触发闸门(跨零翻转但被 noise/limit 保持 TF 开启时不 toggle → 不 clear)。逐字命中代码语义。✅

这是由更优措辞 supersede 旧结论(我上轮判「旧措辞准确」是近似判断,新措辞严格更准确),非自我矛盾、非静默回退——docs 改写方向正确。

格式合规:4 处 JSDoc 均为多行 /** */ 块 + @remarks block tag + 句尾句号(motion.),符合项目 TSDoc 惯例;radial getter 保留 The center is given by \centerOffset`.` 引用(上轮已核准确,本轮未动)。4 处措辞完全一致,无漂移。

CI

tip 682154818 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。docs-only 改动零产物,CI 绿符合预期。

注释合规

本 commit 是 4 处 JSDoc @remarks 改写:多行块 ✓、@remarks block tag ✓、句尾句号 ✓、非 aspirational(逐条 trace _setTransformFeedback:691 早返回门 + _needTransformFeedback 核实新措辞与代码语义一致)✓、非复述代码(解释「何时会 clear active particles」这一 why,非复述实现)✓。docs(particle): clarify transform feedback restart commit message 准确。无新增违规。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本 commit 是纯 docs,未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ [本轮相关] orbital/radial restart 语义文档 —— 682154818 把过度断言收紧到真实 TF-mode-toggle 条件,逐路径 trace _setTransformFeedback:691 早返回门核实,已闭环。
  • offsetcenterOffset 重命名 + setter 去冗余显式 dirty —— 2956d0b71
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本 commit 是一次纯 docs 准确性修正:把 4 个 orbital/radial getter 的 @remarks 从「跨零翻转必然 restart」的过度断言收紧为「may clear active particles 当改动 toggle TF/non-TF 模式时」的真实条件。逐路径 trace _onOrbitalRadialChange_setTransformFeedback:691 needed===_useTransformFeedback 早返回门核实:只有 TF 模式真翻转(跨零翻转且无 noise/limit 保持 TF 开启)才 _clearActiveParticles,新措辞与此逐字对应,旧措辞在「其他 TF 源已开启」时是 over-claim。这是由更优措辞 supersede 我上轮的近似判断,非自我矛盾。格式合规、CI 4/4 全绿、零产物改动。无 P0/P1/新问题。LGTM。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 682154818 线性推进两个 commit 到 6309efa61gh api compare 682154818...6309efa61 返回 status: ahead, ahead_by: 2, behind_by: 06309efa61 父 = 1c84911be1c84911be 父 = 682154818,逐级确认非 force-push 回退)。两个都是纯 docs commit,仅动 2 文件、净 +4/-14:

  • 1c84911be docs(particle): simplify orbital radial requirements —— VelocityOverLifetimeModule.ts,把 4 个 getter(orbitalX/Y/Z + radial)的 @remarks 从三句话(Requires WebGL2 and transform feedback / 跨 TF-mode toggle 可能 clear active particles / LimitVelocityOverLifetime 不 clamp orbital/radial)收缩为一句 Requires WebGL2.
  • 6309efa61 docs(particle): describe orbital center semantics —— VelocityOverLifetime.glsl,把 renderer_VOLOffset 上的注释从 // Orbital / Radial (transform-feedback only). Center of orbit in system-local space. 改写为 // Center of orbital/radial motion in system-local space.

零产物/逻辑改动,CI 全绿。无 P0/P1/新问题。仅一条 P3 观察(下)。--comment LGTM。

✅ 逐条核对 —— docs 简化,删除的是准确但冗长的内容(非 stale-doc,也非静默回退)

我先核实了被删的三段描述在当前代码里仍然为真(判断这是"删对的准确文档"还是"删掉的正确文档"):

  1. "跨 TF-mode toggle 可能 clear active particles" —— 仍准确。orbital/radial setter → _onOrbitalRadialChange_onTransformFeedbackDirty_setTransformFeedbackParticleGenerator.ts:683-694),:691 if (needed === this._useTransformFeedback) return; 早返回,只有 TF 模式真翻转才 :694 _clearActiveParticles()行为未变,删的是准确描述。
  2. "LimitVelocityOverLifetime does not clamp orbital/radial motion" —— 仍准确(上轮已核 shader 里 orbital/radial 块独立于 LVL clamp 块,本轮源码未动)。
  3. "and transform feedback" → 只留 "Requires WebGL2" —— 合理去实现细节:WebGL2 是用户可操作的前置条件,transform feedback 是内部机制。

所以这不是 stale-doc 清理,删的是准确的行为文档。但也不是静默回退——我上轮(682154818)夸的是这段措辞准确(把 over-claim "restarts" 收紧到真实条件),并未主张它必须保留;本轮 commit message 明说 simplify,是透明的编辑取舍,非撤销 fix:。删除准确但冗长的 JSDoc 是站得住的编辑决策,不构成矛盾。

格式合规:4 处 getter 保留多行 /** */ 块 + @remarks block tag + 主描述句尾句号;centerOffset 的 world-simulation-space 解释未动(上轮已核准确)。API 仍保留主描述 + 可操作前置条件(Requires WebGL2)。

shader 注释改写:新旧均准确,renderer_VOLOffset_VOL_ORBITAL_RADIAL_MODULE_ENABLED(TF-only)guard 内,guard 名本身已表达模块门控,删掉冗余的 (transform-feedback only) 后仍解释了字段是什么(orbital/radial 运动中心 / system-local 空间)。非 aspirational、非复述代码。

问题(非阻塞)

[P3] VelocityOverLifetimeModule.ts:145/161/177/193 — 删掉的"TF-mode toggle 会 clear active particles"是真实的用户可观测 gotcha,建议在别处保留一处

被删的这句描述的是一个非平凡、用户可观测的副作用:运行时把 orbital/radial 从 0 调到非零(若恰好翻转 TF 模式)会让所有活跃粒子瞬间清空重启。这类"我拖动这个 slider 粒子怎么全没了/闪了一下"的意外,正是文档存在的意义。同理 "LVL 不 clamp orbital/radial" 是一条跨模块交互意外。

不阻塞——本轮把它从 4 个 getter 各重复一遍收敛掉,本身是合理的去重(避免 4 处同文漂移)。但建议这个 gotcha 别彻底消失:更自然的落点是 orbital/radial 的 setter(副作用发生处)或模块级 JSDoc 各写一处,而非在 4 个 getter 里重复。属编辑取舍,作者按产品文档策略定即可。

CI

tip 6309efa61 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。docs-only 零产物,绿符合预期。

注释合规

两个 commit 均为注释/JSDoc 改动:4 处 getter @remarks 保持多行块 + block tag + 句尾句号 ✓;shader 单行 // 注释无句尾句号 ✓、非 aspirational(逐条核实删的内容在代码里仍为真)✓、非复述代码 ✓。两条 commit message(simplify orbital radial requirements / describe orbital center semantics)准确。无新增违规。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本轮纯 docs,未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。

已闭环(保持,不重复)

  • ✅ orbital/radial restart 语义文档 —— 682154818 收紧到 TF-mode-toggle 条件;本轮 1c84911be 将其连同 LVL-clamp 说明一并简化为 Requires WebGL2.(准确内容的编辑取舍,见 P3)。
  • offsetcenterOffset 重命名 + setter 去冗余显式 dirty —— 2956d0b71
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本轮两个纯 docs commit:① 把 4 个 orbital/radial getter 的 @remarks 从三句(WebGL2+TF / TF-mode-toggle 清空粒子 / LVL 不 clamp)简化为一句 Requires WebGL2.;② 把 shader renderer_VOLOffset 注释改写得更简洁。逐条核实被删内容在代码里仍为真_setTransformFeedback:691 早返回门 + LVL 独立块),故删的是准确但冗长的文档——这是站得住的编辑取舍、非静默回退(我上轮夸的是措辞"准确"非"必须保留",commit message 明说 simplify)。唯一 P3:删掉的"TF-mode toggle 清空活跃粒子"是真实用户 gotcha,建议在 setter 或模块级保留一处。格式合规、CI 4/4 全绿、零产物。无 P0/P1/新问题。LGTM。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

总结

本轮 PR tip 从我上次审查的 6309efa61 线性推进一个 commit 到 2d94f638gh api compare 6309efa61...2d94f638 返回 status: ahead, ahead_by: 1, behind_by: 02d94f638 父 = 6309efa61,确认非 force-push 回退)。唯一改动是 docs(particle): simplify center offset description——VelocityOverLifetimeModule.ts 单文件 +1/-3,纯 JSDoc 改写:把 centerOffset getter 的文档从「主描述(… in the system's local space.)+ 三行 @remarks(world simulation space 下 offset 经每粒子出生帧仿真变换,故移动发射器的粒子绕各自出生帧中心公转)」收缩为一句主描述「The center offset of orbital/radial motion from the particle system origin.」。零产物/逻辑改动,CI 全绿。无 P0/P1/新问题。--comment LGTM。

✅ 逐条核对 —— 删的是准确但冗长的内容(编辑取舍,非 stale-doc,也非静默回退)

我先核实被删的 @remarks(birth-frame center transform 语义)在当前 shader 里仍然为真(判断是「删对的准确文档」还是「删掉的正确文档」):

  • world-space 分支ParticleFeedback.shader:236renderer_SimulationSpace != 0):rel = rotationByQuaternions(position − attr.a_SimulationWorldPosition, invWorldRotation) − renderer_VOLOffset,随后 position = attr.a_SimulationWorldPosition + rotationByQuaternions(renderer_VOLOffset + rel, worldRotation)a_SimulationWorldPosition + invWorldRotation 正是每粒子出生帧的仿真变换(烘进 attribute),故 offset 确实在每粒子出生帧空间应用 → 移动发射器的粒子绕各自出生帧中心公转。被删的 @remarks 逐字命中此语义,删的是准确描述。
  • 主描述改写… in the system's local space.… from the particle system origin.):renderer_VOLOffset = _offsetVector3,3 个 local 分量,:414setVector3 直接上传),故「local space」限定词是准确的,新描述去掉了空间限定——是轻微精度损失但非错误(「offset from origin」隐含 local 语义),属合理去实现细节的编辑取舍。

这不是静默回退:我在 2956d0b71 轮夸的是这段 @remarks 措辞准确、非 aspirational并未主张它必须保留;本轮 commit message 明说 simplify,是透明的编辑取舍。删除准确但冗长的 JSDoc 是站得住的编辑决策——与我 1c84911be 轮接受的 simplify orbital radial requirements 同一模式(那轮把三句 @remarks 简化为 Requires WebGL2.)。不构成自我矛盾。

格式合规:仍是多行 /** */ 块 + 主描述句尾句号(origin.)✓,符合项目 TSDoc 惯例;setter 不重复 JSDoc(getter 覆盖 accessor pair,符合项目惯例)✓。

CI

tip 2d94f638 全绿:build(3 OS: macos/ubuntu/windows)/e2e(4/4 全 success)/codecov(project + patch 均 success)/lint/labeler。docs-only 零产物,绿符合预期。

注释合规

本 commit 是单处 getter JSDoc 收缩:多行块 ✓、句尾句号 ✓、非 aspirational(逐路径核实被删的 birth-frame 语义在 ParticleFeedback.shader:236 world-space 分支仍为真)✓、非复述代码(描述「centerOffset 是什么」而非复述实现)✓。docs(particle): simplify center offset description commit message 准确。无新增违规。

剩余开放项(均非阻塞,历轮已记,本轮未触及,不重复展开)

  • [P2] stretched-billboard vert 路径无测试覆盖 —— 本轮纯 docs,未触及 vert rel/视觉速度重建逻辑。
  • [P3] centerOffset 类型选型(静态 Vector3 vs 曲线)—— 产品需求权衡。
  • [P2] orbital 多轴 Euler 逐帧积分轴序未文档化 —— 单轴无影响。
  • [P3] ParticleGenerator._setTransformFeedback 删掉的 why 注释建议保留。
  • [P3] 删掉的行为 gotcha(TF-mode toggle 清空活跃粒子 / birth-frame 公转中心)建议在别处保留一处 —— 本轮的 birth-frame 语义与 1c84911be 轮的 TF-toggle gotcha 属同类「删了准确的用户可观测行为文档」,硬去重不重复展开;属编辑取舍,作者按产品文档策略定。

已闭环(保持,不重复)

  • ✅ centerOffset birth-frame 语义文档 —— 2d94f638 将其连同 local-space 限定一并简化为一句主描述(准确内容的编辑取舍,见剩余开放项 P3)。
  • ✅ orbital/radial restart 语义文档 —— 682154818 收紧到 TF-mode-toggle 条件;1c84911be 简化为 Requires WebGL2.
  • offsetcenterOffset 重命名 + setter 去冗余显式 dirty —— 2956d0b71
  • ✅ stretched-billboard + linear-VOL 丢速度 —— e911a1dc8
  • _VOL_ORBITAL_RADIAL_MODULE_ENABLED guard 收口 —— 829b679d
  • ??0 空曲线崩溃 + NaN-bounds / _tempVector33 aliasing —— 8bcfc647c
  • evaluateVOLVelocity linear VOL evaluator —— d5f09e951
  • ✅ feedback shader getVOL* 去重 —— 8bcfc647c/1395f995d
  • ✅ orbital reach 未并入 world VOL/FOL + gravity —— fcf37172b
  • ✅ feedback↔vert rel desync —— 73524570b
  • ✅ gravity/FOL/drag/noise 被 orbital 一起旋转 —— 65cc91bc
  • ParticleFeedback.glsl 孤儿死代码 —— 4e2d79f1
  • ✅ offset 原地分量改不脏 bounds —— a5a081e34
  • ✅ TwoConstants/TwoCurves 静默 max-only —— 28f8b29dc
  • ✅ orbital 三轴混搭假抽象 —— 3277c2e57
  • ✅ orbital/radial property 缺 Max —— 5a09858e

本轮一个纯 docs commit:把 centerOffset getter 从「主描述 + 三行 birth-frame @remarks」收缩为一句主描述。逐路径核实被删的 birth-frame 公转语义在 ParticleFeedback.shader:236 world-space 分支仍为真、local-space 限定词准确,故删的是准确但冗长的文档——这是站得住的编辑取舍、非静默回退(我上轮夸的是措辞「准确」非「必须保留」,commit message 明说 simplify,与 1c84911be 同一模式)。格式合规、CI 4/4 全绿、零产物。无 P0/P1/新问题。LGTM。

@GuoLei1990 GuoLei1990 merged commit 4f52983 into galacean:dev/2.0 Jul 10, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this to Done in Particle Jul 10, 2026
cptbtptpbcptdtptp pushed a commit to cptbtptpbcptdtptp/engine that referenced this pull request Jul 10, 2026
…fault

Upstream galacean#3049 guaranteed orbital curve/offset cloning with @deepClone;
the merge dropped those decorators as redundant under type-driven Deep.
Pin the equivalence with identity + value assertions so a regression in
the type default surfaces here instead of in particle rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effect Effect related functions particle

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants