Skip to content

Conversation

@luzhuang
Copy link
Contributor

@luzhuang luzhuang commented Jan 20, 2026

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

What kind of change does this PR introduce?

Feature: MeshColliderShape 支持
Bug Fix: 物理碰撞相关修复

What is the current behavior?

What is the new behavior?

新功能:MeshColliderShape

  • 支持 ConvexMeshColliderShape(凸网格碰撞体)
  • 支持 TriangleMeshColliderShape(三角网格碰撞体,仅用于 StaticCollider)
  • 提供 setMeshData(vertices, indices) API 设置网格数据
  • 支持运行时 cooking 参数配置

Bug 修复

  1. 运行时 addShape 缩放问题Collider._addNativeShape() 中添加 setWorldScale 调用,确保动态添加的 shape 正确应用 transform 缩放
  2. move() 碰撞失效问题:添加 isKinematic 检查,非 kinematic 刚体调用 move() 时打印警告并返回

Does this PR introduce a breaking change?

No

Commits

Commit Description
d160001 feat(physics): 添加 MeshColliderShape 支持
9d47d84 feat(physics): 完善 MeshCollider e2e 测试
c85704c feat(physics): 更新 PhysX CDN 链接,支持 MeshCollider 新 API
f663c0a refactor(physics): 优化 MeshColliderShape 代码结构
c0b567c fix(physics): 修复运行时添加 shape 的缩放问题并补充单测
d49bdb2 fix(physics): 修复 isKinematic 为 false 时调用 move() 导致碰撞失效

Files Changed

Core API

  • packages/core/src/physics/shape/MeshColliderShape.ts - 新增
  • packages/core/src/physics/Collider.ts - 修复 addShape 缩放问题
  • packages/core/src/physics/DynamicCollider.ts - 修复 move() 问题

PhysX Implementation

  • packages/physics-physx/src/shape/PhysXMeshColliderShape.ts - 新增
  • packages/physics-physx/libs/physx.release.* - 更新 PhysX 库

Tests

  • tests/src/core/physics/MeshColliderShape.test.ts - 单元测试
  • e2e/case/physx-mesh-collider*.ts - E2E 测试

Fixes #2879

Summary by CodeRabbit

  • New Features

    • Mesh-based physics colliders (convex & triangle) and runtime mesh cooking controls
    • PhysX cooking integration and mesh collider creation APIs
  • Tests

    • Extensive unit tests for mesh colliders, PhysX downgrade, and collider behaviors
    • New end-to-end physics scenes with screenshot-based validation
  • Bug Fixes

    • Safer native resource handling and improved world-scale propagation for colliders
  • Chores

    • Updated E2E config entries, CI coverage config, and repository ignore patterns

✏️ Tip: You can customize this high-level summary in your review settings.

- 新增 MeshColliderShape 类,支持凸包和三角网格碰撞体
- 新增 IMeshColliderShape 接口定义
- PhysXPhysics 中实现 createMeshColliderShape 方法
- 初始化 PxCooking 用于网格碰撞体的烘焙
- 更新 PhysX WASM 文件以支持网格碰撞体
- 修复 Collider 添加 shape 时设置世界缩放
- 添加 MeshColliderShape 单元测试和 E2E 测试
- 重写 physx-mesh-collider 测试,使用 glTF 模型展示网格碰撞
- 新增 physx-mesh-collider-data 测试,展示 setMeshData API
- 添加 CCD 支持防止高速物体穿透薄网格
- 更新 PhysX WASM 支持 setCookingMeshPreprocessParams
- e2e 测试使用本地 physx 文件而非 CDN
- 删除 engine-mcp 目录
- 删除 C++ 源码中的调试日志
- 重新编译并上传 physx wasm/js 到 CDN
- 更新 PhysXPhysics.ts 中的默认 CDN URL
- e2e case 改回使用默认 CDN
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Walkthrough

Adds mesh collider support across engine layers: new MeshColliderShape and PhysXMeshColliderShape, PhysX cooking integration and flags, runtime guards for DynamicCollider/kinematic usage, E2E and unit tests, and related interface and export updates.

Changes

Cohort / File(s) Summary
Repo config
\.gitignore, codecov.yml
Adds ignore patterns (.serena, docs/plans) and a Codecov config with coverage targets and ignore paths.
E2E tests & config
e2e/case/physx-mesh-collider.ts, e2e/case/physx-mesh-collider-data.ts, e2e/config.ts
Two new PhysX mesh collider E2E scenes and corresponding entries added to e2e config.
Design API
packages/design/src/physics/IPhysics.ts, packages/design/src/physics/shape/IMeshColliderShape.ts, packages/design/src/physics/shape/index.ts
New IMeshColliderShape interface and IPhysics.createMeshColliderShape signature; new export added.
Core physics shapes
packages/core/src/physics/shape/MeshColliderShape.ts, packages/core/src/physics/shape/index.ts
New MeshColliderShape class (convex/triangle support, mesh extraction, native sync) and exported from shape index.
Core physics base & collider
packages/core/src/physics/Collider.ts, packages/core/src/physics/shape/ColliderShape.ts
Propagate world scale to native shape on add; guard native shape calls with optional chaining for safety.
DynamicCollider runtime checks
packages/core/src/physics/DynamicCollider.ts, tests/src/core/physics/DynamicCollider.test.ts
Prevent non-kinematic when non-convex mesh attached; guard move() usage; override addShape to block invalid attachments; test updated to assert CCD flag.
Physics lite stub
packages/physics-lite/src/LitePhysics.ts
Adds createMeshColliderShape method that throws not-supported to match interface.
PhysX core & cooking
packages/physics-physx/src/PhysXPhysics.ts, packages/physics-physx/src/enum/MeshPreprocessingFlag.ts, packages/physics-physx/src/index.ts
Initialize cooking resources, add setCookingParams and createMeshColliderShape, introduce MeshPreprocessingFlag enum and export it.
PhysX mesh shape implementation
packages/physics-physx/src/shape/PhysXMeshColliderShape.ts, packages/physics-physx/src/shape/PhysXColliderShape.ts, packages/physics-physx/src/PhysXDynamicCollider.ts
New PhysXMeshColliderShape (mesh/convex creation, geometry flags, memory management), safer geometry deletion, and CCD-related flag handling adjustments per detection mode.
Tests
tests/src/core/physics/MeshColliderShape.test.ts, tests/src/core/physics/PhysXDowngrade.test.ts
Large test suite for MeshColliderShape behaviors and PhysX downgrade JS-mode checks added.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant MeshShape as MeshColliderShape
    participant Core as Engine Core / Native API
    participant PhysX as PhysXPhysics
    participant PxShape as PhysXMeshColliderShape
    participant Native as Native PhysX

    Client->>MeshShape: construct(isConvex)
    Client->>MeshShape: setMesh / setMeshData(vertices, indices)
    MeshShape->>MeshShape: _extractMeshData()
    MeshShape->>Core: request native mesh shape
    Core->>PhysX: createMeshColliderShape(...)
    PhysX->>PhysX: ensure cooking resources (_pxCooking)
    PhysX->>PxShape: instantiate PhysXMeshColliderShape(...)
    PxShape->>Native: createTriMesh() / createConvexMesh()
    Native-->>PxShape: PxMesh / PxGeometry
    PxShape-->>PhysX: created shape
    PhysX-->>Core: native shape reference
    Core-->>MeshShape: native shape bound
    Client->>MeshShape: setDoubleSided / setTightBounds
    MeshShape->>PxShape: update geometry flags
    PxShape->>Native: update geometry / upload buffers
    Client->>MeshShape: destroy()
    PxShape->>Native: release native resources
