Fix/shaderlab#2983
Conversation
…tin functions texture(sampler2D, vec2) returns GVec4 which was incorrectly resolved to the sampler type instead of vec4, causing "No overload function type found" when passing the result to user-defined functions like decode32(vec4). Add resolveGenericReturnType() to correctly map GSampler* → GVec4: sampler2D/sampler3D/samplerCube → vec4 isampler2D/isampler3D/... → ivec4 usampler2D/usampler3D/... → uvec4
…exture2DLod signatures - Simplify resolveGenericReturnType: remove genericParamType param, only check if return type is GVec4 - Fix textureCube/textureCubeLod return type: SAMPLER_CUBE → VEC4 - Add missing texture2DLod builtin function registration - Add texture2DLod test cases to texture-generic.shader
* feat: implement HorizontalBillboard render mode
… type When a builtin generic function (e.g. normalize) receives TypeAny args, resolvedReturnType stays TypeAny. Previously the else branch returned the raw EGenType enum value (200), which is neither a concrete type nor a wildcard, causing downstream user-function overload matching to fail.
…tin functions texture(sampler2D, vec2) returns GVec4 which was incorrectly resolved to the sampler type instead of vec4, causing "No overload function type found" when passing the result to user-defined functions like decode32(vec4). Add resolveGenericReturnType() to correctly map GSampler* → GVec4: sampler2D/sampler3D/samplerCube → vec4 isampler2D/isampler3D/... → ivec4 usampler2D/usampler3D/... → uvec4
…exture2DLod signatures - Simplify resolveGenericReturnType: remove genericParamType param, only check if return type is GVec4 - Fix textureCube/textureCubeLod return type: SAMPLER_CUBE → VEC4 - Add missing texture2DLod builtin function registration - Add texture2DLod test cases to texture-generic.shader
… type When a builtin generic function (e.g. normalize) receives TypeAny args, resolvedReturnType stays TypeAny. Previously the else branch returned the raw EGenType enum value (200), which is neither a concrete type nor a wildcard, causing downstream user-function overload matching to fail.
…ing CodeGen When #define values contain struct member access like `o.v_uv` (where `o` is a Varyings/Attributes/MRT struct variable), the CodeGen now correctly transforms them to just the property name (e.g. `v_uv`), matching the behavior of direct struct member access in regular code. Closes #2944.
…uct-access Verify the actual CodeGen output instead of just checking GLSL compilation: - #define values with struct member access are correctly transformed - varying/attribute declarations are emitted for referenced properties
…ions Add assertions for macro usage in expressions (not just #define transformation): - Macro as RHS in multiplication, as LHS in assignment - Macro as function argument in dot(), texture2D() - Multiple varying properties (v_uv, v_normal) referenced via #define
…ransform Build a combined _globalStructVarMap in visitShaderProgram by scanning both vertex and fragment entry functions, so global #define values like `attr.POSITION` or `o.v_uv` are correctly transformed in all stages. Rewrite define-struct-access tests to use snapshot file comparison against expected/ GLSL outputs for clearer verification.
…cro as builtin arg
…cro scanning - Suppress `uniform` output for global struct-typed variables (e.g. `Varyings o;`) - Register global struct vars in both per-function and cross-stage maps - Unify macro member access scanning into callback-based _forEachMacroMemberAccess - Add registerStructVar() encapsulation in VisitorContext - Add Cocos VSOutput pattern test (global-varying-var)
…version GLES100 visitJumpStatement converted `return expr;` to `gl_FragColor = expr` without a trailing semicolon, causing WebGL compilation errors. Only triggered when fragment entry returns vec4 (Cocos pattern), not void (standard Galacean).
…eprocessor #if !0 and similar expressions now work correctly, matching C/GLSL preprocessor behavior.
…atrix The viewMatrix getter intentionally ignores the camera entity's world scale (using Matrix.rotationTranslation), but _getInvViewProjMat() was using the full entity.transform.worldMatrix which includes inherited scale. This inconsistency causes screenPointToRay to produce incorrect world-space rays when the camera inherits scale from a parent entity (e.g. UICanvas in ScreenSpaceCamera mode with camera as a child of canvas). Closes #2748 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that screenPointToRay and viewport-world round-trip produce correct results when the camera inherits non-identity scale from a parent entity. Without the fix, the round-trip deviates by the inherited scale factor (e.g. 105 -> 107.5 at scale 1.5). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ReflectionParser 解析 IComponentRef(entityPath + componentType)时, 目标组件可能在 GLB clone 的子 entity 上而非 clone 根 entity 自身。 getComponents 找不到时 fallback 到 getComponentsIncludeChildren。
CloneManager: 当 source 和 target 属性是同类型 Object 实例(如 Vector4)时, 自动升级为 DeepClone,避免 prefab 模板的引用覆盖克隆体的独立实例。 新增 Map/Set 类型的 deep clone 支持。 ModelMesh: throw string 改为 throw Error 以获得正确堆栈。
AnimatorState.speed is part of the shared AnimatorController asset. Modifying it at runtime pollutes all Animator instances sharing the same controller, causing animation speed corruption after cloning. - Add speed field to AnimatorStatePlayData, initialized from AnimatorState.speed on reset - Add proxy properties (name/clip/wrapMode/transitions/addStateMachineScript) - Change speed calculation to playData.speed * animator.speed - findAnimatorState now returns per-instance AnimatorStatePlayData - Export AnimatorStatePlayData for consumer code
When sizeMode is set to Automatic, the UITransform size is automatically synchronized to the sprite's natural dimensions when the sprite changes. This matches Cocos Creator's Sprite.SizeMode.TRIMMED behavior. - Add SpriteSizeMode enum (Custom / Automatic) - Add sizeMode property to Image with getter/setter - Sync UITransform.size in set sprite and _onSpriteChange - Export SpriteSizeMode from component index
Four independent bugs all share the same root pattern — "JS field is the source of truth, native handle synced via setter side-effects" — fails whenever a code path bypasses the setter: - **R0** kinematic + CCD warning: setRigidBodyFlag(eENABLE_CCD,true) on a kinematic actor prints a PhysX warning and the flag becomes undefined when switching back to dynamic. Cache `_isKinematic` and `_collisionDetectionMode`; apply CCD flags only in dynamic state; on kinematic→dynamic transition, reapply the cached mode. - **R5** addForce/addTorque silently ignored on sleeping actors: PhysX wasm wrapper omits the `autowake` parameter (C++ default true, wasm default false). Explicitly `wakeUp()` before adding force/torque; guard kinematic to avoid triggering a separate PhysX warning. - **R6** MeshColliderShape async cook & prefab clone: `_cookMesh` can fail transiently when mesh data is not yet accessible; the cloned shape's `_nativeShape` is `@ignoreClone` so prefab instantiation produces a useless shape. Add `_pendingNativeShapeCreation` flag; retry every physics tick via new `ColliderShape._onPhysicsUpdate` hook (driven by `Collider._onUpdate`); override `_cloneTo` to cook a fresh native shape from already-cloned buffers. - **R8** PhysicsMaterial clone bypasses setters: CloneManager deep-copies `_bounciness/_friction/_combine` fields directly without invoking setters, so the target's freshly-constructed PxMaterial keeps its default values while JS fields show source values. Result: PhysX uses default `b=0` while logs and `material.bounciness` read source values — fully invisible divergence. Mark `_nativeMaterial` `@ignoreClone`; add `_cloneTo` → `_syncNative()` that re-writes all 5 fields to native via setter API. Side fix: PhysicsMaterial constructor was passing `bounceCombine` and `frictionCombine` to `createPhysicsMaterial` in reverse order — the bug was hidden because R8 kept native stuck at default Average (both reversed arguments are 0) and users only ever read JS fields. Fixed in the same patch. Test: `PhysicsMaterial.test.ts > cloned collider shape material keeps native values` verifies a cloned dynamic body bounces off a wall by simulating 40 frames and asserting vx flips negative — fails without the R8 fix.
Add Unity-style applyForceAtPosition(force, worldPosition) to apply a force at an arbitrary world-space point, producing both linear acceleration through the center of mass and angular acceleration about it. Internally decomposes into the textbook (F, τ = r × F) pair: worldCoM = entity.worldPos + entity.worldRot · localCoM (from native) r = worldPosition - worldCoM addForce(F); addTorque(r × F) Equivalent to Cocos `RigidBody.applyForce(force, relativePoint)` (Bullet's implementation is the same cross product); enables direct migration of billiards-style "hit at an offset point" gameplay without manual r × F math. Pure TS-layer composition over existing addForce/addTorque — no design interface change, no native binding change (physX.js untouched). Tests (6 cases, orthogonal coverage of all worldCoM computation terms): 1. force at CoM → only linear acceleration 2. r ≠ 0 produces torque = r × F, validated via differential vs applyTorque(τ) 3. CoM offset + force at world CoM → r = 0 → no torque 4. Entity rotated 90° + CoM offset → worldRot correctly transforms localCoM 5. (position + rotation + CoM offset + r ≠ 0) — full stress test catching any missing term in worldCoM = pos + rot·localCoM 6. After entity.clone() — defends prefab instantiation path (R6/R8 territory)
… root cause Previous commit 273d181 claimed "PhysX wasm wrapper's addForce doesn't pass the autowake parameter, so calls on sleeping actors are silently ignored". This claim is incorrect — the wasm binding explicitly passes autowake=true: body.addForce(force, PxForceMode::eFORCE, true); PhysX 4.1.1 doc confirms: "If true, this call wakes up the actor if it is sleeping." Two new tests verify autowake works as documented in wasm: - "applyForce on sleeping actor must wake up and apply force" → sleep() → applyForce (no explicit wakeUp) → force is applied, actor wakes - "applyForce after kinematic→dynamic switch (mimic billiards game break flow)" → reproduces the exact game scenario where 'force seemed lost' originally Both pass without the explicit wakeUp() call. The original 'applyForce lost' symptom in the billiards game was actually caused by other concurrent bugs (R1 collisionMatrix, R8 PhysicsMaterial, fixedTimeStep mismatch) that were each masking the real cause, not by sleeping-actor behavior. Keep the `_isKinematic` guard: PhysX makes addForce a no-op on kinematic actors (documented), but the guard avoids the wasm boundary cross.
…ysX scene queries) These tests were red on dev/2.0 before any of the recent physics work landed. Diagnosed and fixed at the root, no test-skipping or assertion weakening. **LitePhysicsMaterial — 1 failure** `ColliderShape Lite > clone` broke after R8 added `PhysicsMaterial._cloneTo → _syncNative()` which now calls every native setter on a freshly-cloned material. Lite's setters threw "Physics-lite don't support physics material", violating the no-op-with-log convention already used by `LiteColliderShape.setMaterial / setIsTrigger / setContactOffset`. Change all 5 setters to silent no-op + a one-time `console.log` warning, matching the existing convention. **PhysXPhysicsScene — 7 failures (raycast/boxCast/sphereCast/capsuleCast + overlapBoxAll/overlapSphereAll/overlapCapsuleAll)** Two underlying problems both surface in the JS filter-callback layer: 1. `_pxFilterData` had `POST_FILTER` flag enabled but `_overlapMultiple` reused it with a callback object that only defined `preFilter`. When PhysX tried to invoke `postFilter`, the wasm boundary errored with `Emval.toValue(...)[getStringOrSymbol(...)] is not a function`. 2. The 4 cast tests asserted the old "origin-inside-collider returns true with distance=0" behavior. The wasm side was already switched to the Unity-style "skip initial overlap" design (commit bdf5490 / PR #2998 on dev branch ahead of us), but the tests still encoded the rejected behavior. Mirror PR #2998's JS-side fix already merged on dev: - Split filter data into `_pxFilterData` (PRE only, for overlap) and `_pxRaycastSweepFilterData` (PRE + POST, for raycast/sweep). - Replace per-call `PxQueryFilterCallback.implement` with a single persistent `_pxQueryCallback` defining both `preFilter` and `postFilter`. User callbacks dispatch via a `_currentOnQuery` slot saved/restored around each native call for reentrancy safety. - Cache the `PxSweepHit` instance instead of creating per-call. - Add `QueryHitType` enum. Update 4 stale test assertions in PhysicsScene.test.ts to match the already-shipped "skip initial overlap" wasm contract. Use strictly-inside origin (2.9,2.9,2.9) for the raycast case to avoid the corner-tolerance flake that PR \#2998 already addressed. Verification: 11 physics test files, 213/213 passing locally.
…lone After fixing R0/R5/R6/R8 in commits 273d181 and 9c32650, run a RED-GREEN verification on each: temporarily revert the fix, run the test, confirm it fails; restore the fix, confirm it passes. Outcome reshaped what we thought the root causes were. R6 (MeshColliderShape clone) — real bug fix, RED verified `R6: cloned MeshColliderShape rebuilds its native PhysX shape` — With `_cloneTo` override removed, `clonedShape._nativeShape` is null and the cloned ground entity is not physically present (sphere falls through). With the fix, sphere lands on the cloned ground (sphereY > -1). R0 (kinematic + CCD) — defensive, not a bug fix `R0: CCD mode survives kinematic toggle (PhysX rejects CCD on kinematic)` and `R0: setCollisionDetectionMode in kinematic state defers application` — Both pass even with the R0 fix reverted. PhysX 4.1.1 already manages the CCD↔kinematic flag relation correctly on its own. R0's real value is: 1. Suppressing the PhysX warning that fires when CCD-on actors switch to kinematic (or vice-versa). 2. Preserving user intent — `collisionDetectionMode` getter returns the last user-set mode even while temporarily kinematic; on dynamic restore, CCD is reapplied without the user having to remember. These tests are kept as contract tests, not red-green regression tests. R5's wakeUp call was already removed in 9c32650 after its red test showed PhysX wasm `addForce(force, eFORCE, autowake=true)` wakes sleeping actors on its own — no separate test needed here. Verification: full physics suite 216/216 passing.
…ansform clone dirty flag
Two latent bugs surfaced jointly while fixing kinematic-pair contact callbacks (3D billiard
aim-line):
1. setGlobalPose vs setKinematicTarget routing (PhysX 4.x API misuse)
PhysX docs require setKinematicTarget for moving kinematic actors if collision detection
is needed; setGlobalPose is teleport and skips contact detection. SceneBinding already
sets kineKineFilteringMode = staticKineFilteringMode = eKEEP, but every transform sync
went through setWorldTransform → setGlobalPose, silently short-circuiting that intent.
Split Collider's native-sync responsibility into two virtual hooks:
- _teleportToEntityTransform(pos, rot): init/clone path, always setGlobalPose
- _syncEntityTransformToNative(pos, rot): per-frame runtime path
DynamicCollider overrides the runtime hook to call nativeCollider.move() (=
setKinematicTarget) when _isKinematic. CharacterController overrides the teleport hook
to use setWorldPosition (its native API lacks setWorldTransform).
PhysXDynamicCollider.move() also normalizes the rotation quaternion (PhysX requirement).
2. Transform._cloneTo missed world-flag dispatch
Earlier components on a cloned entity (e.g. DynamicCollider's native ctor) may query
transform.worldPosition, which clears WorldPosition / WorldMatrix dirty flags as a side
effect of caching. _cloneTo then writes new local values but never re-dirties or
dispatches the world flag set, so subsequent worldPosition reads return stale (0,0,0)
and registered listeners (Collider._updateFlag, etc.) never fire. Added
_worldAssociatedChange(WmWpWeWqWsWus) at the end of _cloneTo.
This bug was hidden under the old setGlobalPose path (first _onUpdate teleported the
actor regardless of cache freshness). Switching to setKinematicTarget exposed it as
Joint/HingeJoint/SpringJoint clone regressions where box2 flew at 299 m/s (implied
velocity from native actor at default (0,0,0) being kinematic-animated to entity's
true (0,5,0) over one 1/60s substep).
Test coverage:
- tests/src/core/Transform.test.ts: 2 new cases targeting _cloneTo dirty-flag dispatch
(worldPosition cache invalidation + listener fire). Both fail without the Transform.ts
fix.
- tests/src/core/physics/Collision.test.ts: 9 new cases covering kine-kine / kine-dyn /
dyn-dyn / dyn+frozen pairs via transform.setPosition. Confirms callback firing under
the new routing.
- tests/src/core/physics/PhysicsScene.test.ts: updated 2 assertions ("Dynamic Kinematic
vs Static", "Dynamic Trigger Kinematic vs Dynamic Kinematic") from expecting 0
callbacks to expecting callbacks fire — aligning with SceneDesc.kineKineFilteringMode
= eKEEP design intent.
- tests/src/core/physics/DynamicCollider.test.ts: additional kinematic-mode coverage.
All 231 physics + Transform tests pass. Joint / HingeJoint / SpringJoint / CharacterController
clone tests all GREEN.
GuoLei1990
left a comment
There was a problem hiding this comment.
审查(2026-05-15)
总结
本轮新增两个 commit(5ec7e3f + ab190ac):
5ec7e3ffix(physics): route kinematic transform sync through setKinematicTarget + fix Transform._cloneTo world-flag dispatch — 将 per-frame transform 同步路径拆分为_teleportToEntityTransform(init/clone)和_syncEntityTransformToNative(每帧),DynamicCollider覆盖后者,kinematic 时走move()(即setKinematicTarget)而非setGlobalPose,解决 kine-kine/kine-dynamiconCollisionEnter不触发问题;同时修复Transform._cloneTo漏发 world-flag 导致 clone 后 collider 位置 stale 的 bug。ab190acchore: release — 版本号game.11 → game.12,无逻辑改动
方向完全正确,PhysX setKinematicTarget vs setGlobalPose 的区别是核心问题所在,修复直指根因。Transform dirty-flag 传播修复也准确。
问题
[P1] Transform._cloneTo 测试绕过公开 entity.clone() 链路
// tests/src/core/Transform.test.ts
// @ts-ignore
source.transform._cloneTo(target.transform); // 直接调用私有方法
expect(target.transform.worldPosition).deepEqual({ x: 1, y: 2, z: 3 });这个 bug 的触发链路是:entity.clone() → 组件 ctor 提前读 worldPosition(清 dirty flag) → _cloneTo 写新值 → Collider 读到 stale (0,0,0)。测试却直接调 _cloneTo,绕过了 entity.clone() 的完整 clone 流程(包括组件的 _cloneFrom 顺序)。
这带来两个问题:
- 如果有人重构
entity.clone()内部调度顺序,导致 bug 复现,测试仍然绿——因为它不走entity.clone()路径。 - 测试的实质是验证"直接调
_cloneTo后 worldPosition 正确",而非"clone 后 collider 不读到 stale position"。
建议改为从公开 API 测:
// 源 entity 在 (1,2,3)
const src = rootEntity.createChild("src");
src.transform.setPosition(1, 2, 3);
src.addComponent(DynamicCollider); // DynamicCollider ctor 会读 worldPosition
const cloned = src.clone();
// 断言 cloned 上的 DynamicCollider 收到正确的 worldPosition 通知
// (可通过检查 native actor 位置或注册 worldChangeFlag 来断言)[P2] Collision.test.ts — "frozen + teleport" 测试只有 resolve 无 reject,失败机制是 expect.fail() 意外 throw
it("dynamic + frozen-6 + teleport: ...", function () {
return new Promise<void>((resolve) => { // ← 缺少 reject 参数
// ...
if (!fired) {
expect.fail("expected onCollisionEnter to fire..."); // ← throw 导致 Promise 隐式 reject
}
});
});当前偶然能工作(expect.fail() throw 被 Promise 构造器捕获后隐式 reject),但这不是标准写法。正确形式:
return new Promise<void>((resolve, reject) => {
// ...
if (!fired) {
reject(new Error("expected onCollisionEnter to fire for dynamic-frozen pair after teleport"));
}
});与同文件中其他 probeKinematicCallback 测试(显式传 reject)保持一致。
[P0] e2e/config.ts 删除后 e2e/tests/index.spec.ts 仍然 import "../config" — CI 必然报错(已提 2 轮,仍未修复)
PR diff 删除 e2e/config.ts,但 e2e/tests/index.spec.ts 仍 import { E2E_CONFIG } from "../config"(硬依赖,无降级),合并后 playwright suite 无法启动。
[P1] UIPointerEventEmitter._bubble — 冒泡结束后 currentTarget 未清空(已提 5 轮,仍未修复)
for (let i = 0; i < length; i++) {
eventData.currentTarget = path[i];
fireEvent(path[i], eventData);
}
// 缺少:eventData.currentTarget = null;[P1] Collider._onUpdate — _onPhysicsUpdate 每帧对所有 shape 无条件调用(已提 3 轮,仍未修复)
_onUpdate(): void {
if (this._updateFlag.flag) {
// ... transform sync ...
}
for (let i = 0, n = shapes.length; i < n; i++) { // ← 在 guard 块外,每帧无条件调用
shapes[i]._onPhysicsUpdate();
}
}[P1] findAnimatorState 返回类型 breaking change 且语义错误(已提 2 轮,仍未修复)
state 未在播放时,返回的是当前播放的 srcPlayData(其 .state 是另一个动画),语义错误。应返回 null。
[P2] PointerEventData.target / currentTarget 类型声明不含 null(已提 5 轮,仍未修复)
target: Entity = null; // 应为 Entity | null
currentTarget: Entity = null; // 应为 Entity | null
GuoLei1990
left a comment
There was a problem hiding this comment.
说明
PR #2983(Fix/shaderlab)是一个大型综合 PR,包含多个方向的改动(ShaderLab、动画、GLTF 加载器、物理、UI 等)。从 PR 描述来看,其中的动画+GLTF 修复已被抽离到 #2999,物理 raycast/sweep 修复已被抽离到 #2998。
建议继续将剩余改动拆分成独立 PR 再逐一合入 dev/2.0,而不是合并这个全量 PR。原因:
- 混合多个功能域的 PR 难以做 code review(注意力稀释)
- 如果其中某个方向有问题,回滚代价高
- PR 描述模板中的 checklist 全部未填写,缺少变更说明
如果后续有独立拆分的 PR,请在各 PR 上发起 review。此 PR 暂不做详细 diff 审查。
Please check if the PR fulfills these requirements
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
What is the current behavior? (You can also link to an open issue here)
What is the new behavior (if this is a feature change)?
Does this PR introduce a breaking change? (What changes might users need to make in their application due to this PR?)
Other information: