shaogen1995 5 miesięcy temu
rodzic
commit
c59f65fcd1

+ 2 - 1
package.json

@@ -33,6 +33,7 @@
     "redux-thunk": "^2.4.1",
     "sass": "^1.55.0",
     "three": "^0.174.0",
+    "three-usdz-loader": "^1.0.9",
     "typescript": "^4.8.4",
     "web-vitals": "^2.1.4"
   },
@@ -67,4 +68,4 @@
     "react-app-rewired": "^2.2.1"
   },
   "homepage": "."
-}
+}

+ 1 - 1
public/index.html

@@ -22,7 +22,7 @@
   <!-- <script>
     window.onload = function () {
       var script = document.createElement('script')
-      script.src = 'https://cdn.bootcss.com/eruda/1.5.4/eruda.min.js'
+      script.src = 'http://cdn.bootcss.com/eruda/1.5.4/eruda.min.js'
       document.body.appendChild(script)
       script.onload = function () {
         eruda.init()

public/modelFile/saeukkang.hdr → public/modelFile/setting.hdr


BIN
public/modelFile/test1.glb


+ 2 - 1
src/App.tsx

@@ -12,6 +12,7 @@ import MessageCom from './components/Message'
 import screenImg from '@/assets/img/landtip.png'
 
 const A1home = React.lazy(() => import('./pages/A1home'))
+const A2demo = React.lazy(() => import('./pages/A2demo'))
 
 export default function App() {
   // 从仓库中获取查看图片的信息
@@ -35,7 +36,7 @@ export default function App() {
       <Router history={history}>
         <React.Suspense fallback={<SpinLoding />}>
           <Switch>
-            {/* <Route path='/codeSucc/:id' component={A3codeSucc} /> */}
+            <Route path='/demo' component={A2demo} />
             <Route path='/' component={A1home} />
           </Switch>
         </React.Suspense>

+ 28 - 0
src/pages/A1home/index.module.scss

@@ -1,4 +1,32 @@
 .A1home {
   :global {
+    .usdzBox {
+      width: 100%;
+      height: 100%;
+    }
+    .loding {
+      position: absolute;
+      z-index: 2;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      left: 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      .lodingTiao {
+        width: 90%;
+        max-width: 1200px;
+        height: 10px;
+        border-radius: 5px;
+        border: 1px solid var(--themeColor);
+        overflow: hidden;
+        & > div {
+          height: 100%;
+          width: 0%;
+          background-color: var(--themeColor);
+        }
+      }
+    }
   }
 }

+ 133 - 27
src/pages/A1home/index.tsx

@@ -1,13 +1,22 @@
-import React, { useCallback, useEffect, useRef } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
 import styles from './index.module.scss'
 
 import * as THREE from 'three'
 
 import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
 import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
-import { USDZLoader } from 'three/addons/loaders/USDZLoader.js'
+import { USDZLoader as USDZLoader1 } from 'three/addons/loaders/USDZLoader.js'
+// import { USDZLoader as USDZLoader1 } from 'three/examples/jsm/loaders/USDZLoader.js'
+
+// import { USDZInstance } from 'three-usdz-loader/lib/USDZInstance'
+// import { USDZLoader as USDZLoader2 } from 'three-usdz-loader'
+
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
 
 function A1home() {
+  // 进度条
+  const [loding, setLoding] = useState(0)
+
   const camera = useRef(
     new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100)
   )
@@ -31,30 +40,115 @@ function A1home() {
     // 获取地址栏参数
     const fileName = window.location.href.split('?m=')[1]
 
-    camera.current.position.set(0, 0.75, -3.5)
+    if (!fileName) return alert('参数错误')
 
     const rgbeLoader = new RGBELoader().setPath('./modelFile/')
 
-    const usdzLoader = new USDZLoader().setPath('./modelFile/')
-
-    const [texture, model] = await Promise.all([
-      rgbeLoader.loadAsync(`saeukkang.hdr`),
-      usdzLoader.loadAsync(`${fileName}.usdz`)
-    ])
-
-    // environment
-
-    texture.mapping = THREE.EquirectangularReflectionMapping
-
-    scene.current.background = texture
-    scene.current.backgroundBlurriness = 0.5
-    scene.current.environment = texture
-
-    // model
-
-    model.position.y = 0.25
-    model.position.z = -0.25
-    scene.current.add(model)
+    const usdzLoader = new USDZLoader1().setPath('./modelFile/')
+
+    let textureProgress = 0,
+      modelProgress = 0
+    try {
+      camera.current.position.set(0, 0.75, -3.5)
+
+      // 加载资源(改用回调形式)
+
+      const texturePromise: Promise<any> = new Promise((resolve, reject) => {
+        rgbeLoader.load(
+          'setting.hdr',
+          resolve,
+          (xhr: any) => {
+            textureProgress = (xhr.loaded / xhr.total) * 50 // HDR占50%权重
+            updateProgress()
+          },
+          reject
+        )
+      })
+
+      const modelPromise: Promise<any> = new Promise((resolve, reject) => {
+        usdzLoader.load(
+          `${fileName}`,
+          resolve,
+          (xhr: any) => {
+            modelProgress = (xhr.loaded / xhr.total) * 50 // USDZ占50%权重
+            updateProgress()
+          },
+          reject
+        )
+      })
+
+      const updateProgress = () => {
+        const total = Math.floor(textureProgress + modelProgress)
+        setLoding(total)
+      }
+
+      // 等待所有资源加载
+      const [texture, model] = await Promise.all([texturePromise, modelPromise])
+
+      // environment
+
+      texture.mapping = THREE.EquirectangularReflectionMapping
+
+      scene.current.background = texture
+      scene.current.backgroundBlurriness = 0.5
+      scene.current.environment = texture
+
+      // model
+
+      model.position.y = 0.25
+      model.position.z = -0.25
+      scene.current.add(model)
+    } catch (error) {
+      console.log(123, error)
+      if (1 + 1 === 2) return
+      console.log('usdz是二进制,转成glb格式')
+
+      camera.current.position.set(0, 0.75, 0.75)
+
+      // 这里显示的应该是二进制的usdz文件
+      renderer.current.setClearColor(0xffffff)
+      // 添加光源
+      const ambientLight = new THREE.AmbientLight(0xffffff, 1)
+      scene.current.add(ambientLight)
+      const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
+      directionalLight.position.set(5, 5, 5)
+      scene.current.add(directionalLight)
+
+      // 加载管理器与进度处理
+      const loadingManager = new THREE.LoadingManager()
+      loadingManager.onProgress = (url, loaded, total) => {
+        setLoding((loaded / total) * 100)
+      }
+      loadingManager.onLoad = () => {}
+
+      // 加载GLB模型
+      const loader = new GLTFLoader(loadingManager)
+      loader.load(
+        `./modelFile/${fileName.replace('usdz', 'glb')}`,
+        (gltf: any) => {
+          const model = gltf.scene
+          scene.current.add(model)
+
+          model.position.y = -0.2
+
+          // 播放动画(如果有)
+          if (gltf.animations?.length) {
+            const mixer = new THREE.AnimationMixer(model)
+            const action = mixer.clipAction(gltf.animations[0])
+            action.play()
+            // 动画循环
+            const animate = () => {
+              requestAnimationFrame(animate)
+              mixer.update(0.01)
+              renderer.current.render(scene.current, camera.current)
+            }
+            animate()
+          }
+        }
+        // (xhr: any) => console.log('加载进度:', xhr.loaded / xhr.total),
+        // (error: any) => console.error('加载失败:', error)
+      )
+    }
 
     // renderer
 
@@ -63,11 +157,12 @@ function A1home() {
     renderer.current.setAnimationLoop(animate)
     renderer.current.toneMapping = THREE.ACESFilmicToneMapping
     renderer.current.toneMappingExposure = 2.0
-    document.querySelector('#A1home')!.appendChild(renderer.current.domElement)
+
+    document.querySelector('.usdzBox')!.appendChild(renderer.current.domElement)
 
     const controls = new OrbitControls(camera.current, renderer.current.domElement)
-    controls.minDistance = 1
-    controls.maxDistance = 8
+    controls.minDistance = 0.5
+    controls.maxDistance = 10
     // controls.target.y = 15;
     // controls.update();
 
@@ -78,7 +173,18 @@ function A1home() {
     init()
   }, [init])
 
-  return <div className={styles.A1home} id='A1home'></div>
+  return (
+    <div className={styles.A1home}>
+      <div className='usdzBox'></div>
+      {loding >= 100 ? null : (
+        <div className='loding'>
+          <div className='lodingTiao'>
+            <div style={{ width: loding + '%' }}></div>
+          </div>
+        </div>
+      )}
+    </div>
+  )
 }
 
 const MemoA1home = React.memo(A1home)

+ 160 - 0
src/pages/A2demo/ccc.ts

@@ -0,0 +1,160 @@
+/**
+ * three-usdz-loader 完整示例
+ * 需配合 Three.js 场景使用,建议使用 Vite/Webpack 等构建工具
+ * 前置条件:
+ * 1. 已通过 npm 安装 three three-usdz-loader
+ * 2. 已部署 WASM 文件到 public/external/ 目录
+ * 3. 服务器配置了正确的 COEP/COOP 头信息
+ */
+
+import { USDZLoader } from 'three-usdz-loader'
+import * as THREE from 'three'
+
+// ================= 初始化 Three.js 基础场景 =================
+const scene = new THREE.Scene()
+const camera = new THREE.PerspectiveCamera(
+  75,
+  window.innerWidth / window.innerHeight,
+  0.1,
+  1000
+)
+const renderer = new THREE.WebGLRenderer({ antialias: true })
+
+// 初始化渲染器
+renderer.setSize(window.innerWidth, window.innerHeight)
+renderer.setPixelRatio(window.devicePixelRatio)
+document.querySelector('#A2demo')!.appendChild(renderer.domElement)
+
+// 相机初始位置
+camera.position.set(0, 0, 5)
+camera.lookAt(0, 0, 0)
+
+// 添加环境光(USDZ 材质需要光照)
+const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
+scene.add(ambientLight)
+const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
+directionalLight.position.set(0, 10, 5)
+scene.add(directionalLight)
+
+// ================= USDZ 加载器配置 =================
+let currentModel: any | null = null
+const modelGroup = new THREE.Group() // 用于存放加载的模型
+scene.add(modelGroup)
+
+/**
+ * 初始化加载器
+ * 参数说明:
+ * @param wasmPath - 指定 WASM 文件的基路径(默认同源根目录)
+ * 如果部署在 /public/external/ 需设置为 '/external/'
+ */
+
+let obj: any = {
+  wasmPath: './external/',
+  manager: new THREE.LoadingManager(() => {
+    console.log('所有资源加载完成')
+  })
+}
+
+const loader = new USDZLoader(obj)
+
+// 监听加载进度
+// loader.manager.onProgress = (url, loaded, total) => {
+//   console.log(`加载进度: ${loaded}/${total} ${url}`)
+// }
+
+// ================= 核心加载方法 =================
+/**
+ * 加载 USDZ 文件
+ * @param file - 通过文件输入获取的 File 对象
+ */
+async function loadUSDZModel(file: File) {
+  try {
+    // 清理旧模型(重要!防止内存泄漏)
+    if (currentModel) {
+      currentModel.clean()
+      modelGroup.clear()
+      currentModel = null
+    }
+
+    // 开始加载(第二个参数指定父容器)
+    currentModel = await loader.loadFile(file, modelGroup)
+
+    // 自动调整相机视角
+    fitCameraToModel(camera, modelGroup, 1.5)
+
+    // 处理动画
+    if (currentModel.animations) {
+      currentModel.animations.play() // 自动播放动画
+      console.log(`检测到 ${currentModel.animations.animations.length} 个动画片段`)
+    }
+
+    console.log('模型加载成功:', currentModel)
+  } catch (error) {
+    console.error('模型加载失败:', error)
+    // 建议在此处添加用户提示逻辑
+  }
+}
+
+// ================= 工具函数 =================
+/**
+ * 调整相机位置适配模型
+ * @param camera - 场景相机
+ * @param model - 模型容器
+ * @param scaleFactor - 缩放系数(建议 1-2)
+ */
+function fitCameraToModel(
+  camera: THREE.PerspectiveCamera,
+  model: THREE.Group,
+  scaleFactor: number
+) {
+  const bbox = new THREE.Box3().expandByObject(model)
+  const center = bbox.getCenter(new THREE.Vector3())
+  const size = bbox.getSize(new THREE.Vector3()).length()
+
+  camera.position.copy(center)
+  camera.position.z += size * scaleFactor
+  camera.lookAt(center)
+
+  // 更新相机参数
+  camera.near = size / 100
+  camera.far = size * 100
+  camera.updateProjectionMatrix()
+}
+
+// ================= 清理资源 =================
+function cleanupScene() {
+  if (currentModel) {
+    currentModel.animations?.stop() // 停止动画
+    currentModel.clean() // 销毁几何体和材质
+    modelGroup.clear() // 移除所有子对象
+    currentModel = null
+  }
+  renderer.dispose() // 释放 WebGL 资源
+}
+
+// ================= 渲染循环 =================
+function animate() {
+  requestAnimationFrame(animate)
+  renderer.render(scene, camera)
+}
+animate()
+
+// ================= 窗口尺寸变化处理 =================
+window.addEventListener('resize', () => {
+  camera.aspect = window.innerWidth / window.innerHeight
+  camera.updateProjectionMatrix()
+  renderer.setSize(window.innerWidth, window.innerHeight)
+})
+
+// ================= 示例:通过文件输入触发加载 =================
+// 在 HTML 中添加:<input type="file" id="usdzInput" accept=".usdz" />
+const fileInput = document.getElementById('usdzInput') as HTMLInputElement
+fileInput.addEventListener('change', async e => {
+  const file = (e.target as HTMLInputElement).files?.[0]
+  if (file && file.name.endsWith('.usdz')) {
+    await loadUSDZModel(file)
+  }
+})
+
+// 在页面卸载时清理资源
+window.addEventListener('beforeunload', cleanupScene)

+ 4 - 0
src/pages/A2demo/index.module.scss

@@ -0,0 +1,4 @@
+.A2demo {
+  :global {
+  }
+}

+ 21 - 0
src/pages/A2demo/index.tsx

@@ -0,0 +1,21 @@
+import React, { useEffect } from 'react'
+import styles from './index.module.scss'
+function A2demo() {
+  const loadModule = async () => {
+    await import('./ccc')
+  }
+
+  useEffect(() => {
+    loadModule()
+  }, [])
+
+  return (
+    <div className={styles.A2demo} id='A2demo'>
+      <input type='file' id='usdzInput' accept='.usdz' />
+    </div>
+  )
+}
+
+const MemoA2demo = React.memo(A2demo)
+
+export default MemoA2demo

+ 1 - 0
src/types/declaration.d.ts

@@ -11,3 +11,4 @@ declare module 'braft-utils'
 declare module 'three/addons/controls/OrbitControls.js'
 declare module 'three/addons/loaders/RGBELoader.js'
 declare module 'three/addons/loaders/USDZLoader.js'
+declare module 'three/addons/loaders/GLTFLoader.js'

+ 5 - 0
yarn.lock

@@ -9546,6 +9546,11 @@ text-table@^0.2.0:
   resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz"
   integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
 
+three-usdz-loader@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.npmmirror.com/three-usdz-loader/-/three-usdz-loader-1.0.9.tgz#b6a8ab8b5958cb76c56a35ccaf65ee702966c85f"
+  integrity sha512-UOP2XHHT4dUIZInhVPQieVXFyxpSE1ApMEJZVLDdo5j5flp1KSGlY1uGE3To7KhDIks+kAOmsCnNDjQ3RvjMFA==
+
 three@^0.174.0:
   version "0.174.0"
   resolved "https://registry.npmmirror.com/three/-/three-0.174.0.tgz#53f46d6fd27515231b2af321f798f1e0ecf3f905"