108 lines
3.8 KiB
JavaScript
108 lines
3.8 KiB
JavaScript
import { useEffect, useRef } from "react";
|
|
import * as THREE from "three";
|
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
|
|
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
|
|
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
|
|
export default function Viewer({ url, ext }) {
|
|
const mountRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const mount = mountRef.current;
|
|
if (!mount || !url) return;
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x101014);
|
|
const camera = new THREE.PerspectiveCamera(50, 1, 0.01, 1000);
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
mount.appendChild(renderer.domElement);
|
|
|
|
const resize = () => {
|
|
const w = mount.clientWidth, h = mount.clientHeight;
|
|
renderer.setSize(w, h);
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
};
|
|
resize();
|
|
const ro = new ResizeObserver(resize);
|
|
ro.observe(mount);
|
|
|
|
scene.add(new THREE.HemisphereLight(0xffffff, 0x334, 1.2));
|
|
const dir = new THREE.DirectionalLight(0xffffff, 2.0);
|
|
dir.position.set(3, 5, 4);
|
|
scene.add(dir);
|
|
const grid = new THREE.GridHelper(10, 20, 0x333344, 0x22222c);
|
|
scene.add(grid);
|
|
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
|
|
let mixer = null;
|
|
const clock = new THREE.Clock();
|
|
|
|
const frame = (obj) => {
|
|
const box = new THREE.Box3().setFromObject(obj);
|
|
const size = box.getSize(new THREE.Vector3()).length() || 1;
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
obj.position.sub(center);
|
|
obj.position.y += (box.getSize(new THREE.Vector3()).y / 2);
|
|
camera.position.set(size * 0.7, size * 0.5, size * 0.9);
|
|
controls.target.set(0, box.getSize(new THREE.Vector3()).y / 4, 0);
|
|
camera.near = size / 100;
|
|
camera.far = size * 20;
|
|
camera.updateProjectionMatrix();
|
|
};
|
|
|
|
const addDefaultMaterial = (geometry) => {
|
|
const hasColor = !!geometry.attributes.color;
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color: hasColor ? 0xffffff : 0x8899bb,
|
|
vertexColors: hasColor,
|
|
roughness: 0.7,
|
|
});
|
|
const mesh = new THREE.Mesh(geometry, mat);
|
|
scene.add(mesh);
|
|
frame(mesh);
|
|
};
|
|
|
|
const onLoad = (obj) => {
|
|
scene.add(obj.scene || obj);
|
|
const target = obj.scene || obj;
|
|
if (obj.animations?.length) {
|
|
mixer = new THREE.AnimationMixer(target);
|
|
mixer.clipAction(obj.animations[0]).play();
|
|
}
|
|
frame(target);
|
|
};
|
|
const err = (e) => console.error("viewer load error", e);
|
|
|
|
if (ext === "glb" || ext === "gltf") new GLTFLoader().load(url, onLoad, undefined, err);
|
|
else if (ext === "obj") new OBJLoader().load(url, onLoad, undefined, err);
|
|
else if (ext === "fbx") new FBXLoader().load(url, onLoad, undefined, err);
|
|
else if (ext === "ply") new PLYLoader().load(url, (g) => { g.computeVertexNormals(); addDefaultMaterial(g); }, undefined, err);
|
|
else if (ext === "stl") new STLLoader().load(url, (g) => { g.computeVertexNormals(); addDefaultMaterial(g); }, undefined, err);
|
|
|
|
let raf;
|
|
const loop = () => {
|
|
raf = requestAnimationFrame(loop);
|
|
if (mixer) mixer.update(clock.getDelta());
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
};
|
|
loop();
|
|
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
ro.disconnect();
|
|
renderer.dispose();
|
|
mount.removeChild(renderer.domElement);
|
|
};
|
|
}, [url, ext]);
|
|
|
|
return <div className="viewer" ref={mountRef} />;
|
|
}
|