浏览代码

Initial commit: 3D model viewer with Three.js

wangfumin 1 月之前
当前提交
2562c730e6
共有 9 个文件被更改,包括 73662 次插入0 次删除
  1. 二进制
      bg.png
  2. 二进制
      favicon.ico
  3. 79 0
      index.html
  4. 1556 0
      js/OrbitControls.js
  5. 254 0
      js/main.js
  6. 48830 0
      js/three.core.js
  7. 17313 0
      js/three.module.js
  8. 5630 0
      js/tinyusdz.js
  9. 二进制
      js/tinyusdz.wasm

二进制
bg.png


二进制
favicon.ico


+ 79 - 0
index.html

@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="zh">
+
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+	<title>四维时代</title>
+	<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
+	<style>
+		* {
+			margin: 0;
+			padding: 0;
+			box-sizing: border-box;
+		}
+
+		canvas {
+			touch-action: none;
+			/* 禁用浏览器默认触摸行为 */
+		}
+
+		body {
+			background-color: #ccc;
+		}
+
+		.loadingBox {
+			position: absolute;
+			width: 90%;
+			max-width: 1200px;
+			height: 6px;
+			border-radius: 3px;
+			border: 1px solid #2dc0ef;
+			top: 50%;
+			left: 50%;
+			transform: translate(-50%, -50%);
+			opacity: 1;
+			pointer-events: auto;
+			transition: all 0.5s;
+		}
+
+		#loading {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 0;
+			height: 100%;
+			background-color: #2dc0ef;
+		}
+
+		/* @media screen and (max-width: 1000px) {
+			.loadingBox {
+				height: 10px;
+				border-radius: 5px;
+
+			}
+		} */
+	</style>
+</head>
+
+<body>
+	<script>
+	</script>
+	<div class="loadingBox">
+		<div id="loading"></div>
+	</div>
+	<script type="module" src="./js/main.js"></script>
+</body>
+<!-- 
+  <script>
+    window.onload = function () {
+      var script = document.createElement('script')
+      script.src = 'http://cdn.bootcss.com/eruda/1.5.4/eruda.min.js'
+      document.body.appendChild(script)
+      script.onload = function () {
+        eruda.init()
+      }
+    }
+  </script> -->
+
+</html>

文件差异内容过多而无法显示
+ 1556 - 0
js/OrbitControls.js


+ 254 - 0
js/main.js