Loading
sequenceDiagram
    participant Client as Client Code
    participant DC as DynamicCollider
    participant Shape as MeshColliderShape

    Client->>DC: set isKinematic = false
    DC->>DC: inspect attached shapes
    alt non-convex triangle present
        DC->>Client: log error, reject change
    else
        DC->>DC: apply change, _syncNative()
    end

    Client->>DC: addShape(meshShape)
    alt triangle mesh + non-kinematic
        DC->>Client: log error, skip add
    else
        DC->>Shape: add to native collider
    end

    Client->>DC: move(position)
    alt isKinematic == false
        DC->>Client: log warning, no-op
    else
        DC->>Core: apply native move
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I stitched a mesh with nimble paws,
Cooked shapes and flags with tiny jaws,
Convex hops and triangles play,
Kinematic rules keep chaos away,
PhysX dreams in a carrot-baked maze. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 Title clearly describes the main change: adding MeshColliderShape support to the physics system.
Linked Issues check ✅ Passed PR addresses issue #2879 by adding isKinematic checks in DynamicCollider.move() to prevent collisions failures [#2879], and adds MeshColliderShape support as the primary feature.
Out of Scope Changes check ✅ Passed All changes align with objectives: MeshColliderShape implementation, DynamicCollider.move() fix, cooking params, tests, and codecov config. No unrelated modifications detected.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@augmentcode
Copy link

augmentcode bot commented Jan 20, 2026

🤖 Augment PR Summary

Summary: This PR introduces PhysX mesh collider support (triangle mesh + convex mesh) and adds E2E/test coverage for the new capability.

Changes:

  • Added MeshColliderShape to engine-core, plus design-layer interface (IMeshColliderShape) and exports
  • Added PhysX implementation PhysXMeshColliderShape with runtime cooking support for triangle/convex meshes
  • Initialized and exposed PhysX cooking parameters (setCookingParams) and added MeshPreprocessingFlag enum
  • Updated collider native-shape hookup to apply entity world scale when adding shapes
  • Added new E2E scenes for mesh collider usage (GLTF mesh + raw vertex/index data)
  • Added unit tests for mesh collider creation, collision/trigger behavior, scaling, and mesh data updates
  • Updated PhysX runtime URLs and included new build deps in package.json

Technical Notes: Triangle meshes are intended for StaticCollider only, while convex meshes can be used with DynamicCollider; PhysX cooking is used to generate the underlying mesh data at runtime.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Fix All in Augment

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


