Files
HelloVulkan/Lib/meshoptimizer/demo/remesh.html
T
atdunbg 5bdfb53566 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 抓的)。
2026-09-24 20:53:19 +08:00

816 lines
26 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: #300a24;
color: #fff;
margin: 0px;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100vh;
}
#container {
width: 100%;
flex: 1;
position: relative;
}
.renderer-container {
position: relative;
width: 100%;
height: 100%;
}
.renderer-container canvas {
width: 100% !important;
height: 100% !important;
display: block;
user-select: none;
}
.renderer-label {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 14px;
}
.stats-label {
position: absolute;
top: 45px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 12px;
line-height: 1.4;
}
#progress-label {
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
#texture-view,
#normal-view,
#material-view {
position: fixed;
left: 10px;
bottom: 10px;
width: 256px;
height: 256px;
display: none;
}
#normal-view {
left: 276px;
}
#material-view {
left: 542px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/",
"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.10/build/index.module.js",
"watlas": "https://cdn.jsdelivr.net/npm/watlas@1.0.1/dist/watlas.js",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js"
}
}
</script>
</head>
<body>
<div id="progress-label" class="renderer-label" hidden></div>
<div id="container"></div>
<canvas id="texture-view"></canvas>
<canvas id="normal-view"></canvas>
<canvas id="material-view"></canvas>
<input type="file" id="fileInput" style="display: none" accept=".obj,.glb" />
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
import { OBJLoader } from 'three-examples/loaders/OBJLoader.js';
import { GLTFExporter } from 'three-examples/exporters/GLTFExporter.js';
import { OrbitControls } from 'three-examples/controls/OrbitControls.js';
import { mergeGeometries, mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
import { MeshoptSimplifier } from '../js/meshopt_simplifier.js';
import { MeshoptTangents } from '../js/meshopt_tangents.js';
import * as repaint from './repaint.js';
import { Initialize as initializeWatlas, Atlas } from 'watlas';
import { GUI } from 'lil-gui';
var container;
var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, wireframeMaterial, stats;
var paintCache = null;
var progressLabel = document.getElementById('progress-label');
var settings = {
wireframe: false,
wireframeOverlay: false,
resolution: 100,
thicken: false,
shell: false,
solve: true,
debug: false,
normalCrease: 60,
normalSmoothing: 1.5,
simplify: {
enabled: true,
regularize: true,
solve: true,
errorAuto: false,
errorThresholdLog10: 2.5,
forceTriangleCount: 0,
},
paint: {
mode: 'texture',
resolution: 1024,
gutter: 1,
atlasMode: 'balanced',
preserveNormalSeams: true,
infill: true,
normals: false,
materials: false,
},
loadFile: function () {
var input = document.getElementById('fileInput');
input.onchange = function () {
var file = input.files[0];
var uri = URL.createObjectURL(file);
var ext = file.name.split('.').pop().toLowerCase();
container.querySelector('.renderer-label').textContent = file.name;
load(uri, ext);
};
input.click();
},
remesh: function () {
remesh();
},
revert: function () {
revert();
},
exportFile: function () {
if (!remeshed) return;
exportGlb('remesh.glb');
},
updateModule: function () {
reload();
remesh();
},
autoUpdate: false,
autoUpdateStatus: '',
};
var gui = new GUI({ width: 300 });
gui.add(settings, 'remesh');
gui.add(settings, 'revert');
gui.add(settings, 'loadFile');
gui.add(settings, 'exportFile');
var guiDisplay = gui.addFolder('Display');
guiDisplay.add(settings, 'wireframe').onChange(update);
guiDisplay.add(settings, 'wireframeOverlay').onChange(update);
var guiRemesh = gui.addFolder('Remesh');
guiRemesh.add(settings, 'resolution', 4, 256, 1);
guiRemesh.add(settings, 'thicken').disable();
guiRemesh.add(settings, 'shell');
guiRemesh.add(settings, 'solve');
guiRemesh.add(settings, 'debug');
guiRemesh.add(settings, 'normalCrease', 0, 180, 1).name('normal crease');
guiRemesh.add(settings, 'normalSmoothing', 0, 10, 0.1).name('normal smoothing');
var guiSimplify = gui.addFolder('Simplify');
guiSimplify.add(settings.simplify, 'enabled');
guiSimplify.add(settings.simplify, 'regularize');
guiSimplify.add(settings.simplify, 'solve');
guiSimplify.add(settings.simplify, 'errorAuto');
guiSimplify.add(settings.simplify, 'errorThresholdLog10', 1.5, 3, 0.1);
guiSimplify.add(settings.simplify, 'forceTriangleCount');
var guiPaint = gui.addFolder('Paint');
guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'facets', 'texture']);
guiPaint.add(settings.paint, 'resolution', [256, 512, 1024, 2048]);
guiPaint.add(settings.paint, 'gutter', 0, 8, 1);
guiPaint.add(settings.paint, 'atlasMode', ['fast', 'balanced', 'quality']);
guiPaint.add(settings.paint, 'preserveNormalSeams');
guiPaint.add(settings.paint, 'infill');
guiPaint.add(settings.paint, 'normals').name('normal map');
guiPaint.add(settings.paint, 'materials').name('material map');
var guiUpdate = gui.addFolder('Update');
guiUpdate.add(settings, 'updateModule');
guiUpdate.add(settings, 'autoUpdate').onChange(autoReload);
guiUpdate.add(settings, 'autoUpdateStatus').listen();
init();
loadDefault();
render();
function createView(name) {
// Create renderer container
var rendererContainer = document.createElement('div');
rendererContainer.className = 'renderer-container';
container.appendChild(rendererContainer);
// Add label with file name
var label = document.createElement('div');
label.className = 'renderer-label';
label.textContent = name;
rendererContainer.appendChild(label);
// Add stats label
var statsLabel = document.createElement('div');
statsLabel.className = 'stats-label';
rendererContainer.appendChild(statsLabel);
// Initialize stats
stats = {
triangles: 0,
vertices: 0,
ratio: 0,
label: statsLabel,
};
// Create renderer
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
rendererContainer.appendChild(renderer.domElement);
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
// Add lighting
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
var pointLightR = new THREE.PointLight(0xffffff, 4);
pointLightR.position.set(3, 3, 0);
pointLightR.decay = 0.5;
scene.add(pointLightR);
var pointLightL = new THREE.PointLight(0xffffff, 1);
pointLightL.position.set(-3, 5, 0);
pointLightL.decay = 0.5;
scene.add(pointLightL);
remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd, polygonOffsetFactor: 0.5, polygonOffsetUnits: 16 });
wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, visible: false });
// Create controls
controls = new OrbitControls(camera, renderer.domElement);
// Adjust renderer size
updateRendererSizes();
}
function copyAttribute(attribute, count, itemSize, def) {
var result = new Float32Array(count * itemSize).fill(def);
if (attribute) {
var copy = Math.min(itemSize, attribute.itemSize);
for (var i = 0; i < count; ++i) {
for (var k = 0; k < copy; ++k) {
result[i * itemSize + k] = attribute.getComponent(i, k);
}
}
}
return new THREE.BufferAttribute(result, itemSize);
}
function skinAttribute(object, attribute, w) {
var v = new THREE.Vector4();
for (var i = 0; i < attribute.count; ++i) {
v.set(attribute.getX(i), attribute.getY(i), attribute.getZ(i), w);
object.applyBoneTransform(i, v);
if (w == 0) v.normalize();
attribute.setXYZ(i, v.x, v.y, v.z);
}
}
function mergeMesh(model) {
var geometries = [];
var materials = [];
model.updateMatrixWorld(true);
model.traverse(function (object) {
if (object.isMesh && object.geometry.attributes.position) {
var source = object.geometry.attributes.position;
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', copyAttribute(source, source.count, 3, 0));
// we need original normal/uv/colors for gpu repaint
// for mergeGeometries to agree to merge everything, we maintain consistent attribute sets
geometry.setAttribute('normal', copyAttribute(object.geometry.attributes.normal, source.count, 3, 0));
geometry.setAttribute('uv', copyAttribute(object.geometry.attributes.uv, source.count, 2, 0));
geometry.setAttribute('color', copyAttribute(object.geometry.attributes.color, source.count, 4, 1));
if (object.isSkinnedMesh) {
skinAttribute(object, geometry.attributes.position, 1);
skinAttribute(object, geometry.attributes.normal, 0);
}
var material = Array.isArray(object.material) ? object.material[0] : object.material;
if (materials.indexOf(material) < 0) materials.push(material);
var materialIndex = new Uint32Array(source.count).fill(materials.indexOf(material));
geometry.setAttribute('material', new THREE.BufferAttribute(materialIndex, 1));
if (object.geometry.index) {
geometry.setIndex(object.geometry.index);
} else {
var indices = new Uint32Array(source.count);
for (var i = 0; i < source.count; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
}
// we prefer to use the original tangents, but we need to generate them if not present so that normal baking works
if (object.geometry.attributes.tangent) {
geometry.setAttribute('tangent', copyAttribute(object.geometry.attributes.tangent, source.count, 4, 0));
if (object.isSkinnedMesh) skinAttribute(object, geometry.attributes.tangent, 0);
} else {
geometry = generateTangents(geometry, -1);
geometry = mergeVertices(geometry);
}
geometry.applyMatrix4(object.matrixWorld);
geometries.push(geometry);
}
});
var result = mergeGeometries(geometries);
result.computeBoundingBox();
result.computeBoundingSphere();
result.materials = materials;
result.thickness = result.boundingBox.getSize(new THREE.Vector3()).length() * 0.01;
return result;
}
function unwrapMesh(geometry, resolution, gutter, preserveNormalSeams, maxIterations, maxChartSize) {
var positions = geometry.attributes.position.array;
var indices = new Uint32Array(geometry.index.array);
var triangle = new THREE.Triangle();
var avgTriangleArea = 0;
for (var i = 0; i < indices.length; i += 3) {
triangle.a.fromBufferAttribute(geometry.attributes.position, indices[i + 0]);
triangle.b.fromBufferAttribute(geometry.attributes.position, indices[i + 1]);
triangle.c.fromBufferAttribute(geometry.attributes.position, indices[i + 2]);
avgTriangleArea += triangle.getArea() / (indices.length / 3);
}
var atlas = new Atlas();
atlas.addMesh({
vertexPositionData: positions,
vertexCount: positions.length / 3,
vertexPositionStride: 12,
vertexNormalData: geometry.attributes.normal.array,
vertexNormalStride: 12,
indexData: indices,
indexCount: indices.length,
});
atlas.generate(
{ normalSeamWeight: preserveNormalSeams ? 1000 : 0, maxIterations: maxIterations, maxChartArea: maxChartSize * avgTriangleArea },
{ resolution: resolution, padding: gutter }
);
var mesh = atlas.getMesh(0);
var atlasIndices = new Uint32Array(mesh.indexCount);
var uv = new Float32Array(mesh.indexCount * 2);
mesh.getIndexArray(atlasIndices);
for (var i = 0; i < mesh.indexCount; ++i) {
var v = mesh.getVertex(atlasIndices[i]);
uv[i * 2 + 0] = v.uv[0] / atlas.width;
uv[i * 2 + 1] = v.uv[1] / atlas.height;
}
atlas.delete();
// uvs are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = geometry.toNonIndexed();
geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
return geometry;
}
function generateTangents(geometry, sign = 1) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var tangents = MeshoptTangents.generateTangents(indices, attr.position.array, 3, attr.normal.array, 3, attr.uv.array, 2);
for (var i = 3; i < tangents.length; i += 4) tangents[i] *= sign;
// tangents are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
if (indices) geometry = geometry.toNonIndexed();
geometry.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4));
return geometry;
}
function generateNormals(geometry, creaseAngle, smoothing) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var normals = MeshoptTangents.generateNormals(indices, attr.position.array, 3, creaseAngle, smoothing);
// normals are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = indices ? geometry.toNonIndexed() : geometry.clone();
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
return geometry;
}
function showTexture(id, texture) {
var canvas = document.getElementById(id);
canvas.style.display = texture ? 'block' : 'none';
if (!texture) return;
canvas.width = texture.image.width;
canvas.height = texture.image.height;
canvas.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(texture.image.data), canvas.width, canvas.height), 0, 0);
}
function clearRemeshed() {
if (remeshed) {
scene.remove(remeshed);
remeshed.geometry.dispose();
}
remeshed = undefined;
if (remeshMaterial.map) remeshMaterial.map.dispose();
remeshMaterial.map = null;
if (remeshMaterial.normalMap) remeshMaterial.normalMap.dispose();
remeshMaterial.normalMap = null;
// roughness and metalness share one texture, so we only dispose once
if (remeshMaterial.metalnessMap) remeshMaterial.metalnessMap.dispose();
remeshMaterial.metalnessMap = null;
remeshMaterial.roughnessMap = null;
showTexture('texture-view', null);
showTexture('normal-view', null);
showTexture('material-view', null);
}
function simplifyMesh(geo, target, threshold, flags, solve) {
var positions = solve ? new Float32Array(geo.attributes.position.array) : geo.attributes.position.array;
var indices = solve ? new Uint32Array(geo.index.array) : geo.index.array;
var res;
if (solve) {
res = MeshoptSimplifier.simplifyWithUpdate(indices, positions, 3, new Float32Array(), 0, [], null, target * 3, threshold, flags);
res[0] = indices.slice(0, res[0]);
} else {
res = MeshoptSimplifier.simplify(indices, positions, 3, target * 3, threshold, flags);
}
var [remap, unique] = MeshoptSimplifier.compactMesh(res[0]);
var compact = new Float32Array(unique * 3);
for (var i = 0; i < remap.length; ++i) {
if (remap[i] < unique) compact.set(positions.subarray(i * 3, i * 3 + 3), remap[i] * 3);
}
var rgeo = geo.clone();
rgeo.setAttribute('position', new THREE.BufferAttribute(compact, 3));
rgeo.setIndex(new THREE.BufferAttribute(res[0], 1));
return rgeo;
}
function showProgress(stage) {
progressLabel.textContent = stage;
progressLabel.hidden = false;
return new Promise(requestAnimationFrame);
}
async function remesh() {
if (!merged) return;
await Promise.all([MeshoptSimplifier.ready, MeshoptTangents.ready, initializeWatlas()]);
await showProgress('remesh');
var flags = [];
if (settings.thicken) flags.push('Thicken');
if (settings.shell) flags.push('Shell');
if (settings.solve) flags.push('Solve');
if (settings.debug) flags.push('_InternalDebug');
var result = MeshoptSimplifier.remesh(merged.index.array, merged.attributes.position.array, 3, settings.resolution, flags);
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(result, 3));
geometry.setIndex(new THREE.BufferAttribute(MeshoptSimplifier.generatePositionRemap(result, 3), 1));
if (settings.simplify.enabled) {
await showProgress('simplify');
var sflags = ['PreserveFolds', 'Sparse']; // Sparse is needed because we skip position compaction above to be able to use quick remap to generate indices
if (settings.simplify.regularize) sflags.push('RegularizeLight');
var error = settings.simplify.forceTriangleCount ? 1 : Math.pow(10, -settings.simplify.errorThresholdLog10);
if (settings.simplify.errorAuto) {
sflags.push('ErrorAbsolute'); // merged mesh is in world space, so we don't need to adjust error by mesh scale and can simply use world space units
var distance = Math.max(camera.position.distanceTo(merged.boundingSphere.center) - merged.boundingSphere.radius, 0);
error = distance * 1e-3 * (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) * 0.5)); // ~1 pixel at 1k x 1k
}
geometry = simplifyMesh(geometry, settings.simplify.forceTriangleCount, error, sflags, settings.simplify.solve);
}
await showProgress('normals');
geometry = mergeVertices(
generateNormals(geometry, THREE.MathUtils.degToRad(settings.debug ? 30 : settings.normalCrease), settings.normalSmoothing)
);
var material = remeshMaterial.clone();
material.map = material.normalMap = material.metalnessMap = material.roughnessMap = null;
material.vertexColors = false;
material.color.set(0xffffff);
material.metalness = 0;
material.roughness = 1;
if (settings.paint.mode != 'none' && !paintCache) {
await showProgress('paint cache');
paintCache = repaint.buildCache(merged, merged.materials);
}
if (settings.paint.mode == 'vertex') {
await showProgress('repaint');
repaint.transferColors(renderer, geometry, paintCache, merged.thickness);
material.vertexColors = true;
} else if (settings.paint.mode == 'facets') {
await showProgress('repaint');
geometry = geometry.toNonIndexed();
geometry.computeVertexNormals();
var indices = new Uint32Array(geometry.attributes.position.count);
for (var i = 0; i < indices.length; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
repaint.transferColors(renderer, geometry, paintCache, merged.thickness, true);
material.vertexColors = true;
} else if (settings.paint.mode == 'texture') {
await showProgress('unwrap');
var maxIterations = settings.paint.atlasMode == 'quality' ? 1 : 0;
var maxChartSize = settings.paint.atlasMode == 'fast' ? 256 : settings.paint.atlasMode == 'balanced' ? 512 : 1024;
var preserveSeams = settings.paint.preserveNormalSeams;
geometry = unwrapMesh(geometry, settings.paint.resolution, settings.paint.gutter, preserveSeams, maxIterations, maxChartSize);
if (settings.paint.normals) {
await showProgress('tangents');
geometry = generateTangents(geometry);
}
geometry = mergeVertices(geometry);
await showProgress('repaint');
var textures = repaint.transferTexture(
renderer,
geometry,
paintCache,
merged.thickness,
settings.paint.resolution,
settings.paint.gutter,
settings.paint.infill,
settings.paint.normals,
settings.paint.materials
);
material.map = textures.map;
material.normalMap = textures.normalMap;
if (settings.paint.materials) {
material.metalnessMap = textures.materialMap;
material.roughnessMap = textures.materialMap;
material.metalness = 1;
} else if (settings.paint.normals) {
// we need this to have a specular response
material.roughness = 0.5;
}
} else {
material.color.set(0xdddddd);
}
material.needsUpdate = true;
// remesh material + wireframe overlay material
geometry.addGroup(0, geometry.index.count, 0);
geometry.addGroup(0, geometry.index.count, 1);
clearRemeshed();
remeshMaterial.dispose();
remeshMaterial = material;
remeshed = new THREE.Mesh(geometry, [remeshMaterial, wireframeMaterial]);
model.visible = false;
scene.add(remeshed);
showTexture('texture-view', remeshMaterial.map);
showTexture('normal-view', remeshMaterial.normalMap);
showTexture('material-view', remeshMaterial.metalnessMap);
update();
var triangles = geometry.index.count / 3;
stats.triangles = triangles;
stats.vertices = geometry.attributes.position.count;
stats.ratio = (triangles / (merged.index.count / 3)) * 100;
updateStatsDisplay();
progressLabel.hidden = true;
}
function exportGlb(name) {
// can't export remeshed directly as it uses debug materials for wireframe
var mesh = new THREE.Mesh(remeshed.geometry, remeshMaterial);
new GLTFExporter().parse(
mesh,
function (glb) {
var link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([glb], { type: 'model/gltf-binary' }));
link.download = name;
link.click();
// may resolve rare race conditions
setTimeout(function () {
URL.revokeObjectURL(link.href);
}, 0);
},
function (e) {
console.log(e);
},
{ binary: true }
);
}
function revert() {
if (!model) return;
model.visible = true;
clearRemeshed();
stats.triangles = 0;
stats.vertices = 0;
stats.ratio = 100;
model.traverse(function (object) {
if (object.isMesh) {
stats.triangles += (object.geometry.index ? object.geometry.index.count : object.geometry.attributes.position.count) / 3;
stats.vertices += object.geometry.attributes.position.count;
}
});
updateStatsDisplay();
}
function updateStatsDisplay() {
stats.label.innerHTML =
'Triangles: ' + stats.triangles + '<br>' + 'Vertices: ' + stats.vertices + '<br>' + 'Ratio: ' + stats.ratio.toFixed(1) + '%';
}
function reload() {
var simp = import('/js/meshopt_simplifier.js?x=' + Date.now());
MeshoptSimplifier.ready = simp.then(function (s) {
return s.MeshoptSimplifier.ready.then(function () {
for (var prop in s.MeshoptSimplifier) {
MeshoptSimplifier[prop] = s.MeshoptSimplifier[prop];
}
});
});
}
var moduleLastModified = 0;
function autoReload() {
if (!settings.autoUpdate) return;
fetch('/js/meshopt_simplifier.js?x=' + Date.now(), { method: 'HEAD' })
.then(function (r) {
var last = r.headers.get('Last-Modified');
if (last != moduleLastModified) {
moduleLastModified = last;
reload();
remesh();
}
settings.autoUpdateStatus = new Date(last).toLocaleTimeString();
setTimeout(autoReload, 1000);
})
.catch(function (e) {
settings.autoUpdateStatus = 'error';
setTimeout(autoReload, 5000);
});
}
function update() {
remeshMaterial.polygonOffset = settings.wireframeOverlay;
wireframeMaterial.visible = settings.wireframeOverlay;
scene.traverse(function (child) {
if (child.isMesh) {
if (Array.isArray(child.material)) {
child.material[0].wireframe = settings.wireframe;
} else {
child.material.wireframe = settings.wireframe;
}
}
});
}
function load(path, ext) {
if (model) scene.remove(model);
clearRemeshed();
if (paintCache) paintCache.dispose();
paintCache = null;
model = undefined;
merged = undefined;
var onProgress = function (xhr) {};
var onError = function (e) {
console.log(e);
};
function center(model) {
var bbox = new THREE.Box3().setFromObject(model);
var scale = 2 / Math.max(bbox.max.x - bbox.min.x, bbox.max.y - bbox.min.y, bbox.max.z - bbox.min.z);
var offset = new THREE.Vector3().addVectors(bbox.max, bbox.min).multiplyScalar(scale / 2);
model.scale.set(scale, scale, scale);
model.position.set(-offset.x, -offset.y, -offset.z);
}
function finish(object) {
model = object;
center(model);
scene.add(model);
merged = mergeMesh(model);
revert();
controls.target.set(0, 0, 0);
}
if (ext == 'gltf' || ext == 'glb') {
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
path,
function (gltf) {
finish(gltf.scene);
},
onProgress,
onError
);
} else if (ext == 'obj') {
var loader = new OBJLoader();
loader.load(
path,
function (obj) {
finish(obj);
},
onProgress,
onError
);
} else {
console.error('Unsupported file format');
}
}
function loadDefault() {
createView('pirate.glb');
load('pirate.glb', 'glb');
}
function init() {
container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 1.0;
camera.position.z = 3.0;
window.addEventListener('resize', updateRendererSizes, false);
}
function updateRendererSizes() {
var rendererContainer = renderer.domElement.parentElement;
var aspectRatio = rendererContainer.clientWidth / rendererContainer.clientHeight;
camera.aspect = aspectRatio;
camera.updateProjectionMatrix();
renderer.setSize(rendererContainer.clientWidth, rendererContainer.clientHeight);
}
function render() {
requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>