javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > Three.js导入外部模型GLTF/GLB/FBX

Three.js导入外部模型之GLTF/GLB/FBX详细流程指南

作者:EndingCoder

这篇文章主要介绍了Three.js导入外部模型之GLTF/GLB/FBX的相关资料,涵盖GLTFLoader使用、进度控制、异常处理、可访问性优化及性能部署,文中通过代码介绍的非常详细,需要的朋友可以参考下

引言

在 Three.js 项目中,外部模型的导入是创建复杂 3D 场景的重要环节。GLTF、GLB 和 FBX 是主流的 3D 模型格式,广泛应用于游戏、建筑可视化和虚拟现实等领域。本文将详细对比这三种格式的特点,深入讲解如何使用 GLTFLoader 加载 GLTF/GLB 模型,并探讨模型加载进度控制与异常处理的最佳实践。通过一个交互式城市建筑展示案例,展示如何加载外部模型并实现动态交互。项目基于 Vite、TypeScript 和 Tailwind CSS,支持 ES Modules,确保响应式布局,遵循 WCAG 2.1 可访问性标准。本文适合希望掌握 Three.js 外部模型导入的开发者。

通过本篇文章,你将学会:

导入外部模型

1. 三种主流模型格式对比

以下是 GLTF、GLB 和 FBX 格式的详细对比:

2. 使用GLTFLoader加载模型

GLTFLoader 是 Three.js 官方提供的加载器,支持 GLTF 和 GLB 格式,集成简单,性能优异。

3. 模型加载进度控制与异常处理

4. 可访问性要求

为确保 3D 场景对残障用户友好,遵循 WCAG 2.1:

5. 性能监控

实践案例:交互式城市建筑展示

我们将构建一个交互式城市建筑展示场景,使用 GLTFLoader 加载 GLB 模型(建筑),结合 OrbitControlsRaycaster 实现点击高亮和模型切换功能,支持加载进度显示和异常处理。项目基于 Vite、TypeScript 和 Tailwind CSS。

1. 项目结构

threejs-city-showcase/
├── index.html
├── src/
│   ├── index.css
│   ├── main.ts
│   ├── assets/
│   │   ├── building.glb
│   │   ├── building-texture.jpg
│   ├── tests/
│   │   ├── loader.test.ts
└── package.json

2. 环境搭建

初始化 Vite 项目

npm create vite@latest threejs-city-showcase -- --template vanilla-ts
cd threejs-city-showcase
npm install three@0.157.0 @types/three@0.157.0 tailwindcss postcss autoprefixer stats.js
npx tailwindcss init

配置 TypeScript (tsconfig.json):

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

配置 Tailwind CSS (tailwind.config.js):

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{html,js,ts}'],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#1f2937',
        accent: '#22c55e',
      },
    },
  },
  plugins: [],
};

CSS (src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

.dark {
  @apply bg-gray-900 text-white;
}

#canvas {
  @apply w-full max-w-4xl mx-auto h-[600px] rounded-lg shadow-lg;
}

