import React, { useRef, useEffect } from 'react';
import * as THREE from 'three';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import JSZip from 'jszip';
const ModelViewer = ({ zipUrl }) => {
const mountRef = useRef(null);
useEffect(() => {
const initScene = () => {
const width = mountRef.current.clientWidth;
const height = mountRef.current.clientHeight;
const scene = new THREE.Scene();
// 设置背景颜色为白色
// scene.background = new THREE.Color(0xffffff);
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
mountRef.current.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// Ambient Light with higher intensity
const ambientLight = new THREE.AmbientLight(0x404040, 2); // higher intensity
scene.add(ambientLight);
// Directional Light to simulate sunlight
const directionalLight = new THREE.DirectionalLight(0xffffff, 2); // higher intensity
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
// Point Light to add some more brightness
// const pointLight = new THREE.PointLight(0xffffff, 2, 100); // higher intensity
// pointLight.position.set(0, 10, 10);
// scene.add(pointLight);
camera.position.z = 5;
controls.update();
// return { scene, camera, renderer, controls };
return { scene, camera, renderer };
};
const { scene, camera, renderer } = initScene();
const animate = (object) => {
// let animationId;
const render = function () {
requestAnimationFrame(render);
// animationId = requestAnimationFrame(render);
object.rotation.y += 0.01;
renderer.render(scene, camera);
};
render();
// 监听鼠标点击事件,停止自动旋转
// document.addEventListener('click', function() {
// cancelAnimationFrame(animationId);
// });
};
const loadModel = async (file, fileType, textureBlob) => {
let loader;
const fileBlob = await file.async("blob");
const fileUrl = URL.createObjectURL(fileBlob);
switch (fileType) {
case 'fbx':
loader = new FBXLoader();
break;
case 'obj':
const mtlFileUrl = getMtlFileUrl(fileUrl); // 自定义函数获取MTL文件URL
const mtlBlob = await fetchMtlBlob(mtlFileUrl); // 获取MTL文件的Blob
const mtlUrl = URL.createObjectURL(mtlBlob);
const mtlLoader = new MTLLoader();
mtlLoader.load(mtlUrl, (materials) => {
materials.preload(); // 预加载材质
const objLoader = new OBJLoader();
objLoader.setMaterials(materials); // 设置材质
objLoader.load(fileUrl, (object) => {
applyTexture(object, textureBlob);
// scene.add(object);
sizeAndAddModel(object, scene);
animate(object);
});
});
return; // 由于我们在内部加载,所以直接返回
case 'gltf':
case 'glb':
loader = new GLTFLoader();
break;
default:
console.error(`Unsupported file type: ${fileType}`);
return;
}
loader.load(fileUrl, (object) => {
applyTexture(object, textureBlob);
// scene.add(object);
sizeAndAddModel(object, scene);
animate(object);
});
};
const sizeAndAddModel = (object, scene) => {
// 计算模型边界框
const box = new THREE.Box3().setFromObject(object);
const size = box.getSize(new THREE.Vector3());
const targetSize = 4; // 目标大小
const scale = targetSize / Math.max(size.x, size.y, size.z);
object.scale.set(scale, scale, scale); // 设置缩放
scene.add(object);
};
// 获取MTL文件的Blob示例函数
const fetchMtlBlob = async (mtlFileUrl) => {
const response = await fetch(mtlFileUrl);
return await response.blob();
};
// 应用自定义纹理的通用函数
const applyTexture = (object, textureBlob) => {
if (textureBlob) {
const textureLoader = new THREE.TextureLoader();
const textureUrl = URL.createObjectURL(textureBlob);
textureLoader.load(textureUrl, (loadedTexture) => {
object.traverse((child) => {
if (child.isMesh) {
child.material.map = loadedTexture;
child.material.needsUpdate = true; // 通知材质需要更新
}
});
});
}
};
// 获取MTL文件URL的示例函数,可以根据需求自定义
const getMtlFileUrl = (objFileUrl) => {
return objFileUrl.replace('.obj', '.mtl'); // 假设MTL文件和OBJ文件同名
};
const fetchAndUnzip = async (url) => {
const response = await fetch(url.replace(/http:\/\//g, 'https://'));
const arrayBuffer = await response.arrayBuffer();
const zip = await JSZip.loadAsync(arrayBuffer);
let textureFile;
const modelFiles = [];
zip.forEach((relativePath, file) => {
const ext = relativePath.split('.').pop().toLowerCase();
// 检查并保存纹理文件
if (ext === 'png' || ext === 'jpg' || ext === 'jpeg') {
textureFile = file;
// 检查并保存模型文件
} else if (ext === 'fbx' || ext === 'obj' || ext === 'glb' || ext === 'gltf') {
modelFiles.push({ file, fileType: ext });
// 检查并保存MTL文件
} else if (ext === 'mtl') {
modelFiles.push({ file, fileType: 'mtl' });
}
});
const textureBlob = textureFile ? await textureFile.async("blob") : null;
for (const { file, fileType } of modelFiles) {
await loadModel(file, fileType, textureBlob);
}
};
fetchAndUnzip(zipUrl);
return () => {
mountRef.current.innerHTML = '';
};
}, [zipUrl]);
return <div ref={mountRef} style={{ width: 800, height: 600 }} />;
};
export default ModelViewer;
1-18.zip (3.0 MB)
The original model is blue and purple
Shown in yellow