背景
Renderer._updateTransformShaderData 用 `Matrix.invert` + `transpose` 算 `renderer_NormalMat`:
```ts
// packages/core/src/Renderer.ts:402-404
Matrix.multiply(context.viewMatrix, worldMatrix, mvMatrix);
Matrix.invert(worldMatrix, normalMatrix);
normalMatrix.transpose();
```
当 `worldMatrix` singular(任意一个 scale 分量为 0,比如动画"压扁消失"过渡帧 `scale = (1, 0, 1)`),`Matrix.invert` 的早返保护启动:
```ts
// packages/math/src/Matrix.ts:449-452
let det = ...;
if (!det) {
return null; // ← 不写 out
}
```
`return null` 时 out(`normalMatrix`)不被写入 ,调用方 line 403 不检查返回值 ,下一行 `normalMatrix.transpose()` 在保留值上 transpose 一次,最终上传到 GPU。
问题
上传的 `normalMatrix` 是:
第一帧 singular:identity(构造时初始值).transpose() = identity
后续帧 singular:上一帧已 transpose 过的值再 transpose 一次
两种情况都是有效浮点数,不传染 NaN、不崩屏,但法线方向与几何不一致 :
场景
上传的 normalMatrix
视觉表现
第一帧 singular
identity
法线沿对象局部坐标系,未跟随 scale=0 压扁
持续 singular
上一帧 transpose 链结果
法线方向"卡住"在某个旧状态
从非 singular → singular
上一帧正确法线
法线"延迟反应",光照值在过渡帧偏离
严重度
不阻塞渲染(上传的是有效浮点数,没 NaN)
视觉影响在 singular 帧(压扁动画过渡帧)—— 此时物体几何体积接近 0,视觉感知较弱
严重度 P2 (cosmetic) ,不是 P0
修复方向(与 instance 路径对齐)
instance 路径(GPU shader 端)已在 PR #2957 commit `0805f2d93` 改成 cofactor 形式,代数等价 `det(M) · transpose(inverse(M))`,无除法、对 singular NaN-safe、性能更好。
CPU 路径也应该改为 cofactor 形式:
```ts
// 提议在 Matrix3x3 / Matrix 添加 API,大致形态:
static normalMatrix3FromAffine(affine: Matrix, out: Matrix3x3): void {
const e = affine.elements;
// 取 affine 的 mat3 部分,做 cofactor:
// c0 = cross(m[1], m[2])
// c1 = cross(m[2], m[0])
// c2 = cross(m[0], m[1])
// sign(det) 用三重积修正
...
}
```
Renderer.ts 改:
```ts
// 替换 line 403-404
Matrix3x3.normalMatrix3FromAffine(worldMatrix, this._normalMatrix3);
shaderData.setMatrix3x3(Renderer._normalMatrixProperty, this._normalMatrix3);
```
收益
正确性 :第一帧 singular / 持续 singular 都给精确退化结果,跟几何对齐,不依赖"上一帧值还在"
性能 :cross + dot 不需要 mat4 inverse 的完整 cofactor 展开 + 除法。每帧每 renderer 省 1 次除法 + ~47 ops
一致性 :CPU/GPU 路径行为完全对齐(修完 PR feat(core): GPU instancing auto-batching #2957 后,instance 路径反而比 CPU 路径更准确,这是反常状态)
行业对照
Filament : `getWorldFromModelNormalMatrix` 走 cofactor
Unreal : `MaterialTemplate.ush` 走 cofactor
Three.js : `Matrix3.getNormalMatrix` 走 cofactor (PR #19620+)
Out of scope
关联
背景
Renderer._updateTransformShaderData用 `Matrix.invert` + `transpose` 算 `renderer_NormalMat`:```ts
// packages/core/src/Renderer.ts:402-404
Matrix.multiply(context.viewMatrix, worldMatrix, mvMatrix);
Matrix.invert(worldMatrix, normalMatrix);
normalMatrix.transpose();
```
当 `worldMatrix` singular(任意一个 scale 分量为 0,比如动画"压扁消失"过渡帧 `scale = (1, 0, 1)`),`Matrix.invert` 的早返保护启动:
```ts
// packages/math/src/Matrix.ts:449-452
let det = ...;
if (!det) {
return null; // ← 不写 out
}
```
`return null` 时 out(`normalMatrix`)不被写入,调用方 line 403 不检查返回值,下一行 `normalMatrix.transpose()` 在保留值上 transpose 一次,最终上传到 GPU。
问题
上传的 `normalMatrix` 是:
两种情况都是有效浮点数,不传染 NaN、不崩屏,但法线方向与几何不一致:
严重度
修复方向(与 instance 路径对齐)
instance 路径(GPU shader 端)已在 PR #2957 commit `0805f2d93` 改成 cofactor 形式,代数等价 `det(M) · transpose(inverse(M))`,无除法、对 singular NaN-safe、性能更好。
CPU 路径也应该改为 cofactor 形式:
```ts
// 提议在 Matrix3x3 / Matrix 添加 API,大致形态:
static normalMatrix3FromAffine(affine: Matrix, out: Matrix3x3): void {
const e = affine.elements;
// 取 affine 的 mat3 部分,做 cofactor:
// c0 = cross(m[1], m[2])
// c1 = cross(m[2], m[0])
// c2 = cross(m[0], m[1])
// sign(det) 用三重积修正
...
}
```
Renderer.ts 改:
```ts
// 替换 line 403-404
Matrix3x3.normalMatrix3FromAffine(worldMatrix, this._normalMatrix3);
shaderData.setMatrix3x3(Renderer._normalMatrixProperty, this._normalMatrix3);
```
收益
行业对照
Out of scope
关联