.controls {
  @apply p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md mt-4 text-center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

.progress-bar {
  @apply w-full h-4 bg-gray-200 rounded overflow-hidden;
}

.progress-fill {
  @apply h-4 bg-primary transition-all duration-300;
}

3. 初始化场景与模型加载

src/main.ts:

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import Stats from 'stats.js';
import './index.css';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const canvas = renderer.domElement;
canvas.setAttribute('aria-label', '3D 城市建筑展示');
canvas.setAttribute('tabindex', '0');
document.getElementById('canvas')!.appendChild(canvas);

// 可访问性:屏幕阅读器描述
const sceneDesc = document.createElement('div');
sceneDesc.id = 'scene-desc';
sceneDesc.className = 'sr-only';
sceneDesc.setAttribute('aria-live', 'polite');
sceneDesc.textContent = '3D 城市建筑展示已加载';
document.body.appendChild(sceneDesc);

// 进度条
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
const progressFill = document.createElement('div');
progressFill.className = 'progress-fill';
progressFill.style.width = '0%';
progressBar.appendChild(progressFill);
document.querySelector('.controls')!.appendChild(progressBar);

// 添加地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.name = '地面';
scene.add(ground);

// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 0.5, 100);
pointLight.position.set(5, 5, 5);
scene.add(pointLight);

// 初始化控制器
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 5;
controls.maxDistance = 50;

// 初始化 Raycaster
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const highlightMaterial = new THREE.MeshStandardMaterial({ color: 0x22c55e });
let originalMaterials = new Map();

// 加载模型
const loader = new GLTFLoader();
let currentModel: THREE.Group | null = null;
function loadModel(path: string, position: THREE.Vector3) {
  loader.load(
    path,
    (gltf) => {
      if (currentModel) scene.remove(currentModel);
      currentModel = gltf.scene;
      currentModel.position.copy(position);
      currentModel.scale.set(0.1, 0.1, 0.1); // 假设模型需要缩放
      currentModel.traverse((child) => {
        if (child instanceof THREE.Mesh) {
          originalMaterials.set(child, child.material);
          child.castShadow = true;
          child.receiveShadow = true;
        }
      });
      scene.add(currentModel);
      progressBar.style.display = 'none';
      sceneDesc.textContent = `模型 ${path} 已加载`;
    },
    (progress) => {
      progressFill.style.width = `${(progress.loaded / progress.total) * 100}%`;
    },
    (error) => {
      console.error('加载错误:', error);
      progressBar.style.display = 'none';
      sceneDesc.textContent = '模型加载失败,请检查文件';
    }
  );
}
loadModel('/src/assets/building.glb', new THREE.Vector3(0, 0, 0));

// 性能监控
const stats = new Stats();
stats.showPanel(0); // 显示 FPS
document.body.appendChild(stats.dom);

// 渲染循环
function animate() {
  stats.begin();
  controls.update();
  renderer.render(scene, camera);
  stats.end();
  requestAnimationFrame(animate);
}
animate();

// 鼠标交互:点击高亮
canvas.addEventListener('click', (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(currentModel ? currentModel.children : []);
  currentModel?.traverse((child) => {
    if (child instanceof THREE.Mesh) child.material = originalMaterials.get(child);
  });
  if (intersects.length > 0) {
    const target = intersects[0].object as THREE.Mesh;
    target.material = highlightMaterial;
    sceneDesc.textContent = `点击了模型部分: ${target.name || '未命名'}`;
  }
});

// 键盘控制:切换模型
canvas.addEventListener('keydown', (e: KeyboardEvent) => {
  if (e.key === '1') {
    loadModel('/src/assets/building.glb', new THREE.Vector3(0, 0, 0));
    progressBar.style.display = 'block';
    sceneDesc.textContent = '正在加载模型 building.glb';
  }
});

// 响应式调整
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

// 交互控件:重新加载模型
const reloadButton = document.createElement('button');
reloadButton.className = 'p-2 bg-primary text-white rounded';
reloadButton.textContent = '重新加载模型';
reloadButton.setAttribute('aria-label', '重新加载模型');
document.querySelector('.controls')!.appendChild(reloadButton);
reloadButton.addEventListener('click', () => {
  loadModel('/src/assets/building.glb', new THREE.Vector3(0, 0, 0));
  progressBar.style.display = 'block';
  sceneDesc.textContent = '正在加载模型 building.glb';
});

4. HTML 结构

index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Three.js 城市建筑展示</title>
  <link rel="stylesheet" href="./src/index.css" rel="external nofollow"  />
</head>
<body class="bg-gray-100 dark:bg-gray-900">
  <div class="min-h-screen p-4">
    <h1 class="text-2xl md:text-3xl font-bold text-center text-gray-900 dark:text-white mb-4">
      Three.js 城市建筑展示
    </h1>
    <div id="canvas" class="h-[600px] w-full max-w-4xl mx-auto rounded-lg shadow"></div>
    <div class="controls">
      <p class="text-gray-900 dark:text-white">使用鼠标旋转、缩放,点击高亮模型,或按数字键 1 切换模型</p>
    </div>
  </div>
  <script type="module" src="./src/main.ts"></script>
</body>
</html>

模型文件

5. 响应式适配

使用 Tailwind CSS 确保画布和控件自适应:

#canvas {
  @apply h-[600px] sm:h-[700px] md:h-[800px] w-full max-w-4xl mx-auto;
}

.controls {
  @apply p-2 sm:p-4;
}

6. 可访问性优化

7. 性能测试

src/tests/loader.test.ts:

import Benchmark from 'benchmark';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import Stats from 'stats.js';

async function runBenchmark() {
  const suite = new Benchmark.Suite();
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  const loader = new GLTFLoader();
  const stats = new Stats();

  suite
    .add('GLTFLoader Load', async () => {
      stats.begin();
      await new Promise((resolve) => {
        loader.load('/src/assets/building.glb', (gltf) => {
          scene.add(gltf.scene);
          renderer.render(scene, camera);
          stats.end();
          resolve(null);
        });
      });
    })
    .on('cycle', (event: any) => {
      console.log(String(event.target));
    })
    .run({ async: true });
}

runBenchmark();

测试结果

测试工具

扩展功能

1. 动态切换模型

添加控件切换不同模型:

const modelSelect = document.createElement('select');
modelSelect.className = 'p-2 bg-white dark:bg-gray-800 rounded';
modelSelect.setAttribute('aria-label', '选择模型');
['building.glb', 'building2.glb'].forEach((model) => {
  const option = document.createElement('option');
  option.value = model;
  option.textContent = model;
  modelSelect.appendChild(option);
});
document.querySelector('.controls')!.appendChild(modelSelect);
modelSelect.addEventListener('change', () => {
  loadModel(`/src/assets/${modelSelect.value}`, new THREE.Vector3(0, 0, 0));
  progressBar.style.display = 'block';
  sceneDesc.textContent = `正在加载模型 ${modelSelect.value}`;
});

2. DRACO 压缩支持

集成 DRACO 压缩以优化大模型加载:

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
loader.setDRACOLoader(dracoLoader);

常见问题与解决方案

1. 模型加载失败

问题:模型未显示或报错。
解决方案

2. 材质显示异常

问题:模型材质丢失或显示不正确。
解决方案

3. 性能瓶颈

问题:大模型导致加载慢或卡顿。
解决方案

4. 可访问性问题

问题:屏幕阅读器无法识别加载状态。
解决方案

部署与优化

1. 本地开发

运行本地服务器:

npm run dev

2. 生产部署(阿里云)

部署到阿里云 OSS

3. 优化建议

注意事项

总结与练习题

总结

本文通过交互式城市建筑展示案例,详细解析了 GLTF、GLB 和 FBX 格式的优缺点,展示了如何使用 GLTFLoader 加载模型,并实现进度控制和异常处理。结合 Vite、TypeScript 和 Tailwind CSS,场景实现了动态交互、可访问性优化和性能监控。测试结果表明加载效率高,WCAG 2.1 合规性确保了包容性。本案例为开发者提供了外部模型导入的实践基础。

到此这篇关于Three.js导入外部模型之GLTF/GLB/FBX的文章就介绍到这了,更多相关Three.js导入外部模型GLTF/GLB/FBX内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文