protected _addNativeShape(shape: ColliderShape): void {
shape._collider = this;
shape._nativeShape.setWorldScale(this.entity.transform.lossyWorldScale);
Copy link

Choose a reason for hiding this comment

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

shape._nativeShape is assumed to be non-null here, but MeshColliderShape sets _nativeShape lazily (null until mesh data is provided), so adding such a shape too early would throw at runtime.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

);
} else {
// Create new shape
this._nativeShape = Engine._nativePhysics.createMeshColliderShape(
Copy link

Choose a reason for hiding this comment

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

When the native shape is created here, cached properties set before creation (e.g. doubleSided/tightBounds, and potentially other _nativeShape-backed fields) aren’t applied unless they’re set again after creation, which can lead to surprising defaults.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

* Destroy PhysXPhysics.
*/
destroy(): void {
this._pxCooking.release();
Copy link

Choose a reason for hiding this comment

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

destroy() unconditionally calls this._pxCooking.release() / this._pxCookingParams.delete(), which will throw if destroy() is called before initialization completes or if initialization fails partway.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 78.63501% with 144 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.72%. Comparing base (01a56df) to head (f677c62).
⚠️ Report is 3 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
...ckages/core/src/physics/shape/MeshColliderShape.ts 65.89% 88 Missing ⚠️
packages/core/src/physics/DynamicCollider.ts 50.00% 21 Missing ⚠️
packages/physics-physx/src/PhysXPhysics.ts 72.72% 15 Missing ⚠️
packages/physics-lite/src/LitePhysics.ts 40.00% 9 Missing ⚠️
.../physics-physx/src/shape/PhysXMeshColliderShape.ts 96.91% 8 Missing ⚠️
packages/physics-physx/src/PhysXDynamicCollider.ts 40.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2880      +/-   ##
===========================================
+ Coverage    78.96%   82.72%   +3.75%     
===========================================
  Files          857      794      -63     
  Lines        93517    89853    -3664     
  Branches      9378     9402      +24     
===========================================
+ Hits         73846    74331     +485     
+ Misses       19522    15438    -4084     
+ Partials       149       84      -65     
Flag Coverage Δ
unittests 82.72% <78.63%> (+3.75%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 50-51: The .gitignore contains unrelated patterns ".serena" and
"docs/plans"; either remove them or clarify intent: if ".serena" is a local dev
artifact, remove it from the commit and add a short comment in the PR or keep it
but move to a separate commit labeling it as local; if "docs/plans" is not
present or planned, remove that entry or add the directory in a separate PR; if
you intend these to match only at the repo root, prefix them with "/" (i.e.,
"/.serena" and "/docs/plans") so they don't match subdirectories—update the file
accordingly and add a brief PR description explaining the change.

In `@packages/core/src/physics/shape/MeshColliderShape.ts`:
- Around line 242-261: The static analyzer flagged formatting in the
MeshColliderShape update/create block; reformat the if/else block around
this._nativeShape so it matches project style (consistent spacing, indentation
and comment placement) — specifically tidy the cast and method call
(<IMeshColliderShape>this._nativeShape).setMeshData(...), align the create call
to Engine._nativePhysics.createMeshColliderShape(...) and ensure
Engine._physicalObjectsMap[this._id] = this; follows the same indentation and
trailing comma conventions; you can fix by applying the repository formatter or
manually adjusting spacing around parentheses/comments in the MeshColliderShape
update/create block.

In `@packages/physics-physx/src/shape/PhysXMeshColliderShape.ts`:
- Around line 86-92: setWorldScale currently calls _updateMeshScale
unconditionally which can run before mesh data exists and cause errors; guard
the call by checking that the mesh is created (e.g., that _pxMesh is not
null/undefined or that _createMeshAndShape has already run) and only call
_updateMeshScale when the mesh exists. Locate the override setWorldScale(scale:
Vector3) and add a null/undefined check for this._pxMesh (or a boolean flag set
when _createMeshAndShape completes) before invoking this._updateMeshScale to
avoid creating geometry with a null mesh.
- Around line 194-245: The mesh creation can return null and cause leaked
allocations; update _createMeshGeometry (and similarly _createMeshAndShape) to
ensure verticesPtr and indicesPtr are always freed if mesh creation fails —
either wrap the allocation/creation block in try/finally that calls
physX._free(verticesPtr) and physX._free(indicesPtr) (when set), or immediately
check the result of cooking.createConvexMesh / cooking.createTriMesh (and
this._pxMesh) and free allocated buffers before throwing an Error like "Failed
to create mesh"; ensure you reference and free the same verticesPtr and
indicesPtr variables and only call physX.createConvexMeshGeometry /
physX.createTriMeshGeometry after verifying this._pxMesh is non-null.
🧹 Nitpick comments (8)
tests/src/core/physics/PhysXDowngrade.test.ts (1)

4-33: Consider adding cleanup in afterAll to release PhysX resources.

The test initializes PhysXPhysics but doesn't call physics.destroy() after tests complete. While this may not cause issues in isolated test runs, it's good practice to clean up resources.

Suggested cleanup
 describe("PhysX Downgrade Mode", () => {
   let physics: PhysXPhysics;

   beforeAll(async () => {
     physics = new PhysXPhysics(PhysXRuntimeMode.JavaScript);
     await physics.initialize();
   }, 30000);

+  afterAll(() => {
+    physics?.destroy();
+  });
+
   it("should load PhysX in JavaScript mode", () => {
e2e/case/physx-mesh-collider-data.ts (1)

26-65: Consider extracting shared mesh generation logic to reduce duplication.

The terrain mesh generation (vertices, indices, normals) is duplicated between createTerrainMesh (lines 26-65) for visuals and createWavyTerrain (lines 75-94) for physics. This could lead to subtle inconsistencies if one is modified without the other.

♻️ Suggested refactor

Extract shared vertex/index generation into a helper:

function generateTerrainData(gridSize: number, scale: number) {
  const vertices: number[] = [];
  const indices: number[] = [];
  
  for (let z = 0; z <= gridSize; z++) {
    for (let x = 0; x <= gridSize; x++) {
      const px = (x - gridSize / 2) * scale;
      const pz = (z - gridSize / 2) * scale;
      const py = Math.sin(x * 0.5) * Math.cos(z * 0.5) * 1.5;
      vertices.push(px, py, pz);
    }
  }
  
  for (let z = 0; z < gridSize; z++) {
    for (let x = 0; x < gridSize; x++) {
      const i = z * (gridSize + 1) + x;
      indices.push(i, i + gridSize + 1, i + 1);
      indices.push(i + 1, i + gridSize + 1, i + gridSize + 2);
    }
  }
  
  return { vertices, indices };
}

Also applies to: 68-115

tests/src/core/physics/MeshColliderShape.test.ts (2)

30-38: Consider adding engine cleanup in afterAll.

The engine is created in beforeAll but never destroyed. While this may not cause immediate issues in test environments, it's good practice to clean up resources.

♻️ Suggested addition
afterAll(() => {
  engine.destroy();
});

40-48: Prototype mutation for spies could affect test isolation.

Mutating CollisionScript.prototype methods in beforeEach works but could leak between test files if the test runner doesn't properly isolate modules. Consider using vi.spyOn on specific instances instead.

e2e/case/physx-mesh-collider.ts (2)

169-181: Fallback bounds handling is defensive but could mask issues.

The fallback to hardcoded defaults when bounds are invalid silently handles missing mesh data. Consider logging a warning when falling back so issues are easier to diagnose.

♻️ Suggested improvement
   // Use defaults if bounds are invalid (no mesh data found)
   const validBounds = isFinite(potWidth) && isFinite(potHeight) && potWidth > 0;
+  if (!validBounds) {
+    console.warn("Pot bounds invalid, using defaults. Check if mesh data is accessible.");
+  }
   const actualPotWidth = validBounds ? potWidth : 0.24;

172-174: Consider removing debug console.log statements for E2E tests.

The debug log statements at lines 172-174 and 224 are useful during development but may add noise to E2E test output. Consider removing or guarding them behind a debug flag.

Also applies to: 224-224

packages/core/src/physics/shape/MeshColliderShape.ts (2)

79-86: Consider accessing parent class properties through protected accessors.

Using (this as any)._contactOffset and similar patterns to access parent class internals is fragile. Consider exposing protected accessors in the base ColliderShape class or using a different approach.

Also applies to: 91-104, 109-119


166-173: Consider supporting BufferMesh in addition to ModelMesh.

The setMesh method only supports ModelMesh, logging a warning for other mesh types. If BufferMesh support is planned, consider documenting this as a future enhancement or throwing a more descriptive error.

Comment on lines +50 to +51
.serena
docs/plans
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Read the .gitignore file around lines 50-51 to verify the changes
cat -n .gitignore | sed -n '40,60p'

Repository: galacean/engine

Length of output: 310


🏁 Script executed:

# Search for any references to .serena and docs/plans in the repository
rg -i "serena" --type-list | head -20
rg -i "docs/plans"

Repository: galacean/engine

Length of output: 448


🏁 Script executed:

# Check if there are any directories or files matching these patterns
fd -H "\.serena" 
fd -H "docs/plans"

Repository: galacean/engine

Length of output: 41


🏁 Script executed:

# Look for any comments or context in .gitignore about these entries
cat -n .gitignore | grep -A 5 -B 5 "serena\|docs/plans"

Repository: galacean/engine

Length of output: 208


🏁 Script executed:

# Search for any references to "serena" in the codebase
rg "serena" -i

Repository: galacean/engine

Length of output: 41


🏁 Script executed:

# Check if docs/plans directory exists or is referenced
find . -name "plans" -type d 2>/dev/null
ls -la docs/ 2>/dev/null | head -20

Repository: galacean/engine

Length of output: 273


🏁 Script executed:

# Look for any documentation or comments about .serena
rg "\.serena" -i

Repository: galacean/engine

Length of output: 41


🏁 Script executed:

# Check the git history of .gitignore to understand when these were added
git log --oneline .gitignore | head -10
git diff HEAD~1 .gitignore 2>/dev/null || git show HEAD:.gitignore

Repository: galacean/engine

Length of output: 831


🏁 Script executed:

# Double-check if .serena or docs/plans were recently created/changed
git status --short 2>/dev/null | grep -E "serena|docs/plans" || echo "No matching files in git status"

Repository: galacean/engine

Length of output: 90


🏁 Script executed:

# Check if there's any PR metadata or documentation about why these were added
cat README.md 2>/dev/null | grep -i "serena\|docs/plans" || echo "Not found in README"

Repository: galacean/engine

Length of output: 78


Clarify or remove unrelated .gitignore entries.

These patterns appear unrelated to the MeshCollider feature in this PR. Additionally, .serena has no references in the codebase and docs/plans directory does not exist in the repository.

Please clarify:

  • What is .serena? Is this a local development artifact that should not be committed?
  • Is docs/plans intended for future use, or should this be removed or added in a separate commit?

If these patterns should only match at the repository root, prefix them with /:

Suggested fix
-.serena
-docs/plans
+/.serena
+/docs/plans

Without the leading /, these patterns match anywhere in the directory tree (e.g., packages/foo/docs/plans).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.serena
docs/plans
/.serena
/docs/plans
🤖 Prompt for AI Agents
In @.gitignore around lines 50 - 51, The .gitignore contains unrelated patterns
".serena" and "docs/plans"; either remove them or clarify intent: if ".serena"
is a local dev artifact, remove it from the commit and add a short comment in
the PR or keep it but move to a separate commit labeling it as local; if
"docs/plans" is not present or planned, remove that entry or add the directory
in a separate PR; if you intend these to match only at the repo root, prefix
them with "/" (i.e., "/.serena" and "/docs/plans") so they don't match
subdirectories—update the file accordingly and add a brief PR description
explaining the change.

Comment on lines 86 to 92
/**
* {@inheritDoc IColliderShape.setWorldScale }
*/
override setWorldScale(scale: Vector3): void {
super.setWorldScale(scale);
this._updateMeshScale();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null check before _updateMeshScale in setWorldScale.

If setWorldScale is called before mesh data is set (before _createMeshAndShape), _updateMeshScale will try to create geometry with a null _pxMesh, potentially causing errors.

🐛 Proposed fix
  override setWorldScale(scale: Vector3): void {
    super.setWorldScale(scale);
-   this._updateMeshScale();
+   if (this._pxMesh) {
+     this._updateMeshScale();
+   }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* {@inheritDoc IColliderShape.setWorldScale }
*/
override setWorldScale(scale: Vector3): void {
super.setWorldScale(scale);
this._updateMeshScale();
}
/**
* {`@inheritDoc` IColliderShape.setWorldScale }
*/
override setWorldScale(scale: Vector3): void {
super.setWorldScale(scale);
if (this._pxMesh) {
this._updateMeshScale();
}
}
🤖 Prompt for AI Agents
In `@packages/physics-physx/src/shape/PhysXMeshColliderShape.ts` around lines 86 -
92, setWorldScale currently calls _updateMeshScale unconditionally which can run
before mesh data exists and cause errors; guard the call by checking that the
mesh is created (e.g., that _pxMesh is not null/undefined or that
_createMeshAndShape has already run) and only call _updateMeshScale when the
mesh exists. Locate the override setWorldScale(scale: Vector3) and add a
null/undefined check for this._pxMesh (or a boolean flag set when
_createMeshAndShape completes) before invoking this._updateMeshScale to avoid
creating geometry with a null mesh.

- ColliderShape: setter 使用可选链处理 null
- MeshColliderShape: 删除冗余 override 和回调重绑定
- PhysXMeshColliderShape: 提取内存分配公共方法

减少 122 行重复代码
- Collider._addNativeShape 中 setWorldScale 添加可选链保护
- 补充运行时动态添加 shape 缩放的单元测试
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/src/core/physics/MeshColliderShape.test.ts`:
- Around line 25-48: Add an afterAll teardown that stops and disposes the engine
started in beforeAll to avoid leaking GL/PhysX resources: afterAll should
reference the same engine variable and call the appropriate shutdown/dispose API
(e.g., await engine.destroy() or engine.stop()/engine.destroy() if destroy is
async) after the tests complete, and null out engine/root/physicsScene as needed
to release references; place this afterAll alongside the existing beforeAll and
beforeEach blocks so engine.run() is properly shut down.

Comment on lines +25 to +48
describe("MeshColliderShape PhysX", () => {
let engine: WebGLEngine;
let root: Entity;
let physicsScene: any;

beforeAll(async () => {
engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), physics: new PhysXPhysics() });
engine.run();

const scene = engine.sceneManager.activeScene;
physicsScene = scene.physics;
physicsScene.gravity = new Vector3(0, -9.81, 0);
root = scene.createRootEntity("root");
});

beforeEach(() => {
// Reset collision script spies
CollisionScript.prototype.onCollisionEnter = vi.fn();
CollisionScript.prototype.onCollisionStay = vi.fn();
CollisionScript.prototype.onCollisionExit = vi.fn();
CollisionScript.prototype.onTriggerEnter = vi.fn();
CollisionScript.prototype.onTriggerStay = vi.fn();
CollisionScript.prototype.onTriggerExit = vi.fn();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add teardown for engine lifecycle.
engine.run() starts a render/physics loop and allocates WebGL/PhysX resources. Without an afterAll cleanup, this can leak handles across the test suite.

🧹 Proposed teardown addition
-import { describe, beforeAll, beforeEach, expect, it, vi } from "vitest";
+import { describe, beforeAll, beforeEach, afterAll, expect, it, vi } from "vitest";
@@
   beforeAll(async () => {
     engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), physics: new PhysXPhysics() });
     engine.run();
@@
     root = scene.createRootEntity("root");
   });
+
+  afterAll(() => {
+    engine?.destroy?.();
+  });
🤖 Prompt for AI Agents
In `@tests/src/core/physics/MeshColliderShape.test.ts` around lines 25 - 48, Add
an afterAll teardown that stops and disposes the engine started in beforeAll to
avoid leaking GL/PhysX resources: afterAll should reference the same engine
variable and call the appropriate shutdown/dispose API (e.g., await
engine.destroy() or engine.stop()/engine.destroy() if destroy is async) after
the tests complete, and null out engine/root/physicsScene as needed to release
references; place this afterAll alongside the existing beforeAll and beforeEach
blocks so engine.run() is properly shut down.

- move() 内部调用 PhysX 的 setKinematicTarget(),专为 kinematic 刚体设计
- 在非 kinematic 刚体上调用会导致 PhysX 隐式转换为 kinematic 模式
- 添加 isKinematic 检查,非 kinematic 时打印警告并返回

Closes galacean#2879
@luzhuang luzhuang changed the title Feat/mesh collider feat(physics): 添加 MeshColliderShape 支持及物理碰撞修复 Jan 20, 2026
- 修复 MeshColliderShape.ts prettier 格式问题
- 调大 MeshCollider e2e 测试的 diffPercentage 阈值 (0 -> 0.02)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/physics-physx/src/shape/PhysXMeshColliderShape.ts`:
- Around line 46-61: setMeshData currently releases the existing mesh via
_releaseMesh() before calling _createMesh(), which can leave _pxShape
referencing a null _pxGeometry if _createMesh() throws; change the flow so you
validate inputs or build the new geometry before dropping the old one: either
(A) perform pre-checks that _createMesh() would require (e.g., if !isConvex
ensure indices is non-null and index type valid) and only call _releaseMesh()
after validation passes, or (B) allocate/create the new mesh/geometry in a
temporary variable (invoke the logic inside _createMesh() but return the new
geometry or wrap it) and only call _releaseMesh() and swap in the new geometry
and call _pxShape.setGeometry(_pxGeometry) after creation succeeds; reference
setMeshData, _releaseMesh, _createMesh, _pxGeometry and _pxShape when making the
change.
♻️ Duplicate comments (3)
packages/physics-physx/src/shape/PhysXMeshColliderShape.ts (3)

86-89: Add null check before _updateGeometry in setWorldScale.

Unlike setDoubleSided and setTightBounds, this method calls _updateGeometry() unconditionally. If setWorldScale is called before mesh data is set or after destroy(), _updateGeometry will attempt to create geometry with a null _pxMesh.

🐛 Proposed fix
  override setWorldScale(scale: Vector3): void {
    super.setWorldScale(scale);
-   this._updateGeometry();
+   if (this._pxMesh) {
+     this._updateGeometry();
+   }
  }

136-171: Memory leak risk if mesh creation fails.

If cooking.createConvexMesh() or cooking.createTriMesh() fails and returns null, _pxMesh is null. The subsequent physX.createConvexMeshGeometry() / physX.createTriMeshGeometry() may throw or behave unexpectedly, and if an exception occurs before physX._free(verticesPtr) is reached, memory will leak.

Use try-finally or check for null immediately after mesh creation.

🐛 Proposed fix with try-finally
  private _createMesh(): void {
    const physX = this._physXPhysics._physX;
    const physics = this._physXPhysics._pxPhysics;
    const cooking = this._physXPhysics._pxCooking;

    const verticesPtr = this._allocateVertices();
+   let indicesPtr: number | null = null;

+   try {
      if (this._isConvex) {
        this._pxMesh = cooking.createConvexMesh(verticesPtr, this._vertexCount, physics);
+       if (!this._pxMesh) {
+         throw new Error("Failed to cook convex mesh");
+       }
        this._pxGeometry = physX.createConvexMeshGeometry(
          this._pxMesh,
          this._worldScale.x,
          this._worldScale.y,
          this._worldScale.z,
          this._tightBounds ? TIGHT_BOUNDS_FLAG : 0
        );
      } else {
        if (!this._indices) {
-         physX._free(verticesPtr);
          throw new Error("Triangle mesh requires indices");
        }

-       const { ptr: indicesPtr, isU16, triangleCount } = this._allocateIndices();
+       const { ptr, isU16, triangleCount } = this._allocateIndices();
+       indicesPtr = ptr;
        this._pxMesh = cooking.createTriMesh(verticesPtr, this._vertexCount, indicesPtr, triangleCount, isU16, physics);
+       if (!this._pxMesh) {
+         throw new Error("Failed to cook triangle mesh");
+       }
        this._pxGeometry = physX.createTriMeshGeometry(
          this._pxMesh,
          this._worldScale.x,
          this._worldScale.y,
          this._worldScale.z,
          this._doubleSided ? DOUBLE_SIDED_FLAG : 0
        );
-       physX._free(indicesPtr);
      }
-
-     physX._free(verticesPtr);
+   } finally {
+     physX._free(verticesPtr);
+     if (indicesPtr !== null) {
+       physX._free(indicesPtr);
+     }
+   }
  }

99-134: Missing null check after mesh creation.

After _createMesh(), _pxMesh could be null if cooking.createConvexMesh() or cooking.createTriMesh() fails. The subsequent physX.createConvexMeshShape() or physX.createTriMeshShape() would receive a null mesh, causing undefined behavior.

🐛 Proposed fix
  private _createMeshAndShape(material: PhysXPhysicsMaterial, uniqueID: number): void {
    const physX = this._physXPhysics._physX;
    const physics = this._physXPhysics._pxPhysics;
    const shapeFlags = ShapeFlag.SCENE_QUERY_SHAPE | ShapeFlag.SIMULATION_SHAPE;

    this._createMesh();
+   if (!this._pxMesh) {
+     throw new Error("Failed to create mesh collider: mesh cooking failed");
+   }

    // Create shape with material
    if (this._isConvex) {
🧹 Nitpick comments (1)
packages/physics-physx/src/shape/PhysXMeshColliderShape.ts (1)

24-41: Consider validating vertex data consistency.

The constructor accepts vertices and vertexCount separately without validating that vertices.length >= vertexCount * 3. If mismatched, _allocateVertices will copy incorrect data (or potentially read out of bounds when setting the view).

💡 Suggested validation
  constructor(
    physXPhysics: PhysXPhysics,
    uniqueID: number,
    vertices: Float32Array,
    vertexCount: number,
    indices: Uint16Array | Uint32Array | null,
    isConvex: boolean,
    material: PhysXPhysicsMaterial
  ) {
    super(physXPhysics);
+   if (vertices.length < vertexCount * 3) {
+     throw new Error("Vertices array length must be at least vertexCount * 3");
+   }
    this._isConvex = isConvex;
    this._vertices = vertices;

@luzhuang luzhuang changed the title feat(physics): 添加 MeshColliderShape 支持及物理碰撞修复 feat(physics): 添加 MeshColliderShape 支持 Jan 21, 2026
@luzhuang luzhuang changed the base branch from main to dev/2.0 January 21, 2026 03:00
- 直接读取 vertex buffer 数据,跳过 getPositions() 的 Vector3[] 中间层
- 紧凑布局 (stride=12, offset=0) 使用 Float32Array.set() 直接复制
- 交错布局使用单次遍历逐顶点复制
- 复用 _vertices 数组减少内存分配
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/physics/shape/MeshColliderShape.ts`:
- Around line 159-177: Add a defensive alignment check before creating the
Float32Array view in MeshColliderShape: validate that bufferData.byteOffset % 4
=== 0 and byteStride % 4 === 0 (and byteOffset % 4 if separate) to avoid
RangeError when constructing sourceData; if the check fails, emit a warning
(e.g., via console.warn or processLogger.warn) and bail out or return early from
the method to avoid further unsafe divisions, and only then create sourceData
and compute floatStride/floatOffset = byteStride/4 and byteOffset/4 safely;
reference the sourceData variable and the byteStride/byteOffset calculations
around the current Float32Array construction and the loop that computes
floatStride/floatOffset.
♻️ Duplicate comments (1)
packages/core/src/physics/shape/MeshColliderShape.ts (1)

49-68: Cached doubleSided/tightBounds not applied when native shape is created.

If these properties are set before mesh data is provided, the native shape is created with defaults rather than the cached values. The _updateNativeMesh method should apply these after creating the native shape.

🔧 Proposed fix

In _updateNativeMesh(), after creating the native shape, apply the cached properties:

       this._nativeShape = Engine._nativePhysics.createMeshColliderShape(
         this._id,
         this._vertices,
         vertexCount,
         this._indices,
         this._isConvex,
         this._material._nativeMaterial
       );
       Engine._physicalObjectsMap[this._id] = this;
+      // Apply cached properties
+      (<IMeshColliderShape>this._nativeShape).setDoubleSided(this._doubleSided);
+      (<IMeshColliderShape>this._nativeShape).setTightBounds(this._tightBounds);
     }

Comment on lines 159 to 177
// Create Float32Array view to read source data
const sourceData = new Float32Array(bufferData.buffer, bufferData.byteOffset, bufferData.byteLength / 4);

// Choose optimal copy method based on stride
if (byteStride === 12 && byteOffset === 0) {
// Tightly packed: direct copy
this._vertices.set(sourceData.subarray(0, vertexCount * 3));
} else {
// Interleaved: copy per vertex
const floatStride = byteStride / 4;
const floatOffset = byteOffset / 4;
for (let i = 0; i < vertexCount; i++) {
const srcIdx = i * floatStride + floatOffset;
const dstIdx = i * 3;
this._vertices[dstIdx] = sourceData[srcIdx];
this._vertices[dstIdx + 1] = sourceData[srcIdx + 1];
this._vertices[dstIdx + 2] = sourceData[srcIdx + 2];
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Float32Array TypedArray byteOffset alignment requirement JavaScript RangeError

💡 Result:

Short answer: When you create a typed-array view on an ArrayBuffer, the byteOffset must be a multiple of the view's BYTES_PER_ELEMENT. For Float32Array BYTES_PER_ELEMENT === 4, so byteOffset must be divisible by 4 — otherwise a RangeError is thrown. Also the viewed range must fit: byteOffset + length * BYTES_PER_ELEMENT <= buffer.byteLength. [1][2]

Example:
const buf = new ArrayBuffer(8);
new Float32Array(buf, 1); // RangeError (offset not a multiple of 4)
new Float32Array(buf, 4); // OK (offset is multiple of 4)

Sources: MDN TypedArray (alignment and RangeError) and MDN Float32Array (BYTES_PER_ELEMENT = 4). [1] [2]

Sources
[1] MDN: TypedArray — byteOffset/byteLength alignment and RangeError.
[2] MDN: Float32Array — BYTES_PER_ELEMENT = 4.


🏁 Script executed:

cd packages/core/src/physics/shape && cat -n MeshColliderShape.ts | sed -n '126,192p'

Repository: galacean/engine

Length of output: 2835


🏁 Script executed:

rg "byteOffset.*align|align.*byteOffset|BYTES_PER_ELEMENT|RangeError.*TypedArray" packages/core/src/physics/ --type ts -A 3 -B 3

Repository: galacean/engine

Length of output: 41


🏁 Script executed:

fd -t f "buffer|Buffer" packages/core/src/graphic* --type f -name "*.ts" | head -5 | xargs rg "stride|offset" -l

Repository: galacean/engine

Length of output: 6131


🏁 Script executed:

cat -n packages/core/src/graphic/Buffer.ts | head -100

Repository: galacean/engine

Length of output: 3258


🏁 Script executed:

cat -n packages/core/src/graphic/Buffer.ts | sed -n '100,200p'

Repository: galacean/engine

Length of output: 4096


Add defensive alignment checks for Float32Array view creation.

Line 160 creates a Float32Array view from bufferData.buffer using bufferData.byteOffset. The Float32Array constructor requires the byte offset to be aligned to 4-byte boundaries (BYTES_PER_ELEMENT for Float32Array); otherwise it throws a RangeError. Additionally, lines 168-169 assume byteStride and byteOffset are multiples of 4 when dividing by 4.

While vertex buffers are typically 4-byte aligned in practice, add explicit validation or document this requirement to prevent edge cases:

if (bufferData.byteOffset % 4 !== 0 || byteStride % 4 !== 0) {
  console.warn("MeshColliderShape: Buffer offsets must be 4-byte aligned");
  return;
}
🤖 Prompt for AI Agents
In `@packages/core/src/physics/shape/MeshColliderShape.ts` around lines 159 - 177,
Add a defensive alignment check before creating the Float32Array view in
MeshColliderShape: validate that bufferData.byteOffset % 4 === 0 and byteStride
% 4 === 0 (and byteOffset % 4 if separate) to avoid RangeError when constructing
sourceData; if the check fails, emit a warning (e.g., via console.warn or
processLogger.warn) and bail out or return early from the method to avoid
further unsafe divisions, and only then create sourceData and compute
floatStride/floatOffset = byteStride/4 and byteOffset/4 safely; reference the
sourceData variable and the byteStride/byteOffset calculations around the
current Float32Array construction and the loop that computes
floatStride/floatOffset.

1. MeshColliderShape: 创建 native shape 后同步 doubleSided 和 tightBounds 属性
2. PhysXDynamicCollider: 修复 ContinuousDynamic 模式下 eENABLE_CCD 应为 true
3. PhysXDynamicCollider: 各碰撞检测模式切换时正确清除其他模式的标志位
4. 添加对应的单元测试验证修复
MeshColliderShape:
- Fix: indices missing didn't return false
- Fix: setMesh after addShape didn't add native shape to collider
- Add super._syncNative() to sync base class properties
- Extract _extractIndices method for code reuse
- Add comment for PhysX Uint16/Uint32 indices
- Add isConvex doc noting setMesh must be called after changing

PhysXPhysics:
- Remove unused buildTriangleAdjacencies setting
- Add try/finally to ensure WASM memory (_malloc) is freed on failure
- Add null check for _pxMesh after cooking (returns null on failure)
- Make setMeshData atomic: create new mesh before releasing old one
- Release JS memory (_vertices/_indices) after copying to WASM
When switching from kinematic to dynamic, resync the collision detection
mode to ensure CCD flags are properly restored to the user's setting.
- Block adding triangle mesh to non-kinematic DynamicCollider
- Block switching isKinematic to false when triangle mesh is attached
- Skip mass/inertia calculation for kinematic bodies (not used per PhysX doc)
- Remove unused buildTriangleAdjacencies from setCookingParams
…to dynamic

- Add mass/inertia recalculation when isKinematic changes from true to false
- Improve _pxCookingParams documentation
}
}

@ignoreClone
Copy link
Collaborator

Choose a reason for hiding this comment

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

Delete.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/src/core/physics/MeshColliderShape.test.ts`:
- Around line 459-467: The vertices Float32Array in MeshColliderShape.test.ts
violates the formatter because coordinates must be split per vertex; update the
vertices declaration used with meshShape.setMeshData so each vertex's three
components are on their own line (e.g., one line per v0/v1/v2/v3) and keep the
inline comments (// v0, etc.) as needed; ensure the array remains a Float32Array
and that indices and meshShape.setMeshData calls are unchanged.
♻️ Duplicate comments (4)
packages/physics-physx/src/PhysXPhysics.ts (1)

147-156: Add null guards in destroy() for cooking resources.

The destroy() method unconditionally calls release() and delete() on cooking resources, which will throw if destroy() is called before initialization completes or if initialization fails partway.

🐛 Proposed fix
  destroy(): void {
-   this._pxCooking.release();
-   this._pxCookingParams.delete();
+   this._pxCooking?.release();
+   this._pxCookingParams?.delete();
    this._physX.PxCloseExtensions();
    this._pxPhysics.release();
    this._pxFoundation.release();
packages/core/src/physics/shape/MeshColliderShape.ts (1)

164-181: Consider adding alignment validation for buffer offsets.

The Float32Array constructor requires byteOffset to be 4-byte aligned. While vertex buffers are typically aligned in practice, adding defensive validation would prevent cryptic RangeError exceptions in edge cases.

♻️ Suggested validation
    // Create Float32Array view to read source data
+   if (bufferData.byteOffset % 4 !== 0) {
+     console.warn("MeshColliderShape: Buffer byteOffset is not 4-byte aligned");
+     return false;
+   }
    const sourceData = new Float32Array(bufferData.buffer, bufferData.byteOffset, bufferData.byteLength / 4);

    // Choose optimal copy method based on stride
    if (byteStride === 12 && byteOffset === 0) {
      // Tightly packed: direct copy
      this._vertices.set(sourceData.subarray(0, vertexCount * 3));
    } else {
+     if (byteStride % 4 !== 0 || byteOffset % 4 !== 0) {
+       console.warn("MeshColliderShape: Stride/offset must be 4-byte aligned for interleaved data");
+       return false;
+     }
      // Interleaved: copy per vertex with optimized indexing
packages/physics-physx/src/shape/PhysXMeshColliderShape.ts (1)

96-102: Add null check before _updateGeometry in setWorldScale.

If setWorldScale is called before mesh data is set (which can happen during Collider._addNativeShape), _updateGeometry will attempt to create geometry with a null _pxMesh, causing errors.

🐛 Proposed fix
  override setWorldScale(scale: Vector3): void {
    super.setWorldScale(scale);
-   this._updateGeometry();
+   if (this._pxMesh) {
+     this._updateGeometry();
+   }
  }
tests/src/core/physics/MeshColliderShape.test.ts (1)

15-39: Add engine teardown to avoid leaking WebGL/PhysX across tests.
engine.run() starts a render/physics loop; without an afterAll cleanup, resources can persist across the suite.

🧹 Proposed teardown addition
-import { describe, beforeAll, beforeEach, expect, it, vi } from "vitest";
+import { describe, beforeAll, beforeEach, afterAll, expect, it, vi } from "vitest";
@@
   beforeAll(async () => {
     engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), physics: new PhysXPhysics() });
     engine.run();
@@
     root = scene.createRootEntity("root");
   });
+
+  afterAll(() => {
+    engine?.destroy?.();
+  });
#!/bin/bash
# Locate WebGLEngine and its shutdown API to confirm the right method name.
files=$(rg -l "class WebGLEngine" -g '*.ts')
for f in $files; do
  echo "== $f =="
  rg -n "destroy\\(|stop\\(" "$f"
done
🧹 Nitpick comments (3)
e2e/case/physx-mesh-collider-sphere-container.ts (1)

85-88: Guard move() to kinematic colliders.

Prevents warning spam/no-ops if any non‑kinematic colliders are included.

♻️ Proposed change
 for (const collider of this._colliders) {
-  collider.move(this._tempPosition, this._tempQuaternion);
+  if (!collider.isKinematic) continue;
+  collider.move(this._tempPosition, this._tempQuaternion);
 }
packages/core/src/physics/DynamicCollider.ts (1)

434-462: LGTM - Ensures kinematic actors participate in collision detection.

The implementation correctly addresses the PhysX requirement that kinematic actors must have setKinematicTarget called every frame to participate in collision detection. Without the auto-move in _onUpdate, stationary kinematic bodies would stop detecting collisions.

Consider translating the Chinese comments to English for consistency with the rest of the codebase:

  • Line 436-437: "Kinematic actor needs setKinematicTarget called every frame to participate in collision detection. If user didn't call move(), auto-call to maintain current position."
  • Line 448: "Reset flag"
tests/src/core/physics/DynamicCollider.test.ts (1)

428-435: LGTM - Comprehensive CCD flag assertions.

The test now correctly verifies that ContinuousDynamic mode enables both eENABLE_CCD and eENABLE_CCD_FRICTION flags, matching the implementation changes in PhysXDynamicCollider.setCollisionDetectionMode.

Consider translating the Chinese comment (line 429) to English for consistency: "ContinuousDynamic mode requires both CCD and CCD_FRICTION enabled"

- Change error message from "Triangle mesh" to "triangle mesh" for consistency
- Update test to match the error message
- Remove _kinematicTargetSet flag
- Remove auto move() call in _onUpdate
- Fix PhysicsScene collision tests
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/physics/DynamicCollider.ts`:
- Around line 271-295: When changing a MeshColliderShape from convex to
non-convex (in the isConvex setter) or when switching mesh via
setMesh/setMeshData, validate the parent collider: if this._collider is non-null
and is a DynamicCollider and that DynamicCollider is non-kinematic (isKinematic
=== false), disallow the transition (log/throw and return) because triangle
meshes are not supported on non-kinematic DynamicCollider; update the
MeshColliderShape.isConvex setter and both setMesh/setMeshData to perform this
check before mutating isConvex/mesh so you cannot toggle to a triangle-mesh
while attached to a dynamic (non-kinematic) collider — mirror the same
restriction enforced by DynamicCollider.addShape and the isKinematic setter.
♻️ Duplicate comments (2)
tests/src/core/physics/MeshColliderShape.test.ts (2)

31-39: Add teardown for engine lifecycle to prevent resource leaks.

engine.run() is started but never stopped/destroyed after the suite finishes. This can leak WebGL/PhysX resources across tests.

🧹 Proposed teardown
-import { describe, beforeAll, beforeEach, expect, it, vi } from "vitest";
+import { describe, beforeAll, beforeEach, afterAll, expect, it, vi } from "vitest";
@@
   beforeAll(async () => {
     engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), physics: new PhysXPhysics() });
     engine.run();
@@
     root = scene.createRootEntity("root");
   });
+
+  afterAll(() => {
+    engine?.destroy?.();
+  });

458-464: Fix formatter violation in the vertices array.

Prettier expects vertex components to be split across lines here.

🎨 Formatting fix
-      const vertices = new Float32Array([
-        -5, 0, -5, // v0
-        5, 0, -5, // v1
-        -5, 0, 5, // v2
-        5, 0, 5 // v3
-      ]);
+      const vertices = new Float32Array([
+        -5,
+        0,
+        -5, // v0
+        5,
+        0,
+        -5, // v1
+        -5,
+        0,
+        5, // v2
+        5,
+        0,
+        5 // v3
+      ]);

Comment on lines 271 to +295
set isKinematic(value: boolean) {
if (this._isKinematic !== value) {
// Block switching to non-kinematic if triangle mesh is attached
if (!value) {
const shapes = this._shapes;
for (let i = 0, n = shapes.length; i < n; i++) {
const shape = shapes[i];
if (shape instanceof MeshColliderShape && !shape.isConvex) {
console.error("DynamicCollider: Cannot set isKinematic=false when triangle mesh is attached.");
return;
}
}
}

this._isKinematic = value;
(<IDynamicCollider>this._nativeCollider).setIsKinematic(value);

// Resync when switching back to dynamic
if (!value) {
(<IDynamicCollider>this._nativeCollider).setCollisionDetectionMode(this._collisionDetectionMode);
// Recalculate mass/inertia that was skipped in kinematic mode
if (this._automaticCenterOfMass || this._automaticInertiaTensor) {
this._setMassAndUpdateInertia();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

# Search for MeshColliderShape.isConvex setter and related validation
rg -n -C3 "set isConvex|isConvex" packages/core/src/physics/shape/MeshColliderShape.ts

Repository: galacean/engine


Repository: galacean/engine
Exit code: 0

stdout:

9-/**
10- * Physical collider shape for mesh.
11- * `@remarks`
12: * - Triangle mesh (isConvex=false) works with StaticCollider or kinematic DynamicCollider
13: * - Convex mesh (isConvex=true) works with both StaticCollider and DynamicCollider
14- * `@see` https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/Geometry.html#triangle-meshes
15- */
16-export class MeshColliderShape extends ColliderShape {
17:  private _isConvex: boolean = false;
18-  private _vertices: Float32Array = null;
19-  private _indices: Uint16Array | Uint32Array | null = null;
20-  private _doubleSided: boolean = false;
--
28-   * - Triangle mesh: Works with StaticCollider or kinematic DynamicCollider, requires indices
29-   * - After changing this property, you must call {`@link` setMesh} or {`@link` setMeshData} again to apply the change
30-   */
31:  get isConvex(): boolean {
32:    return this._isConvex;
33-  }
34-
35:  set isConvex(value: boolean) {
36:    if (this._isConvex !== value) {
37:      this._isConvex = value;
38-      if (this._vertices && this._nativeShape) {
39-        this._updateNativeMesh();
40-      }
--
73-
74-  /**
75-   * Create a MeshColliderShape.
76:   * `@param` isConvex - Whether to use convex mesh mode (default: false)
77-   */
78:  constructor(isConvex: boolean = false) {
79-    super();
80:    this._isConvex = isConvex;
81-    // Native shape is created lazily when mesh data is set
82-    this._nativeShape = null;
83-  }
--
181-    }
182-
183-    // Extract indices for triangle mesh
184:    if (!this._isConvex) {
185-      this._extractIndices(mesh);
186-      if (!this._indices) {
187-        console.warn("MeshColliderShape: Triangle mesh requires indices");
--
222-    const vertexCount = this._vertices.length / 3;
223-
224-    // Validate triangle mesh has indices
225:    if (!this._isConvex && !this._indices) {
226-      console.warn("MeshColliderShape: Triangle mesh requires indices, skipping update");
227-      return;
228-    }
229-
230-    if (this._nativeShape) {
231-      // Update existing shape
232:      (<IMeshColliderShape>this._nativeShape).setMeshData(this._vertices, vertexCount, this._indices, this._isConvex);
233-    } else {
234-      // Create new shape
235-      this._nativeShape = Engine._nativePhysics.createMeshColliderShape(
--
237-        this._vertices,
238-        vertexCount,
239-        this._indices,
240:        this._isConvex,
241-        this._material._nativeMaterial
242-      );
243-      Engine._physicalObjectsMap[this._id] = this;

Script executed:

# Check for triangle mesh and MeshColliderShape references in the physics module
rg -n "MeshColliderShape|triangle mesh" packages/core/src/physics --type ts

Repository: galacean/engine


Repository: galacean/engine
Exit code: 0

stdout:

packages/core/src/physics/DynamicCollider.ts:9:import { MeshColliderShape } from "./shape/MeshColliderShape";
packages/core/src/physics/DynamicCollider.ts:273:      // Block switching to non-kinematic if triangle mesh is attached
packages/core/src/physics/DynamicCollider.ts:278:          if (shape instanceof MeshColliderShape && !shape.isConvex) {
packages/core/src/physics/DynamicCollider.ts:279:            console.error("DynamicCollider: Cannot set isKinematic=false when triangle mesh is attached.");
packages/core/src/physics/DynamicCollider.ts:422:    if (shape instanceof MeshColliderShape && !shape.isConvex && !this._isKinematic) {
packages/core/src/physics/DynamicCollider.ts:423:      console.error("DynamicCollider: triangle mesh is not supported on non-kinematic DynamicCollider.");
packages/core/src/physics/shape/index.ts:6:export { MeshColliderShape } from "./MeshColliderShape";
packages/core/src/physics/shape/MeshColliderShape.ts:1:import { IMeshColliderShape } from "@galacean/engine-design";
packages/core/src/physics/shape/MeshColliderShape.ts:16:export class MeshColliderShape extends ColliderShape {
packages/core/src/physics/shape/MeshColliderShape.ts:45:   * Whether the triangle mesh should be double-sided for collision detection.
packages/core/src/physics/shape/MeshColliderShape.ts:46:   * `@remarks` Only applies to triangle mesh (non-convex).
packages/core/src/physics/shape/MeshColliderShape.ts:55:      (<IMeshColliderShape>this._nativeShape)?.setDoubleSided(value);
packages/core/src/physics/shape/MeshColliderShape.ts:70:      (<IMeshColliderShape>this._nativeShape)?.setTightBounds(value);
packages/core/src/physics/shape/MeshColliderShape.ts:75:   * Create a MeshColliderShape.
packages/core/src/physics/shape/MeshColliderShape.ts:88:   * `@param` indices - Triangle indices (required for triangle mesh, optional for convex)
packages/core/src/physics/shape/MeshColliderShape.ts:107:      console.warn("MeshColliderShape: Only ModelMesh is supported");
packages/core/src/physics/shape/MeshColliderShape.ts:137:      console.warn("MeshColliderShape: Mesh has no position attribute");
packages/core/src/physics/shape/MeshColliderShape.ts:145:      console.warn("MeshColliderShape: Position buffer not found");
packages/core/src/physics/shape/MeshColliderShape.ts:150:      console.warn("MeshColliderShape: Buffer is not readable");
packages/core/src/physics/shape/MeshColliderShape.ts:183:    // Extract indices for triangle mesh
packages/core/src/physics/shape/MeshColliderShape.ts:187:        console.warn("MeshColliderShape: Triangle mesh requires indices");
packages/core/src/physics/shape/MeshColliderShape.ts:224:    // Validate triangle mesh has indices
packages/core/src/physics/shape/MeshColliderShape.ts:226:      console.warn("MeshColliderShape: Triangle mesh requires indices, skipping update");
packages/core/src/physics/shape/MeshColliderShape.ts:232:      (<IMeshColliderShape>this._nativeShape).setMeshData(this._vertices, vertexCount, this._indices, this._isConvex);
packages/core/src/physics/shape/MeshColliderShape.ts:235:      this._nativeShape = Engine._nativePhysics.createMeshColliderShape(
packages/core/src/physics/shape/MeshColliderShape.ts:246:      (<IMeshColliderShape>this._nativeShape).setDoubleSided(this._doubleSided);
packages/core/src/physics/shape/MeshColliderShape.ts:247:      (<IMeshColliderShape>this._nativeShape).setTightBounds(this._tightBounds);

Script executed:

# Locate MeshColliderShape.ts file
fd "MeshColliderShape" packages/core/src/physics

Repository: galacean/engine


Repository: galacean/engine
Exit code: 0

stdout:

packages/core/src/physics/shape/MeshColliderShape.ts

Script executed:

# Check setMesh and setMeshData implementations
rg -n -A10 "setMesh\(|setMeshData\(" packages/core/src/physics/shape/MeshColliderShape.ts

Repository: galacean/engine


Repository: galacean/engine
Exit code: 0

stdout:

90:  setMeshData(vertices: Float32Array, indices?: Uint16Array | Uint32Array): void {
91-    this._vertices = vertices;
92-    this._indices = indices || null;
93-    this._updateNativeMesh();
94-  }
95-
96-  /**
97-   * Set mesh data from a Mesh object.
98-   * `@param` mesh - The mesh to extract vertex and index data from
99-   * `@remarks` The mesh must have accessible data (not released after upload)
100-   */
101:  setMesh(mesh: Mesh): void {
102-    if (mesh instanceof ModelMesh) {
103-      if (this._extractMeshData(mesh)) {
104-        this._updateNativeMesh();
105-      }
106-    } else {
107-      console.warn("MeshColliderShape: Only ModelMesh is supported");
108-    }
109-  }
110-
111-  protected override _syncNative(): void {
--
232:      (<IMeshColliderShape>this._nativeShape).setMeshData(this._vertices, vertexCount, this._indices, this._isConvex);
233-    } else {
234-      // Create new shape
235-      this._nativeShape = Engine._nativePhysics.createMeshColliderShape(
236-        this._id,
237-        this._vertices,
238-        vertexCount,
239-        this._indices,
240-        this._isConvex,
241-        this._material._nativeMaterial
242-      );

Script executed:

# Check if there's any shape attachment tracking in ColliderShape or DynamicCollider
rg -n "_shapes|_collider|parent" packages/core/src/physics/shape/ColliderShape.ts packages/core/src/physics/Collider.ts

Repository: galacean/engine


Repository: galacean/engine
Exit code: 0

stdout:

packages/core/src/physics/Collider.ts:28:  protected _shapes: ColliderShape[] = [];
packages/core/src/physics/Collider.ts:35:    return this._shapes;
packages/core/src/physics/Collider.ts:71:    const oldCollider = shape._collider;
packages/core/src/physics/Collider.ts:76:      this._shapes.push(shape);
packages/core/src/physics/Collider.ts:87:    const index = this._shapes.indexOf(shape);
packages/core/src/physics/Collider.ts:89:      this._shapes.splice(index, 1);
packages/core/src/physics/Collider.ts:99:    const shapes = this._shapes;
packages/core/src/physics/Collider.ts:173:    const shapes = this._shapes;
packages/core/src/physics/Collider.ts:184:    shape._collider = this;
packages/core/src/physics/Collider.ts:190:    shape._collider = null;
packages/core/src/physics/shape/ColliderShape.ts:18:  _collider: Collider;
packages/core/src/physics/shape/ColliderShape.ts:44:    return this._collider;
packages/core/src/physics/shape/ColliderShape.ts:148:    const collider = this._collider;
packages/core/src/physics/shape/ColliderShape.ts:188:    this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);
packages/core/src/physics/shape/ColliderShape.ts:194:    this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);
packages/core/src/physics/shape/ColliderShape.ts:200:    this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);

Add validation to prevent post-attachment isConvex toggles that would violate collider constraints.

The isConvex setter in MeshColliderShape (and setMesh/setMeshData methods) can switch a shape between convex and triangle-mesh modes without validating the parent collider's state. This bypasses the triangle-mesh restrictions enforced in DynamicCollider.addShape() and isKinematic setter.

Scenario: Add MeshColliderShape(isConvex=true) to a non-kinematic DynamicCollider (passes check), then toggle shape.isConvex = false—the shape becomes a non-supported triangle mesh on a non-kinematic collider.

Since ColliderShape tracks its parent via the _collider property, the setter should validate that if switching to non-convex mode (isConvex=false), the parent collider is either kinematic or null (not attached).

Also applies to: 421-427

🤖 Prompt for AI Agents
In `@packages/core/src/physics/DynamicCollider.ts` around lines 271 - 295, When
changing a MeshColliderShape from convex to non-convex (in the isConvex setter)
or when switching mesh via setMesh/setMeshData, validate the parent collider: if
this._collider is non-null and is a DynamicCollider and that DynamicCollider is
non-kinematic (isKinematic === false), disallow the transition (log/throw and
return) because triangle meshes are not supported on non-kinematic
DynamicCollider; update the MeshColliderShape.isConvex setter and both
setMesh/setMeshData to perform this check before mutating isConvex/mesh so you
cannot toggle to a triangle-mesh while attached to a dynamic (non-kinematic)
collider — mirror the same restriction enforced by DynamicCollider.addShape and
the isKinematic setter.

Remove e2e test files without baseline images:
- physx-mesh-collider-kinematic-bug.ts
- physx-mesh-collider-sphere-container.backup.ts
- physx-mesh-collider-sphere-container.ts
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/physics/shape/MeshColliderShape.ts`:
- Around line 217-229: Validate that this._vertices length is divisible by 3
and, when this._indices exists, its length is divisible by 3 before computing
vertexCount or using triangle indices in _updateNativeMesh (and consider similar
checks in setMeshData); if lengths are invalid, log a clear warning mentioning
MeshColliderShape and the offending array (vertices or indices) and return early
to avoid fractional vertexCount or incomplete triangles. Ensure checks reference
_updateNativeMesh, setMeshData, this._vertices, this._indices, and vertexCount
so reviewers can find the guarded logic quickly.
♻️ Duplicate comments (1)
packages/core/src/physics/shape/MeshColliderShape.ts (1)

155-176: Guard against unaligned buffer offsets before creating the Float32Array view.

Float32Array(buffer, byteOffset, length) requires 4‑byte alignment; the current code also divides byteStride/byteOffset by 4 without validation. A quick guard avoids RangeError and misreads.

🛡️ Proposed fix
     const vertexCount = mesh.vertexCount;
     const byteOffset = vertexElement.offset;
     const byteStride = bufferBinding.stride;
     const bufferData = buffer.data;
+    if (bufferData.byteOffset % 4 !== 0 || byteOffset % 4 !== 0 || byteStride % 4 !== 0) {
+      console.warn("MeshColliderShape: buffer offset/stride must be 4-byte aligned");
+      return false;
+    }
 
     // Reuse or create Float32Array
     if (!this._vertices || this._vertices.length !== vertexCount * 3) {
       this._vertices = new Float32Array(vertexCount * 3);
     }
JavaScript Float32Array byteOffset alignment requirement RangeError

Comment on lines +217 to +229
private _updateNativeMesh(): void {
if (!this._vertices || this._vertices.length === 0) {
return;
}

const vertexCount = this._vertices.length / 3;

// Validate triangle mesh has indices
if (!this._isConvex && !this._indices) {
console.warn("MeshColliderShape: Triangle mesh requires indices, skipping update");
return;
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate vertex/indices lengths to avoid fractional vertex counts and invalid triangles.

setMeshData is public; if the caller passes malformed arrays, vertexCount becomes fractional and triangle indices can be incomplete. Add a small guard and warn early.

🧯 Proposed fix
-    const vertexCount = this._vertices.length / 3;
+    if (this._vertices.length % 3 !== 0) {
+      console.warn("MeshColliderShape: vertices length must be a multiple of 3");
+      return;
+    }
+    const vertexCount = this._vertices.length / 3;
 
     // Validate triangle mesh has indices
     if (!this._isConvex && !this._indices) {
       console.warn("MeshColliderShape: Triangle mesh requires indices, skipping update");
       return;
     }
+    if (!this._isConvex && this._indices.length % 3 !== 0) {
+      console.warn("MeshColliderShape: indices length must be a multiple of 3");
+      return;
+    }
🤖 Prompt for AI Agents
In `@packages/core/src/physics/shape/MeshColliderShape.ts` around lines 217 - 229,
Validate that this._vertices length is divisible by 3 and, when this._indices
exists, its length is divisible by 3 before computing vertexCount or using
triangle indices in _updateNativeMesh (and consider similar checks in
setMeshData); if lengths are invalid, log a clear warning mentioning
MeshColliderShape and the offending array (vertices or indices) and return early
to avoid fractional vertexCount or incomplete triangles. Ensure checks reference
_updateNativeMesh, setMeshData, this._vertices, this._indices, and vertexCount
so reviewers can find the guarded logic quickly.

Copy link
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Choose a reason for hiding this comment

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

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isKinematic 为 false 时调用move() 后物理碰撞会失效

2 participants