@@ -0,0 +1,254 @@
+import * as THREE from "../js/three.module.js"
+
+import initTinyUSDZ from "../js/tinyusdz.js"
+
+import { OrbitControls } from "../js/OrbitControls.js"
+
+const urlAll = window.location.href.split("?m=")[1]
+
+// console.log(123456,urlAll);
+
+// const USDZ_FILEPATH = `https://4dkankan.oss-cn-shenzhen.aliyuncs.com/sxz/${urlAll}.usdz`;
+// const USDZ_FILEPATH = `https://us-test.4dkankan.com/20241202dome/Shuiyueguanyin.usdz`;
+const USDZ_FILEPATH = `https://swkz-1332577016.cos.ap-guangzhou.myqcloud.com/kanzhan/2025/06/23/data.usdz`;
+// const USDZ_FILEPATH = `https://3d-usdz.4dkankan.com/sxz/${urlAll}.usdz`
+
+document.addEventListener("DOMContentLoaded", async () => {
+  const loadingBar = document.getElementById("loading") // 获取加载条元素
+  loadingBar.style.display = "block" // 显示加载条
+
+  const usd_res = await fetch(USDZ_FILEPATH)
+  const totalBytes = parseInt(usd_res.headers.get("Content-Length"), 10) // 获取总字节数
+  const reader = usd_res.body.getReader() // 获取读取器
+  const chunks = [] // 存储数据块
+  let receivedLength = 0 // 已接收字节数
+
+  let timeOut = -1
+
+  // 更新加载条的函数
+  const updateLoadingBar = (loaded) => {
+    let percentage = (loaded / totalBytes) * 100
+    if (percentage >= 100) {
+      percentage = 100
+      // 隐藏加载条
+
+      clearTimeout(timeOut)
+      timeOut = setTimeout(() => {
+        const loadingBoxDom = document.querySelector(".loadingBox")
+        loadingBoxDom.style.opacity = 0
+        loadingBoxDom.style.pointerEvents = "none"
+      }, 300)
+    }
+
+    loadingBar.style.width = `${percentage}%` // 更新加载条宽度
+  }
+
+  // 读取数据流
+  while (true) {
+    const { done, value } = await reader.read() // 读取数据
+    if (done) break // 如果读取完成,退出循环
+    chunks.push(value) // 存储数据块
+    receivedLength += value.length // 更新已接收字节数
+    updateLoadingBar(receivedLength) // 更新加载条
+  }
+
+  const usd_data = new Uint8Array(receivedLength) // 创建最终数据数组
+  let position = 0
+  for (const chunk of chunks) {
+    usd_data.set(chunk, position) // 将数据块写入最终数组
+    position += chunk.length // 更新位置
+  }
+
+  // const usd_binary = new Uint8Array(usd_data);
+
+  // 加载 TinyUSDZ
+  initTinyUSDZ().then(function (TinyUSDZLoader) {
+    const usd = new TinyUSDZLoader.TinyUSDZLoader(usd_data)
+    // console.log(usd.numMeshes());
+    // 隐藏加载条
+    loadingBar.style.display = "none"
+
+    const scene = new THREE.Scene()
+    // 添加渐变背景
+    const bgTexture = new THREE.TextureLoader().load('./bg.png')
+    scene.background = bgTexture
+    const camera = new THREE.PerspectiveCamera(
+      85,
+      window.innerWidth / window.innerHeight,
+      0.1,
+      1000
+    )
+
+    const renderer = new THREE.WebGLRenderer({
+      alpha: true, // 关键配置,设置背景透明
+      antialias: true, // 可选抗锯齿
+    })
+
+    renderer.setClearColor(0x000000, 1) // 第二个参数为透明度(0表示完全透明)
+
+    renderer.setSize(window.innerWidth, window.innerHeight)
+    renderer.setAnimationLoop(animate)
+    // 设置输出颜色空间为线性SRGB
+    renderer.outputColorSpace = THREE.LinearSRGBColorSpace
+    document.body.appendChild(renderer.domElement)
+
+    // First mesh only
+    const mesh = usd.getMesh(0)
+    //console.log("usd", usd)
+    //console.log("mesh", mesh);
+
+    //const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+    const geometry = new THREE.BufferGeometry()
+    geometry.setAttribute(
+      "position",
+      new THREE.BufferAttribute(mesh.points, 3)
+    )
+    // TODO: set normal from mesh
+
+    if (mesh.hasOwnProperty("texcoords")) {
+      // console.log(mesh.texcoords);
+      geometry.setAttribute("uv", new THREE.BufferAttribute(mesh.texcoords, 2))
+    }
+
+    const usdMaterial = usd.getMaterial(mesh.materialId)
+    //console.log("usdMat", usdMaterial);
+    //if (usdMaterial.aaa) {
+    //  console.log("aaa");
+    //}
+
+    var material
+
+    if (usdMaterial.hasOwnProperty("diffuseColorTextureId")) {
+      const diffTex = usd.getTexture(usdMaterial.diffuseColorTextureId)
+
+      const img = usd.getImage(diffTex.textureImageId)
+      // console.log(img);
+
+      // assume RGBA for now.
+      let image8Array = new Uint8ClampedArray(img.data)
+      let imgData = new ImageData(image8Array, img.width, img.height)
+
+      const texture = new THREE.DataTexture(imgData, img.width, img.height)
+      texture.flipY = true
+      texture.needsUpdate = true
+
+      material = new THREE.MeshBasicMaterial({
+        map: texture,
+      })
+    } else {
+      material = new THREE.MeshNormalMaterial()
+    }
+
+    // Assume triangulated indices.
+    geometry.setIndex(
+      new THREE.Uint32BufferAttribute(mesh.faceVertexIndices, 1)
+    )
+
+    geometry.computeVertexNormals()
+
+    //const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
+    const cube = new THREE.Mesh(geometry, material)
+
+    // 缩放模型
+    cube.scale.set(7, 7, 7)
+    cube.updateMatrixWorld(true) // 强制更新世界矩阵[3](@ref)
+
+    // 计算包围盒
+    const box = new THREE.Box3().setFromObject(cube)
+    const center = new THREE.Vector3()
+    box.getCenter(center)
+
+    // 居中模型
+    cube.position.sub(center)
+
+    // 计算尺寸并调整相机
+    const size = box.getSize(new THREE.Vector3()).length()
+    camera.near = size / 100
+    camera.far = size * 100
+    camera.updateProjectionMatrix()
+    camera.position.set(size * 0.5, size * 0.5, size * 0.5)
+    camera.lookAt(0, 0, 0)
+    // 向上移动模型
+    // cube.position.y += 0.5  // 向上
+    scene.add(cube)
+
+    const controls = new OrbitControls(camera, renderer.domElement)
+
+    controls.enablePan = true // 禁用右键平移功能
+
+    controls.enableZoom = true // 必须禁用轨道控制器的默认缩放[1](@ref)
+
+    controls.enableDamping = true
+    controls.dampingFactor = 0.25
+    controls.screenSpacePanning = false
+    controls.minPolarAngle = Math.PI / 180 * 1 // 1 度(约 0.01745 弧度)
+    controls.maxPolarAngle = Math.PI - Math.PI / 180 * 1 // 179 度(约 3.124 弧度)
+
+    // 兼容鼠标滚轮与触摸屏双指缩放
+    // 兼容鼠标滚轮与触摸屏双指缩放
+    // const handleZoom = (delta) => {
+    //   // console.log('--------',delta);
+
+    //   cube.updateMatrixWorld(true);
+    //   // 计算包围盒
+    //   const box = new THREE.Box3().setFromObject(cube);
+    //   const center = new THREE.Vector3();
+    //   box.getCenter(center);
+
+    //   // 居中模型
+    //   cube.position.sub(center);
+
+    //   cube.scale.multiplyScalar(delta);
+    //   cube.scale.clampScalar(0.5, 4); // 限制缩放范围0.5-3倍[1,6](@ref)
+    // };
+
+    // 鼠标滚轮事件
+    // window.addEventListener(
+    //   "wheel",
+    //   (e) => {
+    //     e.preventDefault();
+    //     const zoomFactor = e.deltaY > 0 ? 0.95 : 1.05;
+    //     handleZoom(zoomFactor);
+    //   },
+    //   { passive: false }
+    // );
+
+    // 触摸屏双指缩放
+    // let initialDistance = 0;
+    // window.addEventListener("touchstart", (e) => {
+    //   if (e.touches.length === 2) {
+    //     initialDistance = Math.hypot(
+    //       e.touches[0].pageX - e.touches[1].pageX,
+    //       e.touches[0].pageY - e.touches[1].pageY
+    //     );
+    //   }
+    // });
+
+    // window.addEventListener("touchmove", (e) => {
+    //   if (e.touches.length === 2) {
+    //     const currentDistance = Math.hypot(
+    //       e.touches[0].pageX - e.touches[1].pageX,
+    //       e.touches[0].pageY - e.touches[1].pageY
+    //     );
+    //     const zoomFactor = currentDistance > initialDistance ? 1.01 : 0.99;
+    //     handleZoom(zoomFactor);
+    //     initialDistance = currentDistance;
+    //   }
+    // });
+
+    function animate() {
+      //cube.rotation.x += 0.01;
+      // cube.rotation.y += 0.02;
+
+      controls.update()
+
+      renderer.render(scene, camera)
+    }
+
+    window.addEventListener("resize", () => {
+      camera.aspect = window.innerWidth / window.innerHeight
+      camera.updateProjectionMatrix()
+      renderer.setSize(window.innerWidth, window.innerHeight)
+    })
+  })
+})

文件差异内容过多而无法显示
+ 48830 - 0
js/three.core.js


文件差异内容过多而无法显示
+ 17313 - 0
js/three.module.js


文件差异内容过多而无法显示
+ 5630 - 0
js/tinyusdz.js


二进制
js/tinyusdz.wasm