179 lines
5.2 KiB
HTML
179 lines
5.2 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>OBJ Viewer (three.js modules)</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<style>
|
|
html, body { margin: 0; height: 100%; overflow: hidden; background: #111; }
|
|
#hud {
|
|
position: fixed; left: 12px; top: 12px;
|
|
padding: 10px 12px; border-radius: 10px;
|
|
background: rgba(0,0,0,0.75); color: #fff;
|
|
font: 13px/1.4 system-ui, sans-serif;
|
|
max-width: 460px;
|
|
white-space: pre-wrap;
|
|
}
|
|
#hud b { display:block; margin-bottom: 6px; }
|
|
code { background: rgba(255,255,255,0.08); padding: 1px 4px; border-radius: 6px; }
|
|
</style>
|
|
|
|
<!-- Import map: map "three" and "three/addons/" to CDN module files -->
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="hud"><b>OBJ Viewer</b>Waiting...</div>
|
|
|
|
<script type="module">
|
|
import * as THREE from "three";
|
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
import { MTLLoader } from "three/addons/loaders/MTLLoader.js";
|
|
|
|
const OBJ_URL = "./output.obj";
|
|
const MTL_URL = "./output.mtl"; // optional
|
|
const hud = document.getElementById("hud");
|
|
|
|
function hudMsg(lines) { hud.textContent = lines.join("\n"); }
|
|
|
|
hudMsg(["OBJ Viewer", "Booting (modules)..."]);
|
|
|
|
// Scene
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x111111);
|
|
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 1e7);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
|
|
// Lights
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
|
const dir = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
dir.position.set(3, 5, 4);
|
|
scene.add(dir);
|
|
|
|
function frameObject(obj) {
|
|
const box = new THREE.Box3().setFromObject(obj);
|
|
const size = box.getSize(new THREE.Vector3());
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
|
|
obj.position.sub(center);
|
|
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
const fov = camera.fov * Math.PI / 180;
|
|
let dist = (maxDim / 2) / Math.tan(fov / 2);
|
|
dist *= 1.6;
|
|
|
|
camera.position.set(dist, dist * 0.6, dist);
|
|
camera.near = Math.max(maxDim / 1000, 0.01);
|
|
camera.far = maxDim * 1000;
|
|
camera.updateProjectionMatrix();
|
|
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
}
|
|
|
|
async function urlExists(url) {
|
|
try {
|
|
// Some servers block HEAD; fall back to GET if needed.
|
|
let r = await fetch(url, { method: "HEAD" });
|
|
if (!r.ok) r = await fetch(url, { method: "GET" });
|
|
return r.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function load() {
|
|
hudMsg(["OBJ Viewer", `Loading: ${OBJ_URL}`]);
|
|
|
|
const objLoader = new OBJLoader();
|
|
|
|
// Optional MTL
|
|
const hasMtl = await urlExists(MTL_URL);
|
|
if (hasMtl) {
|
|
hudMsg(["OBJ Viewer", `Loading MTL: ${MTL_URL}`]);
|
|
const mtlLoader = new MTLLoader();
|
|
const materials = await new Promise((resolve, reject) => {
|
|
mtlLoader.load(MTL_URL, resolve, undefined, reject);
|
|
});
|
|
materials.preload();
|
|
objLoader.setMaterials(materials);
|
|
}
|
|
|
|
hudMsg(["OBJ Viewer", `Loading OBJ: ${OBJ_URL}`]);
|
|
|
|
const obj = await new Promise((resolve, reject) => {
|
|
objLoader.load(
|
|
OBJ_URL,
|
|
resolve,
|
|
(xhr) => {
|
|
const mb = (xhr.loaded / (1024 * 1024)).toFixed(2);
|
|
hudMsg(["OBJ Viewer", `Loading OBJ: ${OBJ_URL}`, `${mb} MB loaded`]);
|
|
},
|
|
reject
|
|
);
|
|
});
|
|
|
|
// Ensure normals/material
|
|
obj.traverse((c) => {
|
|
if (c.isMesh) {
|
|
c.geometry.computeVertexNormals?.();
|
|
if (!c.material) c.material = new THREE.MeshStandardMaterial({ color: 0xb0b0b0 });
|
|
}
|
|
});
|
|
|
|
scene.add(obj);
|
|
frameObject(obj);
|
|
|
|
hudMsg([
|
|
"OBJ Viewer",
|
|
"Loaded OK",
|
|
hasMtl ? "MTL: used" : "MTL: not used",
|
|
"",
|
|
"Left drag: rotate",
|
|
"Wheel: zoom",
|
|
"Right drag: pan"
|
|
]);
|
|
}
|
|
|
|
load().catch((e) => {
|
|
console.error(e);
|
|
hudMsg([
|
|
"OBJ Viewer",
|
|
"ERROR",
|
|
String(e),
|
|
"",
|
|
"Open DevTools -> Console for details."
|
|
]);
|
|
});
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
|
|
window.addEventListener("resize", () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|