threejs编写加载本地的obj、gltf(glb)、3dtiles数据可视化页面

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3D 模型加载器</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      font-family: Arial, sans-serif;
    }

    #info {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(0, 0, 0, 0.7);
      color: white;
      padding: 10px;
      border-radius: 5px;
      z-index: 100;
    }

    #loading {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: white;
      font-size: 20px;
      z-index: 1000;
      background: rgba(0, 0, 0, 0.8);
      padding: 20px;
      border-radius: 10px;
    }
  </style>
</head>

<body>
  <div id="info">加载中...</div>
  <div id="loading" style="display: none;"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "./node_modules/three/build/three.module.js",
        "three/examples/jsm/": "./node_modules/three/examples/jsm/",
        "3d-tiles-renderer": "./node_modules/3d-tiles-renderer/build/index.three.js",
        "dat.gui": "./node_modules/dat.gui/build/dat.gui.module.js"
      }
    }
  </script>

  <script type="module">
    // 动态导入所有模块
    (async () => {
      try {
        const THREE = await import('three');
        window.THREE = THREE;
        const { OrbitControls } = await import('three/examples/jsm/controls/OrbitControls.js');
        const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
        const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
        // const { TilesRenderer } = await import('3d-tiles-renderer');
        const { GUI } = await import('dat.gui');
        const { MTLLoader } = await import('three/examples/jsm/loaders/MTLLoader.js');
        const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
        const { TilesRenderer, GoogleTilesRenderer } = await import('https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.3.30/+esm');
        // const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
        // const { Loader3DTiles } = await import('three-loader-3dtiles');



        console.log('所有模块加载成功!');

        // ========== 1. 创建基础 3D 场景 ==========
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x87ceeb); // 天空蓝

        const camera = new THREE.PerspectiveCamera(
          75,
          window.innerWidth / window.innerHeight,
          0.1,
          100000
        );
        camera.position.set(0, 100, 200);

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true;
        document.body.appendChild(renderer.domElement);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;

        // 添加光源
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        scene.add(ambientLight);
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(100, 100, 50);
        directionalLight.castShadow = true;
        scene.add(directionalLight);

        // ========== 2. 创建图层分组 ==========
        const glbGroup = new THREE.Group();
        glbGroup.name = 'GLB_Group';
        scene.add(glbGroup);

        const objGroup = new THREE.Group();
        objGroup.name = 'OBJ_Group';
        scene.add(objGroup);

        // 注意:3D Tiles 不再使用单独的组,直接添加到场景

        // ========== 3. 读取 input.txt 文件 ==========
        async function readInputFile(url) {
          const response = await fetch(url);
          const text = await response.text();
          return text.split('\n')
            .map(line => line.trim())
            .filter(line => line && !line.startsWith('#'))
            .map(line => line.replace(/^\.\\/, '').replace(/\\/g, '/'));
        }

        // ========== 4. 加载 GLB 文件 ==========
        const gltfLoader = new GLTFLoader();
        let glbLoadedCount = 0;
        let glbTotalCount = 0;

        // 限制并发加载数量,避免服务器过载
        async function loadWithRetry(loader, path, maxRetries = 3, delay = 1000) {
          for (let i = 0; i < maxRetries; i++) {
            try {
              return await new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                  reject(new Error('加载超时'));
                }, 60000); // 60秒超时

                loader.load(
                  path,
                  (result) => {
                    clearTimeout(timeout);
                    resolve(result);
                  },
                  undefined,
                  (error) => {
                    clearTimeout(timeout);
                    reject(error);
                  }
                );
              });
            } catch (error) {
              if (i === maxRetries - 1) {
                throw error;
              }
              // 等待后重试
              await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
            }
          }
        }

        async function loadGLBFiles() {
          try {
            const glbPaths = await readInputFile('./assets/glb/input.txt');
            glbTotalCount = glbPaths.length;
            updateLoadingInfo(`正在加载 GLB 文件: 0/${glbTotalCount}`);

            // 限制并发数量为 5
            const concurrency = 5;
            for (let i = 0; i < glbPaths.length; i += concurrency) {
              const batch = glbPaths.slice(i, i + concurrency);

              await Promise.all(batch.map(async (path) => {
                const fullPath = `./assets/glb/${path}`;
                try {
                  const gltf = await loadWithRetry(gltfLoader, fullPath);
                  // 确保场景已更新矩阵
                  gltf.scene.updateMatrixWorld(true);
                  glbGroup.add(gltf.scene);
                  glbLoadedCount++;
                  updateLoadingInfo(`正在加载 GLB 文件: ${glbLoadedCount}/${glbTotalCount}`);

                  // 调试:输出第一个模型的位置
                  if (glbLoadedCount === 1 && gltf.scene.children.length > 0) {
                    const firstChild = gltf.scene.children[0];
                    if (firstChild.position) {
                      console.log('第一个 GLB 模型位置:', firstChild.position);
                    }
                  }
                } catch (error) {
                  console.warn(`加载 GLB 文件失败: ${fullPath}`, error);
                }
              }));

              // 批次之间稍作延迟
              if (i + concurrency < glbPaths.length) {
                await new Promise(resolve => setTimeout(resolve, 100));
              }
            }
            console.log(`GLB 文件加载完成: ${glbLoadedCount}/${glbTotalCount}`);
          } catch (error) {
            console.error('读取 GLB input.txt 失败:', error);
          }
        }

        // ========== 5. 加载 OBJ 文件 ==========
        const mtlLoader = new MTLLoader();
        const objLoader = new OBJLoader();
        let objLoadedCount = 0;
        let objTotalCount = 0;

        async function loadOBJFiles() {
          try {
            const objPaths = await readInputFile('./assets/objs-3dtiles/3dtiles/input.txt');
            objTotalCount = objPaths.length;
            updateLoadingInfo(`正在加载 OBJ 文件: 0/${objTotalCount}`);

            // 限制并发数量为 5
            const concurrency = 5;
            for (let i = 0; i < objPaths.length; i += concurrency) {
              const batch = objPaths.slice(i, i + concurrency);

              await Promise.all(batch.map(async (path) => {
                const fullPath = `./assets/objs-3dtiles/3dtiles/${path}`;
                const basePath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1);
                const fileName = path.substring(path.lastIndexOf('/') + 1);
                const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));

                try {
                  // 尝试加载对应的 MTL 文件
                  const mtlPath = `${basePath}${nameWithoutExt}.mtl`;
                  let materials = null;

                  try {
                    materials = await loadWithRetry(mtlLoader, mtlPath, 2, 500);
                    materials.preload();
                  } catch (mtlError) {
                    // MTL 文件不存在或加载失败,继续加载 OBJ
                  }

                  if (materials) {
                    objLoader.setMaterials(materials);
                  }

                  const obj = await loadWithRetry(objLoader, fullPath);
                  // 确保对象已更新矩阵
                  obj.updateMatrixWorld(true);
                  objGroup.add(obj);
                  objLoadedCount++;
                  updateLoadingInfo(`正在加载 OBJ 文件: ${objLoadedCount}/${objTotalCount}`);

                  // 调试:输出第一个模型的位置
                  if (objLoadedCount === 1 && obj.children.length > 0) {
                    const firstChild = obj.children[0];
                    if (firstChild.position) {
                      console.log('第一个 OBJ 模型位置:', firstChild.position);
                    }
                  }
                } catch (error) {
                  console.warn(`加载 OBJ 文件失败: ${fullPath}`, error);
                }
              }));

              // 批次之间稍作延迟
              if (i + concurrency < objPaths.length) {
                await new Promise(resolve => setTimeout(resolve, 100));
              }
            }
            console.log(`OBJ 文件加载完成: ${objLoadedCount}/${objTotalCount}`);
          } catch (error) {
            console.error('读取 OBJ input.txt 失败:', error);
          }
        }

        // ========== 6. 加载 3D Tiles ==========
        let tilesRenderer = null;
        let tilesGroup = null; // 用于存储实际的 3D Tiles 组

        async function load3DTiles() {
          try {
            // 使用本地 tileset.json 路径
            tilesRenderer = new TilesRenderer('./assets/objs-3dtiles/3dtiles/tileset.json');
            // tilesRenderer = new TilesRenderer('https://data1.mars3d.cn/3dtiles/qx-dyt/tileset.json');
            // tilesRenderer = new TilesRenderer('./assets/PC01JZ0001/tileset.json');

            // tilesRenderer.setLatLonToYUp(36.266494 * THREE.MathUtils.DEG2RAD, 120.460205 * THREE.MathUtils.DEG2RAD); // Tokyo Tower

            // 设置相机和渲染器
            tilesRenderer.setCamera(camera);
            tilesRenderer.setResolutionFromRenderer(camera, renderer);




            // const dracoLoader = new DRACOLoader();
            // dracoLoader.setDecoderPath('https://unpkg.com/three@0.181.0/examples/jsm/libs/draco/gltf/');

            // const loader = new GLTFLoader(tilesRenderer.manager);
            // loader.setDRACOLoader(dracoLoader);

            // tilesRenderer.manager.addHandler(/\.gltf$/, loader);




            // 监听加载完成事件
            tilesRenderer.addEventListener('load-tile-set', () => {
              console.log('3D Tiles 加载完成');

              // 确保 group 已添加到场景
              if (tilesRenderer.group && !scene.children.includes(tilesRenderer.group)) {
                tilesGroup = tilesRenderer.group;
                scene.add(tilesGroup);
                tilesGroup.visible = true;
                tilesGroup.userData.is3DTiles = true;
                console.log('3D Tiles group 在加载完成后添加到场景');
              }

              // 更新 group 信息
              if (tilesGroup) {
                tilesGroup.updateMatrixWorld(true);
                console.log('3D Tiles group 信息:', {
                  children: tilesGroup.children.length,
                  visible: tilesGroup.visible,
                  position: tilesGroup.position
                });
              }

              updateLoadingInfo('3D Tiles 已加载');
            });

            // 监听错误事件
            tilesRenderer.addEventListener('load-tile-error', (event) => {
              console.warn('3D Tiles 加载错误:', event);
            });

            // 监听瓦片加载事件
            tilesRenderer.addEventListener('load-tile', (event) => {
              console.log('3D Tiles 瓦片加载:', event);
            });

          } catch (error) {
            console.error('加载 3D Tiles 失败:', error);
          }
        }

        // ========== 7. 更新加载信息 ==========
        const infoDiv = document.getElementById('info');
        const loadingDiv = document.getElementById('loading');

        function updateLoadingInfo(text) {
          infoDiv.textContent = text;
        }

        // ========== 8. 图层控制 UI ==========
        const gui = new GUI();
        const layerControls = {
          showGLB: true,
          showOBJ: true,
          show3DTiles: true,
          resetCamera: function () {
            fitCameraToScene();
          }
        };

        // 调整相机到指定组的函数
        function fitCameraToGroup(group, groupName) {
          if (!group) {
            console.warn(`${groupName} 组不存在`);
            return;
          }

          // 对于 3D Tiles,确保使用正确的 group
          if (groupName === '3D Tiles') {
            const actualGroup = tilesGroup || (tilesRenderer && tilesRenderer.group);
            if (actualGroup && actualGroup !== group) {
              group = actualGroup;
              console.log('使用实际的 3D Tiles group');
            }
          }

          group.updateMatrixWorld(true);

          // 检查是否有子对象
          const hasChildren = group.children.length > 0;
          console.log(`${groupName} 组信息:`, {
            children: group.children.length,
            visible: group.visible,
            hasChildren: hasChildren
          });

          const box = new THREE.Box3();
          box.setFromObject(group);

          if (!box.isEmpty()) {
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            const maxDim = Math.max(size.x, size.y, size.z);

            // 如果尺寸为0或很小,使用默认距离
            const distance = maxDim > 0.1 ? maxDim * 1.5 : 100;

            camera.position.set(
              center.x + distance * 0.7,
              center.y + distance * 0.8,
              center.z + distance * 0.7
            );

            camera.lookAt(center);
            controls.target.copy(center);
            controls.update();

            console.log(`${groupName} 相机已调整到:`, center, '距离:', distance, '尺寸:', size);
          } else {
            console.warn(`${groupName} 边界框为空,使用默认位置`);
            // 如果边界框为空,尝试使用 tilesRenderer 的根位置
            if (groupName === '3D Tiles' && tilesRenderer && tilesRenderer.group) {
              const group = tilesRenderer.group;
              if (group.position) {
                const pos = group.position;
                camera.position.set(pos.x + 100, pos.y + 100, pos.z + 100);
                camera.lookAt(pos);
                controls.target.copy(pos);
                controls.update();
                console.log('使用 3D Tiles group 位置:', pos);
              }
            }
          }
        }

        gui.add(layerControls, 'showGLB').name('显示 GLB/GLTF').onChange((value) => {
          glbGroup.visible = value;
          console.log('GLB 可见性:', value, '子对象数量:', glbGroup.children.length);

          // 如果显示,调整相机到 GLB 组
          if (value) {
            setTimeout(() => {
              fitCameraToGroup(glbGroup, 'GLB');
            }, 100);
          }
        });

        gui.add(layerControls, 'showOBJ').name('显示 OBJ').onChange((value) => {
          objGroup.visible = value;
          console.log('OBJ 可见性:', value, '子对象数量:', objGroup.children.length);

          // 如果显示,调整相机到 OBJ 组
          if (value) {
            setTimeout(() => {
              fitCameraToGroup(objGroup, 'OBJ');
            }, 100);
          }
        });

        let tilesPositionUpdateInterval = null;
        const box = new THREE.Box3();

        // 更新 3D Tiles 位置的函数
        function updateTilesPosition() {
          if (!tilesRenderer || !tilesRenderer.group) {
            return;
          }

          // 获取 3D Tiles 的边界框
          if (tilesRenderer.getBounds && tilesRenderer.getBounds(box)) {
            // 获取边界框的中心并设置到 group 的位置
            const center = new THREE.Vector3();
            box.getCenter(center);

            // 将 group 的位置调整到包围盒的合适位置
            tilesRenderer.group.position.copy(center).multiplyScalar(-1);

            // 确保更新场景中的对象矩阵
            scene.updateMatrixWorld(true);
            camera.updateMatrixWorld();
          }
        }

        gui.add(layerControls, 'show3DTiles').name('显示 3D Tiles').onChange((value) => {
          // 优先使用 tilesGroup,如果没有则从 tilesRenderer 获取
          const group = tilesGroup || (tilesRenderer && tilesRenderer.group);

          if (group) {
            group.visible = value;
            console.log('3D Tiles 可见性:', value, '子对象数量:', group.children.length);

            // 如果显示,调整相机到 3D Tiles 并更新位置
            if (value) {
              // // 清除之前的定时器(如果存在)
              // if (tilesPositionUpdateInterval) {
              //   clearInterval(tilesPositionUpdateInterval);
              //   tilesPositionUpdateInterval = null;
              // }

              // // 立即更新一次位置
              // updateTilesPosition();

              // // 设置定时器,只在显示时更新位置
              // tilesPositionUpdateInterval = setInterval(() => {
              //   const currentGroup = tilesGroup || (tilesRenderer && tilesRenderer.group);
              //   if (currentGroup && currentGroup.visible && tilesRenderer) {
              //     updateTilesPosition();
              //     tilesRenderer.update();
              //   } else {
              //     // 如果不可见,清除定时器
              //     if (tilesPositionUpdateInterval) {
              //       clearInterval(tilesPositionUpdateInterval);
              //       tilesPositionUpdateInterval = null;
              //     }
              //   }
              // }, 100); // 每隔 100 毫秒检查一次

              // 使用边界球调整相机到 3D Tiles
              setTimeout(() => {
                if (tilesRenderer && tilesRenderer.getBoundingSphere) {
                  const sphere = new THREE.Sphere();
                  if (tilesRenderer.getBoundingSphere(sphere)) {
                    // 调整相机位置到合适的观察距离
                    const distance = sphere.radius * 2.5;
                    camera.position.set(
                      sphere.center.x + distance,
                      sphere.center.y + distance,
                      sphere.center.z + distance
                    );

                    
                    camera.lookAt(sphere.center);

                    // 更新控制器目标到模型中心
                    controls.target.copy(sphere.center);
                    controls.update();

                    console.log('相机已调整到 3D Tiles,中心:', sphere.center, '距离:', distance);
                  } else {
                    // 如果获取边界球失败,使用原来的方法
                    fitCameraToGroup(group, '3D Tiles');
                  }
                } else {
                  // 如果没有 getBoundingSphere 方法,使用原来的方法
                  fitCameraToGroup(group, '3D Tiles');
                }
              }, 300); // 增加延迟,确保瓦片已加载
            } else {
              // 如果隐藏,清除定时器
              if (tilesPositionUpdateInterval) {
                clearInterval(tilesPositionUpdateInterval);
                tilesPositionUpdateInterval = null;
              }
            }
          } else {
            console.warn('3D Tiles 未加载或 group 不存在');
            console.log('tilesRenderer:', tilesRenderer);
            console.log('tilesGroup:', tilesGroup);
            if (tilesRenderer) {
              console.log('tilesRenderer.group:', tilesRenderer.group);
            }
          }
        });









        gui.add(layerControls, 'resetCamera').name('重置相机位置');

        // ========== 9. 计算场景边界并调整相机 ==========
        function fitCameraToScene() {
          // 强制更新所有对象的矩阵
          scene.updateMatrixWorld(true);

          const box = new THREE.Box3();

          // 分别计算每个组的边界(只计算可见的组)
          const glbBox = new THREE.Box3();
          const objBox = new THREE.Box3();
          const tilesBox = new THREE.Box3();

          if (glbGroup.children.length > 0 && glbGroup.visible) {
            glbGroup.updateMatrixWorld(true);
            glbBox.setFromObject(glbGroup);
            console.log('GLB 边界:', glbBox);
          }

          if (objGroup.children.length > 0 && objGroup.visible) {
            objGroup.updateMatrixWorld(true);
            objBox.setFromObject(objGroup);
            console.log('OBJ 边界:', objBox);
          }

          // 3D Tiles 现在直接添加到场景
          const tilesGroupForBounds = tilesGroup || (tilesRenderer && tilesRenderer.group);
          if (tilesGroupForBounds && tilesGroupForBounds.visible) {
            tilesGroupForBounds.updateMatrixWorld(true);
            tilesBox.setFromObject(tilesGroupForBounds);
            console.log('3D Tiles 边界:', tilesBox);
          }

          // 合并所有可见组的边界框
          if (!glbBox.isEmpty()) box.union(glbBox);
          if (!objBox.isEmpty()) box.union(objBox);
          if (!tilesBox.isEmpty()) box.union(tilesBox);

          // 如果边界框无效,使用默认值
          if (!box.isEmpty()) {
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            const maxDim = Math.max(size.x, size.y, size.z);

            console.log('场景中心:', center);
            console.log('场景尺寸:', size);
            console.log('最大维度:', maxDim);

            // 计算合适的相机距离
            const distance = maxDim * 1.5; // 距离为最大维度的1.5倍

            // 设置相机位置,从上方和侧面观察
            camera.position.set(
              center.x + distance * 0.7,
              center.y + distance * 0.8,
              center.z + distance * 0.7
            );

            camera.lookAt(center);
            controls.target.copy(center);
            controls.update();

            console.log('相机位置:', camera.position);
            console.log('相机目标:', controls.target);
          } else {
            // 如果边界框为空,使用默认位置
            console.warn('场景边界框为空,使用默认相机位置');
            camera.position.set(0, 100, 200);
            camera.lookAt(0, 0, 0);
            controls.target.set(0, 0, 0);
            controls.update();
          }
        }

        // ========== 10. 开始加载所有模型 ==========
        async function loadAllModels() {
          loadingDiv.style.display = 'block';
          loadingDiv.textContent = '开始加载模型...';

          await Promise.all([
            loadGLBFiles(),
            loadOBJFiles(),
            load3DTiles()
          ]);

          loadingDiv.style.display = 'none';
          updateLoadingInfo(`加载完成 - GLB: ${glbLoadedCount}, OBJ: ${objLoadedCount}, 3D Tiles: ${tilesRenderer ? '已加载' : '未加载'}`);

          // 输出调试信息
          console.log('=== 加载统计 ===');
          console.log('GLB 组子对象数量:', glbGroup.children.length);
          console.log('OBJ 组子对象数量:', objGroup.children.length);
          console.log('3D Tiles group:', tilesRenderer && tilesRenderer.group ? '已加载' : '未加载');
          console.log('场景总子对象数量:', scene.children.length);

          // 延迟一下再调整相机,确保所有模型都已添加到场景
          setTimeout(() => {
            fitCameraToScene();
          }, 2000); // 增加到2秒,确保3D Tiles也有时间加载
        }

        loadAllModels();

        // ========== 11. 动画循环 ==========
        function animate() {
          requestAnimationFrame(animate);
          controls.update();
          camera.updateMatrixWorld();

          if (tilesRenderer) {
            tilesRenderer.update();
          }

          renderer.render(scene, camera);
        }
        animate();

        // ========== 12. 窗口大小调整 ==========
        window.addEventListener('resize', () => {
          camera.aspect = window.innerWidth / window.innerHeight;
          camera.updateProjectionMatrix();
          renderer.setSize(window.innerWidth, window.innerHeight);
          if (tilesRenderer) {
            tilesRenderer.setResolutionFromRenderer(camera, renderer);
          }
        });

      } catch (error) {
        console.error('❌ 模块导入错误:', error);
        console.error('错误详情:', error.message);
        document.getElementById('loading').textContent = '加载失败: ' + error.message;
      }
    })();




  </script>

</body>

</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值