feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级
vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh 多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间 选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半, 相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。 SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod): 主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联 按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。 A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off): - 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异 - 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异 结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施 保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy GPU 时间线定位真实瓶颈。 坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加 (buffer 无重复数据,累加必越界——VUID-08798 抓的)。
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import assert from 'assert/strict';
|
||||
import { MeshoptTangents as tangents } from './meshopt_tangents.js';
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.log('unhandledRejection', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
var tests = {
|
||||
tangentsBasic: function () {
|
||||
// unindexed quad with matching corner data for shared diagonal
|
||||
var positions = new Float32Array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0]);
|
||||
var normals = new Float32Array([-0.28, 0, 0.96, 0.28, 0, 0.96, 0.28, 0, 0.96, -0.28, 0, 0.96, 0.28, 0, 0.96, -0.28, 0, 0.96]);
|
||||
var uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
|
||||
|
||||
// (1, 0, 0) reprojected onto tilted normals
|
||||
var left = [0.96, 0, 0.28, 1];
|
||||
var right = [0.96, 0, -0.28, 1];
|
||||
var expected = [left, right, right, left, right, left].flat();
|
||||
|
||||
// unindexed input: indices = null
|
||||
var result = tangents.generateTangents(null, positions, 3, normals, 3, uvs, 2, []);
|
||||
|
||||
assert.equal(result.length, expected.length);
|
||||
|
||||
for (var i = 0; i < result.length; ++i) {
|
||||
assert(Math.abs(result[i] - expected[i]) < 1e-3);
|
||||
}
|
||||
|
||||
// shared vertices get the same tangent vector
|
||||
for (var k = 0; k < 4; ++k) {
|
||||
assert(result[0 * 4 + k] === result[3 * 4 + k]); // diag 1
|
||||
assert(result[2 * 4 + k] === result[4 * 4 + k]); // diag 2
|
||||
}
|
||||
},
|
||||
|
||||
tangentDegenerate: function () {
|
||||
var positions = new Float32Array([0, 0, 0, 1, 1, 0, 2, 2, 0, -1, -2, 1, 0, 1, 1, -1, 0, 0]);
|
||||
var normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
|
||||
var uvs = new Float32Array([0, 0, 1, 1, 2, 2, 0, -2, 1, 0, 1, 0]);
|
||||
|
||||
var indices = new Uint32Array([
|
||||
// outer, positive
|
||||
0, 3, 1,
|
||||
// inner, degenerate UVs
|
||||
0, 1, 2,
|
||||
// outer, positive
|
||||
1, 4, 2,
|
||||
// outer, negative
|
||||
2, 5, 0,
|
||||
]);
|
||||
|
||||
var vx = 0.0836;
|
||||
var vy = 0.9965;
|
||||
var expected = [
|
||||
// outer, positive
|
||||
[1, 0, 0, 1],
|
||||
[1, 0, 0, 1],
|
||||
[vx, vy, 0, 1],
|
||||
// inner, degenerate UVs
|
||||
[1, 0, 0, 1],
|
||||
[vx, vy, 0, 1],
|
||||
[0, 1, 0, 1],
|
||||
// outer, positive
|
||||
[vx, vy, 0, 1],
|
||||
[0, 1, 0, 1],
|
||||
[0, 1, 0, 1],
|
||||
// outer, negative
|
||||
[-1, 0, 0, -1],
|
||||
[-1, 0, 0, -1],
|
||||
[-1, 0, 0, -1],
|
||||
].flat();
|
||||
|
||||
var result = tangents.generateTangents(indices, positions, 3, normals, 3, uvs, 2, ['Compatible']);
|
||||
|
||||
assert.equal(result.length, expected.length);
|
||||
|
||||
for (var i = 0; i < result.length; ++i) {
|
||||
assert(Math.abs(result[i] - expected[i]) < 1e-3);
|
||||
}
|
||||
},
|
||||
|
||||
normalsBasic: function () {
|
||||
var positions = new Float32Array([-1, -0.57735, 0, 1, -0.57735, 0, 0, 1.1547, 0, 0, 0, 0.38]);
|
||||
|
||||
// flattened tetrahedron (apex is closer to the base so that we can get soft side edges)
|
||||
var indices = new Uint32Array([0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3]);
|
||||
|
||||
var base = [0, 0, -1];
|
||||
var side0 = [-0.2707, -0.1563, 0.9499];
|
||||
var side1 = [0.2707, -0.1563, 0.9499];
|
||||
var side2 = [0, 0.3126, 0.9499];
|
||||
var apex = [0, 0, 1];
|
||||
var expected = [base, base, base, side0, side1, apex, side1, side2, apex, side2, side0, apex].flat();
|
||||
|
||||
var result = tangents.generateNormals(indices, positions, 3, Math.PI / 3);
|
||||
|
||||
assert.equal(result.length, expected.length);
|
||||
|
||||
for (var i = 0; i < result.length; ++i) {
|
||||
assert(Math.abs(result[i] - expected[i]) < 1e-3);
|
||||
}
|
||||
|
||||
// shared vertices around soft side edges get the same normal vector
|
||||
for (var k = 0; k < 3; ++k) {
|
||||
assert(result[3 * 3 + k] === result[10 * 3 + k]); // side0
|
||||
assert(result[4 * 3 + k] === result[6 * 3 + k]); // side1
|
||||
assert(result[7 * 3 + k] === result[9 * 3 + k]); // side2
|
||||
assert(result[5 * 3 + k] === result[8 * 3 + k]); // apex
|
||||
assert(result[5 * 3 + k] === result[11 * 3 + k]); // apex
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tangents.ready.then(() => {
|
||||
var count = 0;
|
||||
|
||||
for (var key in tests) {
|
||||
tests[key]();
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log(count, 'tests passed');
|
||||
});
|
||||
Reference in New Issue
Block a user