<!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>
threejs编写加载本地的obj、gltf(glb)、3dtiles数据可视化页面
最新推荐文章于 2025-12-18 16:03:39 发布
2951

被折叠的 条评论
为什么被折叠?



