|
@@ -0,0 +1,248 @@
|
|
|
+// `#{serverUrl}0/model.glb`
|
|
|
+// <div className={classNames(styles.A0model, isShow ? styles.A0modelShow : '')}>
|
|
|
+import React, { useRef, useState, useEffect, Suspense } from 'react'
|
|
|
+import * as THREE from 'three'
|
|
|
+import { Canvas, useThree, useFrame } from '@react-three/fiber'
|
|
|
+import { OrbitControls, useProgress, Html, Loader } from '@react-three/drei'
|
|
|
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
|
|
+import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
|
|
|
+
|
|
|
+// 1. 加载进度条组件
|
|
|
+function LoadProgress() {
|
|
|
+ const { progress } = useProgress()
|
|
|
+ return (
|
|
|
+ <Html center>
|
|
|
+ <div style={{ color: 'white', fontSize: '14px' }}>
|
|
|
+ 加载中: {progress.toFixed(2)}%
|
|
|
+ </div>
|
|
|
+ </Html>
|
|
|
+ )
|
|
|
+}
|
|
|
+// 2. 模型组件
|
|
|
+interface ModelProps {
|
|
|
+ onLoaded: (model: THREE.Group, parts: { [name: string]: THREE.Object3D }) => void
|
|
|
+ onProgress: (progress: number) => void
|
|
|
+}
|
|
|
+
|
|
|
+const Model: React.FC<ModelProps> = ({ onLoaded, onProgress }) => {
|
|
|
+ const groupRef = useRef<THREE.Group>(null)
|
|
|
+ const [modelParts, setModelParts] = useState<{ [name: string]: THREE.Object3D }>({})
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const manager = new THREE.LoadingManager()
|
|
|
+ manager.onProgress = (url, itemsLoaded, itemsTotal) => {
|
|
|
+ onProgress((itemsLoaded / itemsTotal) * 100)
|
|
|
+ }
|
|
|
+
|
|
|
+ const dracoLoader = new DRACOLoader()
|
|
|
+ dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')
|
|
|
+ dracoLoader.setDecoderConfig({ type: 'js' })
|
|
|
+
|
|
|
+ const loader = new GLTFLoader(manager)
|
|
|
+ loader.setDRACOLoader(dracoLoader)
|
|
|
+
|
|
|
+ loader.load(
|
|
|
+ '/path/to/your/model.glb', // 替换为你的GLB模型路径
|
|
|
+ gltf => {
|
|
|
+ const model = gltf.scene
|
|
|
+ model.traverse((child: any) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ child.castShadow = true
|
|
|
+ child.receiveShadow = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 3. 假设模型中的三个部分分别命名为'bs1', 'bs2', 'bs3'
|
|
|
+ const parts: { [name: string]: THREE.Object3D } = {}
|
|
|
+ model.children.forEach(child => {
|
|
|
+ if (['bs1', 'bs2', 'bs3'].includes(child.name)) {
|
|
|
+ parts[child.name] = child
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ setModelParts(parts)
|
|
|
+ onLoaded(model, parts)
|
|
|
+ },
|
|
|
+ xhr => {
|
|
|
+ // 加载进度回调已在LoadingManager中处理
|
|
|
+ },
|
|
|
+ error => {
|
|
|
+ console.error('加载模型出错:', error)
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }, [onLoaded, onProgress])
|
|
|
+
|
|
|
+ return groupRef.current ? <primitive object={groupRef.current} /> : null
|
|
|
+}
|
|
|
+
|
|
|
+// 4. 场景设置与控制器组件
|
|
|
+function SceneSetup() {
|
|
|
+ const { camera, gl } = useThree()
|
|
|
+ const controlsRef = useRef<any>(null)
|
|
|
+
|
|
|
+ useFrame(() => {
|
|
|
+ if (controlsRef.current) {
|
|
|
+ controlsRef.current.update() // 启用阻尼效果必须每帧更新控制器
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <ambientLight intensity={0.5} />
|
|
|
+ <directionalLight position={[10, 10, 5]} intensity={1} castShadow />
|
|
|
+ <OrbitControls
|
|
|
+ ref={controlsRef}
|
|
|
+ enableDamping={true} // 启用阻尼效果
|
|
|
+ dampingFactor={0.05} // 阻尼系数[7](@ref)
|
|
|
+ maxDistance={2} // 根据初始缩放比例调整
|
|
|
+ minDistance={0.5} // 根据初始缩放比例调整[9](@ref)
|
|
|
+ screenSpacePanning={true} // 移动端友好的平移方式
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// 5. 主组件
|
|
|
+const GLBModelViewer: React.FC = () => {
|
|
|
+ const [loadingProgress, setLoadingProgress] = useState(0)
|
|
|
+ const [model, setModel] = useState<THREE.Group | null>(null)
|
|
|
+ const [modelParts, setModelParts] = useState<{ [name: string]: THREE.Object3D }>({})
|
|
|
+ const [activePart, setActivePart] = useState<string | null>(null)
|
|
|
+ const { camera, scene } = useThree()
|
|
|
+
|
|
|
+ const handleModelLoaded = (
|
|
|
+ loadedModel: THREE.Group,
|
|
|
+ parts: { [name: string]: THREE.Object3D }
|
|
|
+ ) => {
|
|
|
+ setModel(loadedModel)
|
|
|
+ setModelParts(parts)
|
|
|
+ // 初始状态下显示所有模型
|
|
|
+ Object.values(parts).forEach(part => {
|
|
|
+ part.visible = true
|
|
|
+ })
|
|
|
+ scene.add(loadedModel)
|
|
|
+ }
|
|
|
+
|
|
|
+ const focusOnPart = (partName: string) => {
|
|
|
+ if (!model || !modelParts[partName]) return
|
|
|
+
|
|
|
+ // 隐藏所有部分,然后显示选中的部分
|
|
|
+ Object.entries(modelParts).forEach(([name, part]) => {
|
|
|
+ part.visible = name === partName
|
|
|
+ })
|
|
|
+
|
|
|
+ // 获取选中部分的包围盒并居中相机
|
|
|
+ const bbox = new THREE.Box3().setFromObject(modelParts[partName])
|
|
|
+ const center = bbox.getCenter(new THREE.Vector3())
|
|
|
+ const size = bbox.getSize(new THREE.Vector3())
|
|
|
+
|
|
|
+ // 计算相机位置,使其能够完整看到模型
|
|
|
+ const maxDim = Math.max(size.x, size.y, size.z)
|
|
|
+ const fov =
|
|
|
+ camera instanceof THREE.PerspectiveCamera ? (camera.fov * Math.PI) / 180 : 0
|
|
|
+ const cameraDistance = maxDim / (2 * Math.tan(fov / 2))
|
|
|
+
|
|
|
+ camera.position.copy(
|
|
|
+ center.clone().add(new THREE.Vector3(0, 0, cameraDistance * 1.5))
|
|
|
+ )
|
|
|
+ camera.lookAt(center)
|
|
|
+
|
|
|
+ setActivePart(partName)
|
|
|
+ }
|
|
|
+
|
|
|
+ const showAllModels = () => {
|
|
|
+ if (!model) return
|
|
|
+
|
|
|
+ // 显示所有模型部分
|
|
|
+ Object.values(modelParts).forEach(part => {
|
|
|
+ part.visible = true
|
|
|
+ })
|
|
|
+
|
|
|
+ // 将相机定位到整个模型的中心
|
|
|
+ const bbox = new THREE.Box3().setFromObject(model)
|
|
|
+ const center = bbox.getCenter(new THREE.Vector3())
|
|
|
+ const size = bbox.getSize(new THREE.Vector3())
|
|
|
+
|
|
|
+ const maxDim = Math.max(size.x, size.y, size.z)
|
|
|
+ const fov =
|
|
|
+ camera instanceof THREE.PerspectiveCamera ? (camera.fov * Math.PI) / 180 : 0
|
|
|
+ const cameraDistance = maxDim / (2 * Math.tan(fov / 2))
|
|
|
+
|
|
|
+ camera.position.copy(
|
|
|
+ center.clone().add(new THREE.Vector3(0, 0, cameraDistance * 1.5))
|
|
|
+ )
|
|
|
+ camera.lookAt(center)
|
|
|
+
|
|
|
+ setActivePart(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
|
|
|
+ <Canvas
|
|
|
+ shadows
|
|
|
+ camera={{ position: [0, 0, 5], fov: 50 }}
|
|
|
+ onCreated={({ gl }) => {
|
|
|
+ gl.setClearColor(new THREE.Color(0x000000))
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Suspense fallback={<LoadProgress />}>
|
|
|
+ <SceneSetup />
|
|
|
+ <Model onLoaded={handleModelLoaded} onProgress={setLoadingProgress} />
|
|
|
+ </Suspense>
|
|
|
+ </Canvas>
|
|
|
+
|
|
|
+ {/* 6. 控制按钮 UI */}
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ bottom: '20px',
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ display: 'flex',
|
|
|
+ justifyContent: 'center',
|
|
|
+ gap: '10px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <button onClick={() => focusOnPart('bs1')} style={{ padding: '10px' }}>
|
|
|
+ 显示 BS1
|
|
|
+ </button>
|
|
|
+ <button onClick={() => focusOnPart('bs2')} style={{ padding: '10px' }}>
|
|
|
+ 显示 BS2
|
|
|
+ </button>
|
|
|
+ <button onClick={() => focusOnPart('bs3')} style={{ padding: '10px' }}>
|
|
|
+ 显示 BS3
|
|
|
+ </button>
|
|
|
+ <button onClick={showAllModels} style={{ padding: '10px' }}>
|
|
|
+ 显示全部
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 加载进度条 */}
|
|
|
+ {loadingProgress < 100 && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: '50%',
|
|
|
+ left: '20%',
|
|
|
+ right: '20%',
|
|
|
+ height: '20px',
|
|
|
+ background: 'rgba(255, 255, 255, 0.2)',
|
|
|
+ borderRadius: '10px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ height: '100%',
|
|
|
+ width: `${loadingProgress}%`,
|
|
|
+ background: 'linear-gradient(90deg, #4facfe 0%, #00f2fe 100%)',
|
|
|
+ borderRadius: '10px',
|
|
|
+ transition: 'width 0.3s ease'
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default GLBModelViewer
|