react專案中使用threejs載入glb檔案

ZerlinM發表於2024-11-18

安裝threejs

yarn add three

首先建立 renderModel.js檔案

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module';

const modeLoader = {
    gltf: () => {
        return new GLTFLoader()
    },
    glb: () => {
        return new GLTFLoader()
    }
}

let canvasContainer = null
let camera = null
let scene = null
let renderer = null
let controls = null
let model = null

const initEvents = {
    raycaster: new THREE.Raycaster(),
    pickedObject: null,
    pickedObjectSavedColor: 0,
    pickPosition: new THREE.Vector2(), //建立二維平面
}

export const renderInit = (container) => {
    return new Promise((resolve, reject) => {
        canvasContainer = container.current
        clearModel()
        init()
        setLight()

        resolve()
    })
}

const init = () => {
    camera = new THREE.PerspectiveCamera(
        45,
        canvasContainer.clientWidth / canvasContainer.clientHeight,
        0.1,
        1000
    )
    camera.position.set(0, 0, 150)
    camera.up.x = 0;
    camera.up.y = 1;
    camera.up.z = 0;
    camera.updateProjectionMatrix()
    scene = new THREE.Scene()
    scene.background = new THREE.Color(0xf8f8f8);
    renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
    })
    renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight)
    renderer.gammaFactor = 2.2
    console.log('THREE', THREE)
    renderer.setPixelRatio(window.devicePixelRatio) // 設定裝置畫素比
    canvasContainer.appendChild(renderer.domElement)

    controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    animate()
}

const animate = () => {
    requestAnimationFrame(() => animate())
    renderer.render(scene, camera)
    controls.update()
    // TWEEN.update()
}


const setLight = () => {
    const light1 = new THREE.DirectionalLight(0xffffff, 2)
    light1.position.set(0, 50, 0)
    scene.add(light1)
    const light2 = new THREE.DirectionalLight(0xffffff, 2)
    light2.position.set(0, 50, 100)
    scene.add(light2)
    const light3 = new THREE.DirectionalLight(0xffffff, 2)
    light3.position.set(0, 50, -100)
    scene.add(light3)
    const ambientLight = new THREE.AmbientLight(0xffffff, 2)
    scene.add(ambientLight)
}


// 設定模型大小
const initSize = (obj) => {
    let group = obj
    group.updateMatrixWorld()
    const box = new THREE.Box3().setFromObject(group)

    const size = box.getSize(new THREE.Vector3())
    const center = box.getCenter(new THREE.Vector3())
    const maxSize = Math.max(size.x, size.y, size.z)
    const targetSize = 1 // 目標大小
    const scale = targetSize / (maxSize > 1 ? maxSize : 0.5)
    group.scale.set(scale, scale, scale)
    controls.maxDistance = size.length() * 10
    camera.lookAt(center)
    camera.updateProjectionMatrix()
}

const getDracoLoader = () => {
    const dracoloader = new DRACOLoader()
    dracoloader.setDecoderPath('3d/draco/')
    dracoloader.setDecoderConfig({ type: 'js' })
    dracoloader.preload()
    return dracoloader
}

export const loadModel = (config) => {
    return new Promise((resolve, reject) => {
        const { type, url } = config
        const loader = modeLoader[type]()
        if (['gltf', 'glb'].includes(type)) {
            const dracoloader = getDracoLoader()
            loader.setDRACOLoader(dracoloader)
        }
        loader.load(url, (object) => {
            const obj = object.scene
            model = obj.children[0]
            scene.add(obj)

            // 新增世界座標輔助器
            const axesHelper = new THREE.AxesHelper(5);
            scene.add(axesHelper);

            initSize(obj)
            resolve(true)
        }, () => {

        }, err => {
            console.log(err)
            reject()
        })
    })
}

// 模型切換清除上個模型
const clearModel = () => {
    // 必須要清空當前div下的canvas不然canvas會繼續疊加
    if (canvasContainer) {
        canvasContainer.firstChild && canvasContainer.removeChild(canvasContainer.firstChild)
    }
}

export const tween3D = (Position) => {  //傳遞任意目標位置,從當前位置運動到目標位置
    camera.lookAt(new THREE.Vector3(0, 0, 0));
    // var p1 = { //定義相機位置是目標位置到中心點距離的2.2倍
    //   x: camera.position.x / 2.2,
    //   y: camera.position.y - 15,
    //   z: camera.position.z / 2.2
    // }
    var p1 = { //定義相機位置是目標位置到中心點距離的2.2倍
        x: camera.position.x,
        y: camera.position.y,
        z: camera.position.z
    }
    var p2 = {
        x: Position.x,
        y: Position.y,
        z: Position.z,
    }
    var update = function (object) {
        // camera.position.set(object.x * 2.2, object.y + 15, object.z * 2.2);
        camera.position.set(object.x, object.y, object.z);
        // controls.target.set(object.x, object.y, object.z);
        camera.lookAt(0, 0, 0); //保證動畫執行時,相機焦距在中心點
        controls.enabled = false;
        controls.update();
    };
    var tween = new TWEEN.Tween(p1).to(p2, 1200)//第一段動畫
        .onUpdate(update)
        // 動畫完成後的執行函式
        .onComplete(() => {
            controls.enabled = true; //執行完成後開啟控制
        })
        .easing(TWEEN.Easing.Quadratic.InOut)
        .start();

    // 放大動畫
    var targetScale = { x: 2, y: 2, z: 2 }; // 目標縮放比例
    const tweenScale = new TWEEN.Tween(model.scale) // 縮放物件
        .to(targetScale, 1000) // 動畫時間
        .onComplete(() => {
            // Tween動畫完成後重新繫結事件監聽器
            // window.addEventListener('wheel', onMouseWheel, false);
        })
        .easing(TWEEN.Easing.Quadratic.InOut) // 緩動函式
        .start(); // 開始動畫
}

// 組合模型載入(以obj和mtl 為例)
// const loadGroupModel = () => {
//   const { type, url } = config
//   const typeList = type.split(',')
//   let t1 = typeList[0]
//   let t2 = typeList[1]
//   const loader1 = modeLoader[t1]()
//   const loader2 = modeLoader[t2]()
//   loader1.load(url[0], (materials) => {
//     materials.preload()
//     loader2.setMaterials(materials)
//     loader2.load(url[1], (object) => {
//       scene.add(object)
//       initSize(object)
//     })
//   })
// }

DRACOLoader需要將 node_modules/three/examples/jsm/libs下的draco資料夾複製到 public 檔案下

需要安裝 copy-webpack-plugin 打包時將public資料夾複製出來

yarn add copy-webpack-plugin

webpack.config.js中

const copyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    ...
    plugins: [
        ...
        new copyWebpackPlugin([{ from: "public" }]),
    ],
}

使用

import React, { useState, useEffect, useRef } from 'react';
import { renderInit, loadModel } from './renderModel'
import s from './ThreeModel.less'

import model from './images/3d/pump3d.glb';

export default function ThreeModel(props) {
    const container = useRef()

    const config = {
        type: 'glb',
        url: model,
    }

    useEffect(() => {
        if (container.current && props.show?.pool3D) {
            loadObj(config)
        }
    }, [container, props.show?.pool3D])

    const loadObj = async (params) => {
        await renderInit(container)
        const load = await loadModel(params)
        // load =true 表示載入完成
        console.log('load', load)
    }

    return (<div className={s.modelCon}>
        <div className={s.modelBox} style={{ width: '100%', height: '100%' }} ref={container}>
        </div>
    </div>
    )
}

相關文章