avatar

sunday

Sunday's Blog

  • 首页
Home nextjs使用three写一个3D模型的查看器Viewer
文章

nextjs使用three写一个3D模型的查看器Viewer

Posted recently Updated recently
By sunday
13~17 min read

直接上代码

// 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"/>

nextjs
nextjs
License:  CC BY 4.0
Share

Further Reading

Jul 1, 2025

nextjs使用three写一个3D模型的查看器Viewer

直接上代码 // components/GLBViewer.js "use client"; import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { OrbitCon

May 3, 2025

nextjs15使用better-sqlite3的连接报错问题

1.出现如下错误 ⨯ Error: Could not locate the bindings file. Tried: 解决方法 我是使用pnpm包管理器,执行以下操作 首先安装node-gyp pnpm i node-gyp -D 然后执行 pnpm approve-builds 执行后会

Apr 6, 2025

nextjs + clerk + supabase + realtime 实时监听数据库更改

1.开启realtime.messages策略 在supabase的SQL Editor执行以下命令 create policy "Enable all access for authenticated users" on "public"."messages" as PERMISSIVE for

OLDER

nextjs15使用better-sqlite3的连接报错问题

NEWER

Recently Updated

  • nextjs使用three写一个3D模型的查看器Viewer
  • nextjs15使用better-sqlite3的连接报错问题
  • nextjs + clerk + supabase + realtime 实时监听数据库更改
  • 解决nextjs15使用useLocalStorage报错的问题
  • mac上使用nodejs appium控制chrome浏览器

Trending Tags

nginx acme 强制跳转HTTPS nodejs 代理 mac 神器 vue3 工具 docker

Contents

©2025 sunday. Some rights reserved.

Using the Halo theme Chirpy