<!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;
}
#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;
}
#grid-container {
width: 100%;
flex: 1;
display: grid;
grid-template-columns: repeat(var(--grid-columns, 1), 1fr);
grid-auto-rows: minmax(300px, 1fr);
overflow-y: auto;
}
.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: auto;
font-size: 14px;
transition: background-color 0.2s;
}
.renderer-label:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.renderer-label.focused {
background-color: rgba(255, 100, 100, 0.7);
color: white;
}
.renderer-label.clickable {
cursor: pointer;
}
.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;
}
.renderer-container.hidden {
display: none;
}
#grid-container::-webkit-scrollbar {
width: 16px;
}
#grid-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
#grid-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
}
#grid-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@2.0.10/dist/es-module-shims.min.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.174.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js"
}
}
</script>
</head>
<body>
<div id="grid-container"></div>
<input type="file" id="fileInput" style="display: none" accept=".obj,.glb" multiple />
<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 { OrbitControls } from 'three-examples/controls/OrbitControls.js';
import { mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.module.js';
import { MeshoptSimplifier } from '../js/meshopt_simplifier.module.js';
import { GUI } from 'lil-gui';
var container, gridContainer;
var camera, clock;
var renderers = [];
var scenes = [];
var controls = [];
var models = [];
var mixers = [];
var viewStats = [];
var focusedViewIndex = -1;
var lastCameraDistance = 0;
var settings = {
wireframe: false,
wireframeOverlay: false,
animate: false,
pointSize: 1.0,
ratio: 1.0,
debugOverlay: false,
sloppy: false,
permissive: false,
lockBorder: false,
preprune: 0.0,
prune: true,
regularize: false,
solve: false,
useAttributes: true,
errorThresholdLog10: 1,
errorScaled: true,
normalWeight: 1.0,
colorWeight: 1.0,
textureWeight: 0.0,
autoLod: false,
autoLodFactor: 1.0,
gridColumns: 2,
loadFile: function () {
var input = document.getElementById('fileInput');
input.onchange = function () {
var files = Array.from(input.files).sort(function (a, b) {
var an = a.name.split('.')[0];
var bn = b.name.split('.')[0];
return an.localeCompare(bn);
});
loadFiles(files);
};
input.click();
},
updateModule: function () {
reload();
simplify();
},
autoUpdate: false,
autoUpdateStatus: '',
};
var gui = new GUI({ width: 300 });
var guiDisplay = gui.addFolder('Display');
guiDisplay.add(settings, 'wireframe').onChange(update);
guiDisplay.add(settings, 'wireframeOverlay').onChange(simplify); guiDisplay.add(settings, 'animate').onChange(update);
guiDisplay.add(settings, 'pointSize', 1, 16).onChange(update);
guiDisplay.add(settings, 'debugOverlay').onChange(simplify); guiDisplay.add(settings, 'gridColumns', 1, 6, 1).onChange(updateGridLayout);
var guiSimplify = gui.addFolder('Simplify');
guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify);
guiSimplify.add(settings, 'sloppy').onChange(simplify);
guiSimplify.add(settings, 'permissive').onChange(simplify);
guiSimplify.add(settings, 'lockBorder').onChange(simplify);
guiSimplify.add(settings, 'prune').onChange(simplify);
guiSimplify.add(settings, 'regularize').onChange(simplify);
guiSimplify.add(settings, 'solve').onChange(simplify);
guiSimplify.add(settings, 'preprune', 0, 0.2, 0.01).onChange(simplify);
guiSimplify.add(settings, 'useAttributes').onChange(simplify).onFinishChange(updateSettings);
var guiAttributes = [];
guiAttributes.push(guiSimplify.add(settings, 'normalWeight', 0, 2, 0.01).onChange(simplify));
guiAttributes.push(guiSimplify.add(settings, 'colorWeight', 0, 2, 0.01).onChange(simplify));
guiAttributes.push(guiSimplify.add(settings, 'textureWeight', 0, 100, 0.1).onChange(simplify));
guiSimplify.add(settings, 'errorScaled').onChange(simplify);
guiSimplify.add(settings, 'errorThresholdLog10', 0, 3, 0.1).onChange(simplify);
var guiLod = gui.addFolder('LOD');
guiLod.add(settings, 'autoLod').onChange(simplify);
guiLod.add(settings, 'autoLodFactor', 0, 10, 0.01).onChange(simplify);
var guiLoad = gui.addFolder('Load');
guiLoad.add(settings, 'loadFile');
guiLoad.add(settings, 'updateModule');
guiLoad.add(settings, 'autoUpdate').onChange(autoReload);
guiLoad.add(settings, 'autoUpdateStatus').listen();
updateSettings();
init();
loadDefault();
animate();
function loadFiles(files) {
clearViews();
if (files.length > 0) {
const columns = Math.min(files.length, settings.gridColumns);
document.documentElement.style.setProperty('--grid-columns', columns);
for (var i = 0; i < files.length; i++) {
var file = files[i];
var uri = URL.createObjectURL(file);
var ext = file.name.split('.').pop().toLowerCase();
createView(i, file.name);
loadIntoView(i, uri, ext);
}
}
}
function updateGridLayout() {
if (renderers.length > 0) {
var visibleViews = focusedViewIndex >= 0 ? 1 : renderers.length;
var columns = Math.min(visibleViews, settings.gridColumns);
document.documentElement.style.setProperty('--grid-columns', columns);
if (columns > 1) {
var cellWidth = Math.floor(window.innerWidth / columns);
document.documentElement.style.setProperty('--grid-cell-height', cellWidth + 'px');
} else {
document.documentElement.style.setProperty('--grid-cell-height', '100vh');
}
updateRendererSizes();
}
}
function updateAutoLod() {
if (settings.autoLod) {
var center = new THREE.Vector3();
var currentDistance = camera.position.distanceTo(center);
if (Math.abs(currentDistance - lastCameraDistance) > 1e-3) {
lastCameraDistance = currentDistance;
simplify();
}
}
}
function updateSettings() {
for (var i = 0; i < guiAttributes.length; ++i) {
guiAttributes[i].enable(settings.useAttributes);
}
}
function clearViews() {
for (var i = 0; i < renderers.length; i++) {
var container = renderers[i].domElement.parentElement;
if (container) {
container.parentElement.removeChild(container);
}
}
renderers = [];
scenes = [];
controls = [];
models = [];
mixers = [];
viewStats = [];
focusedViewIndex = -1;
gridContainer.innerHTML = '';
}
function updateLabelsClickability() {
var isClickable = renderers.length > 1;
for (var i = 0; i < renderers.length; i++) {
var container = renderers[i].domElement.parentElement;
var label = container.querySelector('.renderer-label');
if (isClickable) {
label.classList.add('clickable');
label.title = 'Click to focus on this view';
} else {
label.classList.remove('clickable');
label.title = '';
}
}
}
function toggleFocus(viewIndex) {
if (focusedViewIndex === viewIndex) {
focusedViewIndex = -1;
} else {
focusedViewIndex = viewIndex;
}
updateFocusDisplay();
simplify(); }
function updateFocusDisplay() {
for (var i = 0; i < renderers.length; i++) {
var container = renderers[i].domElement.parentElement;
var label = container.querySelector('.renderer-label');
if (focusedViewIndex === -1) {
container.classList.remove('hidden');
label.classList.remove('focused');
if (renderers.length > 1) {
label.title = 'Click to focus on this view';
}
} else if (focusedViewIndex === i) {
container.classList.remove('hidden');
label.classList.add('focused');
label.title = 'Click to exit focus mode';
} else {
container.classList.add('hidden');
label.classList.remove('focused');
}
}
updateGridLayout();
}
function createView(index, name) {
var rendererContainer = document.createElement('div');
rendererContainer.className = 'renderer-container';
gridContainer.appendChild(rendererContainer);
var label = document.createElement('div');
label.className = 'renderer-label';
label.textContent = name;
label.onclick = function () {
if (renderers.length > 1) {
toggleFocus(index);
}
};
rendererContainer.appendChild(label);
var statsLabel = document.createElement('div');
statsLabel.className = 'stats-label';
rendererContainer.appendChild(statsLabel);
viewStats.push({
triangles: 0,
vertices: 0,
error: 0,
ratio: 0,
label: statsLabel,
});
var renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
rendererContainer.appendChild(renderer.domElement);
renderers.push(renderer);
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
scenes.push(scene);
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);
var control = new OrbitControls(camera, renderer.domElement);
control.addEventListener('change', updateAutoLod);
controls.push(control);
models.push(null);
mixers.push(null);
updateRendererSizes();
updateLabelsClickability();
}
function simplifyMesh(geo, threshold, scale) {
var attributes = 8;
var positions = new Float32Array(geo.attributes.position.array).slice(); var indices = new Uint32Array(geo.index.array).slice(); var target = settings.autoLod ? 0 : Math.floor((indices.length * settings.ratio) / 3) * 3;
if (settings.useAttributes) {
var attrib = new Float32Array(geo.attributes.position.count * attributes);
for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
if (geo.attributes.normal) {
attrib[i * attributes + 0] = geo.attributes.normal.getX(i);
attrib[i * attributes + 1] = geo.attributes.normal.getY(i);
attrib[i * attributes + 2] = geo.attributes.normal.getZ(i);
}
if (geo.attributes.color) {
attrib[i * attributes + 3] = geo.attributes.color.getX(i);
attrib[i * attributes + 4] = geo.attributes.color.getY(i);
attrib[i * attributes + 5] = geo.attributes.color.getZ(i);
}
if (geo.attributes.uv) {
attrib[i * attributes + 6] = geo.attributes.uv.getX(i);
attrib[i * attributes + 7] = geo.attributes.uv.getY(i);
}
}
var attrib_weights = [
settings.normalWeight,
settings.normalWeight,
settings.normalWeight,
settings.colorWeight,
settings.colorWeight,
settings.colorWeight,
settings.textureWeight,
settings.textureWeight,
];
} else {
var attrib = new Float32Array();
var attrib_weights = [];
attributes = 0;
}
var flags = [];
if (settings.lockBorder) {
flags.push('LockBorder');
}
if (settings.prune) {
flags.push('Prune');
}
if (settings.regularize) {
flags.push('Regularize');
}
if (settings.permissive) {
flags.push('Permissive');
}
if (settings.debugOverlay) {
flags.push('_InternalDebug');
}
var locks = new Uint8Array(geo.attributes.position.count);
if (settings.permissive) {
var pos_cache = {};
var pos_remap = [];
var unique = 0;
for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
var px = geo.attributes.position.getX(i);
var py = geo.attributes.position.getY(i);
var pz = geo.attributes.position.getZ(i);
var pk = px.toString() + ' ' + py.toString() + ' ' + pz.toString();
if (pos_cache[pk] === undefined) {
pos_cache[pk] = i;
pos_remap[i] = i;
unique++;
} else {
pos_remap[i] = pos_cache[pk];
}
}
if (geo.attributes.normal) {
for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
if (pos_remap[i] != i) {
var rx = geo.attributes.normal.getX(pos_remap[i]),
ry = geo.attributes.normal.getY(pos_remap[i]),
rz = geo.attributes.normal.getZ(pos_remap[i]);
var nx = geo.attributes.normal.getX(i),
ny = geo.attributes.normal.getY(i),
nz = geo.attributes.normal.getZ(i);
if (rx * nx + ry * ny + rz * nz < 0.25) {
locks[i] |= 2;
}
}
}
}
if (geo.attributes.uv) {
for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
if (pos_remap[i] != i) {
var ru = geo.attributes.uv.getX(pos_remap[i]),
rv = geo.attributes.uv.getY(pos_remap[i]);
var u = geo.attributes.uv.getX(i),
v = geo.attributes.uv.getY(i);
if (u != ru || v != rv) {
locks[i] |= 2;
}
}
}
}
}
var stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3;
if (settings.preprune > 0) {
indices = MeshoptSimplifier.simplifyPrune(indices, positions, stride, settings.preprune / scale);
target = Math.min(target, indices.length);
}
var S = MeshoptSimplifier; var res = settings.sloppy
? S.simplifySloppy(indices, positions, stride, null, target, threshold)
: settings.solve
? S.simplifyWithUpdate(indices, positions, stride, attrib, attributes, attrib_weights, locks, target, threshold, flags)
: S.simplifyWithAttributes(indices, positions, stride, attrib, attributes, attrib_weights, locks, target, threshold, flags);
if (!settings.sloppy && settings.solve) {
res[0] = indices.slice(0, res[0]);
}
var rgeo = geo.clone();
var dind = res[0];
var dlen = dind.length;
if (settings.debugOverlay) {
dind = Array.from(dind);
var mask = (1 << 28) - 1;
for (var kind = 1; kind <= 5; ++kind) {
var offset = dind.length;
for (var i = 0; i < dlen; i += 3) {
for (var e = 0; e < 3; ++e) {
var a = dind[i + e],
b = dind[i + ((e + 1) % 3)];
var ak = (a >> 28) & 7,
bk = (b >> 28) & 7;
if ((a >> 31 != 0 || kind == 3) && (ak == kind || bk == kind) && (ak == kind || ak == 4) && (bk == kind || bk == 4)) {
dind.push(a & mask);
dind.push(a & mask);
dind.push(b & mask);
} else if (a >> 31 != 0 && kind == 5 && ak != bk && ak != 4 && bk != 4) {
dind.push(a & mask);
dind.push(a & mask);
dind.push(b & mask);
} else if (kind == 4 && ak == kind && bk == kind) {
dind.push(a & mask);
dind.push(a & mask);
dind.push(b & mask);
}
}
}
if (offset != dind.length) {
rgeo.addGroup(offset, dind.length - offset, kind + 1); offset = dind.length;
}
}
for (var i = 0; i < dlen; i++) {
dind[i] &= mask;
}
if (settings.wireframeOverlay) {
rgeo.addGroup(0, dlen, 1); }
} else if (settings.wireframeOverlay) {
rgeo.addGroup(0, dind.length, 1); }
rgeo.index.array = new Uint32Array(dind);
rgeo.index.count = dind.length;
rgeo.index.needsUpdate = true;
if (settings.solve) {
rgeo.attributes.position = new THREE.BufferAttribute(positions, stride);
if (settings.useAttributes) {
for (var i = 0, e = geo.attributes.position.count; i < e; ++i) {
var nx = attrib[i * attributes + 0];
var ny = attrib[i * attributes + 1];
var nz = attrib[i * attributes + 2];
var nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
if (nl > 0) {
attrib[i * attributes + 0] = nx / nl;
attrib[i * attributes + 1] = ny / nl;
attrib[i * attributes + 2] = nz / nl;
}
}
var attribbuf = new THREE.InterleavedBuffer(attrib, attrib_weights.length);
if (geo.attributes.normal) {
rgeo.attributes.normal = new THREE.InterleavedBufferAttribute(attribbuf, 3, 0, false);
}
if (geo.attributes.color) {
rgeo.attributes.color = new THREE.InterleavedBufferAttribute(attribbuf, 3, 3, false);
}
if (geo.attributes.uv) {
rgeo.attributes.uv = new THREE.InterleavedBufferAttribute(attribbuf, 2, 6, false);
}
}
}
rgeo.addGroup(0, dlen, 0);
return [rgeo, dlen / 3, res[1]];
}
function simplifyPoints(geo) {
var positions = new Float32Array(geo.attributes.position.array);
var colors = undefined;
var target = Math.floor(geo.attributes.position.count * settings.ratio);
if (settings.useAttributes && geo.attributes.color) {
colors = new Float32Array(geo.attributes.color.array);
}
var pos_stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3;
var col_stride =
geo.attributes.color instanceof THREE.InterleavedBufferAttribute
? geo.attributes.color.data.stride
: geo.attributes.color.itemSize;
var colorScale = geo.attributes.color && geo.attributes.color.normalized ? 255 : 1;
console.time('simplify');
var res = MeshoptSimplifier.simplifyPoints(positions, pos_stride, target, colors, col_stride, settings.colorWeight / colorScale);
console.timeEnd('simplify');
console.log('simplified to', res.length);
geo.index = new THREE.BufferAttribute(res, 1);
}
function getRadius(obj) {
var box = new THREE.Box3().setFromObject(obj);
return box.max.sub(box.min).length() / 2;
}
function simplify() {
MeshoptSimplifier.ready.then(function () {
var threshold = Math.pow(10, -settings.errorThresholdLog10);
if (settings.autoLod) {
var center = new THREE.Vector3();
var radius = 1;
var distance = Math.max(camera.position.distanceTo(center) - radius, 0);
var loderrortarget = 1e-3 * (2 * Math.tan(camera.fov * 0.5 * (Math.PI / 180)));
threshold = distance * loderrortarget * settings.autoLodFactor;
}
for (var sceneIndex = 0; sceneIndex < scenes.length; sceneIndex++) {
var scene = scenes[sceneIndex];
if (focusedViewIndex >= 0 && sceneIndex != focusedViewIndex) {
continue;
}
var stats = viewStats[sceneIndex];
stats.triangles = 0;
stats.vertices = 0;
stats.error = 0;
stats.ratio = 0;
var rnum = 0;
var rden = 0;
var extent = getRadius(scene);
scene.traverse(function (object) {
if (object.isMesh && object.geometry.index) {
if (!object.original) {
object.original = object.geometry.clone();
object.material.polygonOffset = true;
object.material.polygonOffsetFactor = 0.5;
object.material.polygonOffsetUnits = 16;
object.material = [
object.material,
new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }), new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }), new THREE.MeshBasicMaterial({ color: 0x009f9f, wireframe: true }), new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }), new THREE.MeshBasicMaterial({ color: 0xff9f00, wireframe: true }), ];
}
var scale = settings.errorScaled ? getRadius(object) / extent : 1.0;
var [geo, tri, err] = simplifyMesh(object.original, threshold / scale, scale);
object.geometry = geo;
stats.error = Math.max(stats.error, err * scale);
stats.triangles += tri;
stats.vertices += object.geometry.attributes.position.count;
rnum += tri;
rden += object.original.index.count / 3;
}
if (object.isPoints) {
simplifyPoints(object.geometry);
stats.vertices += object.geometry.index.count;
rnum += object.geometry.index.count;
rden += object.geometry.attributes.position.count;
}
});
stats.ratio = rden > 0 ? (rnum / rden) * 100 : 0;
updateStatsDisplay(sceneIndex);
}
});
}
function updateStatsDisplay(viewIndex) {
if (viewIndex >= 0 && viewIndex < viewStats.length) {
var stats = viewStats[viewIndex];
stats.label.innerHTML =
'Triangles: ' +
stats.triangles +
'<br>' +
'Vertices: ' +
stats.vertices +
'<br>' +
'Error: ' +
stats.error.toExponential(3) +
'<br>' +
'Ratio: ' +
stats.ratio.toFixed(1) +
'%';
}
}
function reload() {
var simp = import('/js/meshopt_simplifier.module.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.module.js?x=' + Date.now(), { method: 'HEAD' })
.then(function (r) {
var last = r.headers.get('Last-Modified');
if (last != moduleLastModified) {
moduleLastModified = last;
reload();
simplify();
}
settings.autoUpdateStatus = new Date(last).toLocaleTimeString();
setTimeout(autoReload, 1000);
})
.catch(function (e) {
settings.autoUpdateStatus = 'error';
setTimeout(autoReload, 5000);
});
}
function update() {
for (var sceneIndex = 0; sceneIndex < scenes.length; sceneIndex++) {
var scene = scenes[sceneIndex];
scene.traverse(function (child) {
if (child.isMesh) {
if (Array.isArray(child.material)) {
child.material[0].wireframe = settings.wireframe;
} else {
child.material.wireframe = settings.wireframe;
}
}
if (child.isPoints) {
child.material.size = settings.pointSize;
}
});
}
}
function loadIntoView(viewIndex, path, ext) {
if (models[viewIndex]) {
scenes[viewIndex].remove(models[viewIndex]);
models[viewIndex] = undefined;
mixers[viewIndex] = 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);
}
if (ext == 'gltf' || ext == 'glb') {
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
path,
function (gltf) {
models[viewIndex] = gltf.scene;
center(models[viewIndex]);
scenes[viewIndex].add(models[viewIndex]);
mixers[viewIndex] = new THREE.AnimationMixer(models[viewIndex]);
if (gltf.animations.length) {
mixers[viewIndex].clipAction(gltf.animations[gltf.animations.length - 1]).play();
}
simplify();
},
onProgress,
onError
);
} else if (ext == 'obj') {
var loader = new OBJLoader();
loader.load(
path,
function (obj) {
obj.traverse(function (node) {
if (node.isMesh) {
node.geometry = mergeVertices(node.geometry);
if (Array.isArray(node.material)) {
node.material = node.material[0];
node.geometry.clearGroups();
}
}
});
models[viewIndex] = obj;
center(models[viewIndex]);
scenes[viewIndex].add(models[viewIndex]);
simplify();
},
onProgress,
onError
);
} else {
console.error('Unsupported file format');
}
}
function loadDefault() {
document.documentElement.style.setProperty('--grid-columns', 1);
createView(0, 'pirate.glb');
loadIntoView(0, 'pirate.glb', 'glb');
}
function init() {
container = document.createElement('div');
document.body.appendChild(container);
gridContainer = document.getElementById('grid-container');
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 1.0;
camera.position.z = 3.0;
clock = new THREE.Clock();
window.addEventListener('resize', updateRendererSizes, false);
}
function updateRendererSizes() {
var container = renderers[focusedViewIndex >= 0 ? focusedViewIndex : 0].domElement.parentElement;
var aspectRatio = container.clientWidth / container.clientHeight;
camera.aspect = aspectRatio;
camera.updateProjectionMatrix();
for (var i = 0; i < renderers.length; i++) {
var domElement = renderers[i].domElement;
var container = domElement.parentElement;
var width = container.clientWidth;
var height = container.clientHeight;
renderers[i].setSize(width, height);
}
}
function animate() {
requestAnimationFrame(animate);
for (var i = 0; i < controls.length; i++) {
controls[i].update();
}
if (settings.animate) {
var delta = clock.getDelta();
for (var i = 0; i < mixers.length; i++) {
if (mixers[i]) {
mixers[i].update(delta);
}
}
}
for (var i = 0; i < renderers.length; i++) {
if (focusedViewIndex === -1 || focusedViewIndex === i) {
renderers[i].render(scenes[i], camera);
}
}
}
</script>
</body>
</html>