nextjs使用three写一个3D模型的查看器Viewer
直接上代码
// components/GLBViewer.js
"use client";
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 添加类型定义
interface GLBViewerProps {
modelPath: string;
}
export default function GLBViewer({ modelPath }: GLBViewerProps) {
const containerRef = useRef<HTMLDivElement>(null)
const mountRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
// 添加renderer引用以便在组件中访问
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
useEffect(() => {
// Add early return if modelPath is undefined or invalid
if (!mountRef.current) return;
if (!modelPath) {
setError("Model path is undefined or invalid");
setLoading(false);
return;
}
setLoading(true);
setError("");
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance",
preserveDrawingBuffer: true, // 添加这个属性以支持截图
});
// 保存renderer引用
rendererRef.current = renderer;
renderer.setSize(
mountRef.current.clientWidth,
mountRef.current.clientHeight
);
renderer.setClearColor(0x333333);
renderer.outputColorSpace = THREE.SRGBColorSpace; // 新版本Three.js使用outputColorSpace代替outputEncoding
mountRef.current.innerHTML = "";
mountRef.current.appendChild(renderer.domElement);
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
// 设置相机
const camera = new THREE.PerspectiveCamera(
75,
mountRef.current.clientWidth / mountRef.current.clientHeight,
0.1,
1000
);
camera.position.z = 5;
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 增强环境光强度
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2); // 增强定向光强度
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加额外的光源以改善模型照明
const backLight = new THREE.DirectionalLight(0xffffff, 1);
backLight.position.set(-1, 0.5, -1);
scene.add(backLight);
// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.autoRotate = false; // 移除自动旋转效果
controls.autoRotateSpeed = 1;
// 加载GLB模型
const loader = new GLTFLoader();
loader.load(
modelPath,
(gltf) => {
// 计算模型边界框以便自动调整相机位置
const box = new THREE.Box3().setFromObject(gltf.scene);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// 将模型居中
gltf.scene.position.x = -center.x;
gltf.scene.position.y = -center.y;
gltf.scene.position.z = -center.z;
// 调整相机位置
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
const cameraZ = Math.abs(maxDim / Math.sin(fov / 2));
camera.position.z = cameraZ * 0.6;
// 更新控制器
const minDistance = maxDim / 1.5;
const maxDistance = maxDim * 1.5;
controls.minDistance = minDistance;
controls.maxDistance = maxDistance;
controls.update();
scene.add(gltf.scene);
setLoading(false);
},
// 加载进度回调
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
// 错误回调
(error) => {
console.error("加载模型时出错:", error);
setError("加载模型时出错,请检查模型路径是否正确。");
setLoading(false);
}
);
// 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
// 处理窗口大小调整
const handleResize = () => {
if (!mountRef.current) return;
camera.aspect =
mountRef.current.clientWidth / mountRef.current.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(
mountRef.current.clientWidth,
mountRef.current.clientHeight
);
};
window.addEventListener("resize", handleResize);
// 添加双击事件以重置视图
const handleDoubleClick = () => {
const box = new THREE.Box3().setFromObject(scene);
const size = box.getSize(new THREE.Vector3());
// const center = box.getCenter(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
const cameraZ = Math.abs(maxDim / Math.sin(fov / 2));
// 重置相机位置
controls.reset();
camera.position.z = cameraZ * 1.2;
controls.update();
};
renderer.domElement.addEventListener("dblclick", handleDoubleClick);
// 清理函数
return () => {
window.removeEventListener("resize", handleResize);
renderer.domElement.removeEventListener("dblclick", handleDoubleClick);
if (mountRef.current && renderer.domElement) {
mountRef.current.removeChild(renderer.domElement);
}
scene.clear();
renderer.dispose();
};
}, [modelPath]);
return (
<>
<div ref={containerRef} id="feet-canvas" className="relative w-full h-[300px]">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 z-10">
<div className="text-white text-xl">加载中...</div>
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-red-800 bg-opacity-50 z-10">
<div className="text-white text-xl">{error}</div>
</div>
)}
<div
ref={mountRef}
className="w-full h-full shadow-lg overflow-hidden"
/>
</div>
</>
);
}
使用方法
<GLBViewer modelPath="/models/3d.glb"/>
License:
CC BY 4.0