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 抓的)。
149 lines
3.5 KiB
HTML
149 lines
3.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>meshoptimizer - demo</title>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
|
|
<style>
|
|
body {
|
|
font-family: Monospace;
|
|
background-color: #000;
|
|
color: #fff;
|
|
margin: 0px;
|
|
overflow: hidden;
|
|
}
|
|
#info {
|
|
color: #fff;
|
|
position: absolute;
|
|
top: 10px;
|
|
width: 100%;
|
|
text-align: center;
|
|
z-index: 100;
|
|
display: block;
|
|
}
|
|
#info a,
|
|
.button {
|
|
color: #f00;
|
|
font-weight: bold;
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
|
|
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="info">
|
|
<a href="https://github.com/zeux/meshoptimizer" target="_blank" rel="noopener">meshoptimizer</a>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
|
|
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
|
|
|
|
var container;
|
|
|
|
var camera, scene, renderer, mixer, timer;
|
|
|
|
var windowHalfX = window.innerWidth / 2;
|
|
var windowHalfY = window.innerHeight / 2;
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100);
|
|
camera.position.y = 1.0;
|
|
camera.position.z = 3.0;
|
|
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x300a24);
|
|
|
|
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
|
|
scene.add(ambientLight);
|
|
|
|
var pointLight = new THREE.PointLight(0xffffff, 4);
|
|
pointLight.position.set(3, 3, 0);
|
|
pointLight.decay = 0.5;
|
|
camera.add(pointLight);
|
|
scene.add(camera);
|
|
|
|
var onProgress = function (xhr) {};
|
|
var onError = function (e) {
|
|
console.log(e);
|
|
};
|
|
|
|
var loader = new GLTFLoader();
|
|
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
loader.load(
|
|
'pirate.glb',
|
|
function (gltf) {
|
|
var bbox = new THREE.Box3().setFromObject(gltf.scene);
|
|
var scale = 2 / (bbox.max.y - bbox.min.y);
|
|
|
|
gltf.scene.scale.set(scale, scale, scale);
|
|
gltf.scene.position.set(0, 0, 0);
|
|
|
|
scene.add(gltf.scene);
|
|
|
|
mixer = new THREE.AnimationMixer(gltf.scene);
|
|
|
|
if (gltf.animations.length) {
|
|
mixer.clipAction(gltf.animations[gltf.animations.length - 1]).play();
|
|
}
|
|
},
|
|
onProgress,
|
|
onError
|
|
);
|
|
|
|
renderer = new THREE.WebGLRenderer();
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
container.appendChild(renderer.domElement);
|
|
|
|
window.addEventListener('resize', onWindowResize, false);
|
|
|
|
timer = new THREE.Timer();
|
|
timer.connect(document);
|
|
}
|
|
|
|
function onWindowResize() {
|
|
windowHalfX = window.innerWidth / 2;
|
|
windowHalfY = window.innerHeight / 2;
|
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate(timestamp) {
|
|
timer.update(timestamp);
|
|
|
|
if (mixer) {
|
|
mixer.update(timer.getDelta());
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|