Three.js 开发实战教程(五):外部 3D 模型加载与优化实战

在前四篇教程中,我们用基础几何体、灯光、相机构建了完整 3D 场景,但实际开发中,复杂模型(如产品、角色、工业设备)需从 Blender、3ds Max 等建模软件导出,再导入 Three.js 使用。本篇将系统讲解外部模型加载的核心知识,通过 "静态模型加载""动画模型控制" 两大实战案例,结合官方规范与开发经验,补充模型优化技巧和常见问题解决方案,帮大家打通 "模型落地" 最后一公里。​

一、理论解析:外部模型加载核心基础​

Three.js 通过 "加载器(Loader)" 解析外部模型文件,不同格式对应不同加载器,需先明确格式特性与加载逻辑,避免开发走弯路。​

1. 主流 3D 模型格式对比(基于官方支持与实战场景)​

|-------|-------------|----------------------------------------|-------------------------------|------------------|-----------------------------------------------------------------|
| 格式​ | 文件后缀​ | 核心特性​ | 加载器​ | 适用场景​ | 优缺点​ |
| glTF​ | .gltf/.glb​ | Web3D 官方标准(Three.js 推荐),支持模型、纹理、动画、骨骼​ | GLTFLoader​ | 产品展示、游戏角色、交互场景​ | 优点:体积小(.glb 二进制压缩)、加载快、功能全;缺点:需建模软件正确导出(如 Blender 需启用 glTF 插件)​ |
| OBJ​ | .obj/.mtl​ | 文本格式,仅存几何数据,材质需单独.mtl 文件​ | OBJLoader + MTLLoader​ | 静态模型(家具、建筑构件)​ | 优点:兼容性极强(所有建模软件支持);缺点:不支持动画、体积大、需手动关联纹理​ |
| FBX​ | .fbx​ | Autodesk 格式,支持复杂骨骼动画​ | FBXLoader(需inflate.min.js依赖)​ | 专业动画(影视角色、机械关节)​ | 优点:动画功能强;缺点:体积大、加载慢、高版本兼容性差​ |

关键结论:优先选择 glTF 格式(尤其是二进制.glb),这是 Three.js 官方强烈推荐的 Web 端标准,兼顾体积(比 OBJ 小 50%+)、性能与功能完整性,是 90% 场景的最优解。​

2. 加载器通用工作流程(所有格式通用)​

无论加载哪种模型,Three.js 加载逻辑均遵循 "导入→配置→加载→适配→清理" 五步,缺一不可(尤其是资源清理,避免内存泄漏):​

  1. **导入加载器:**从three/addons/loaders/导入对应加载器(如GLTFLoader);
  2. **配置加载器:**设置进度回调、纹理基础路径(解决纹理丢失)、跨域等;
  3. **加载模型文件:**调用loader.load(),处理成功 / 失败 / 进度事件;
  4. **场景适配:**调整模型位置、缩放、旋转,开启阴影(如需),添加到场景;
  5. **资源清理:**组件销毁时,释放模型、几何体、材质、纹理的 GPU 资源。

二、实战 1:加载静态 glTF 模型(家具示例)​

先从简单的静态模型入手,加载一个带纹理的家具模型(如椅子),掌握基础加载流程与纹理适配。​

1. 前置准备​

  • 模型资源:Sketchfab下载免费 glTF 模型(筛选 "glTF" 格式,推荐 "Low Poly" 低面数模型,避免性能问题),将模型文件(如chair.glb)及配套纹理文件夹放入public/models/chair/(手动创建目录);
  • **依赖确认:**无需额外安装,Three.js 内置GLTFLoader,直接导入即可。

2. 完整代码实现(Vue3 + Vite)​

在src/components新建StaticModelLoader.vue,代码含详细注释,可直接运行:

javascript 复制代码
<template>
  <div class="model-scene">
    <!-- 3D场景容器 -->
    <div class="three-container" ref="sceneRef"></div>
    <!-- 加载进度提示 -->
    <div class="loading" v-if="isLoading">{{ loadProgress }}%</div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
// 导入glTF加载器(Three.js内置,无需额外安装)
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'

// 核心状态与DOM引用
const sceneRef = ref(null) // 场景容器DOM
const state = reactive({
  scene: null,       // 场景实例
  camera: null,      // 相机实例
  renderer: null,    // 渲染器实例
  controls: null,    // 轨道控制器实例
  model: null,       // 加载后的模型实例
  animationId: null, // 动画循环ID(用于清理)
  isLoading: true,   // 加载状态
  loadProgress: 0    // 加载进度
})

// 1. 初始化3D基础场景(场景、相机、渲染器)
const initBasicScene = () => {
  // 创建场景
  state.scene = new THREE.Scene()
  state.scene.background = new THREE.Color(0xf8f8f8) // 浅灰色背景

  // 获取容器宽高(避免渲染器尺寸异常)
  const { clientWidth: width, clientHeight: height } = sceneRef.value

  // 创建透视相机(适配家具观察视角)
  state.camera = new THREE.PerspectiveCamera(
    60,                // 视场角(60度,兼顾视野与细节)
    width / height,     // 宽高比(与容器一致,避免画面拉伸)
    0.1,               // 近裁剪面(小于此值的物体不渲染)
    100                // 远裁剪面(大于此值的物体不渲染)
  )
  state.camera.position.set(3, 2, 4) // 斜上方视角,完整显示家具

  // 创建渲染器(开启抗锯齿,画面更平滑)
  state.renderer = new THREE.WebGLRenderer({ antialias: true })
  state.renderer.setSize(width, height)
  state.renderer.shadowMap.enabled = true // 开启阴影渲染(如需)
  // 将渲染器的canvas元素添加到容器
  sceneRef.value.appendChild(state.renderer.domElement)

  // 添加轨道控制器(支持旋转、缩放、平移)
  state.controls = new OrbitControls(state.camera, state.renderer.domElement)
  state.controls.enableDamping = true // 开启阻尼,交互更平滑
  state.controls.dampingFactor = 0.05
  state.controls.target.set(0, 0.5, 0) // 控制器目标对准模型中心(家具高度约1单位)
}

// 2. 添加灯光(确保模型材质正常显示)
const addSceneLights = () => {
  // 环境光:基础照明,避免模型暗部过黑
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  state.scene.add(ambientLight)

  // 平行光:模拟太阳光,产生阴影,增强立体感
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
  directionalLight.position.set(5, 10, 7.5) // 光源位置(斜上方)
  directionalLight.castShadow = true // 开启灯光阴影
  // 调整阴影分辨率(1024*1024为平衡清晰度与性能的常用值)
  directionalLight.shadow.mapSize.set(1024, 1024)
  // 调整阴影相机范围(避免阴影模糊或不完整)
  directionalLight.shadow.camera.near = 0.5
  directionalLight.shadow.camera.far = 50
  state.scene.add(directionalLight)
}

// 3. 加载静态glTF模型(核心步骤)
const loadStaticModel = () => {
  // 创建glTF加载器实例
  const loader = new GLTFLoader()

  // 配置纹理基础路径(解决纹理丢失问题:模型中纹理路径基于此目录)
  loader.setResourcePath('/models/chair/textures/')

  // 加载进度回调(实时更新加载进度)
  loader.onProgress = (xhr) => {
    state.loadProgress = Math.round((xhr.loaded / xhr.total) * 100)
  }

  // 加载模型文件(路径基于public目录,无需写public前缀)
  loader.load(
    '/models/chair/chair.glb', // 模型文件路径
    (gltf) => {
      // 加载成功:gltf.scene包含模型所有Mesh和层级结构
      state.model = gltf.scene

      // 模型适配:避免位置偏移或大小异常
      state.model.position.set(0, 0, 0) // 模型居中
      state.model.scale.set(1, 1, 1)    // 若模型过大,可改为0.1;过小则改为10

      // 遍历模型,开启阴影(根据需求配置,静态模型可选)
      state.model.traverse((child) => {
        if (child.isMesh) { // 仅对Mesh对象处理
          child.castShadow = true  // 模型投射阴影
          child.receiveShadow = true // 模型接收其他物体阴影
        }
      })

      // 将模型添加到场景(未添加则不会被渲染)
      state.scene.add(state.model)

      // 加载完成:更新加载状态
      state.isLoading = false
    },
    undefined, // 进度回调(已通过onProgress单独配置)
    (error) => {
      // 加载失败:打印错误并提示用户
      console.error('静态模型加载失败:', error)
      state.isLoading = false
      alert('模型加载失败,请检查文件路径或模型格式是否为glTF')
    }
  )
}

// 4. 启动动画循环(渲染场景)
const startAnimationLoop = () => {
  const animate = () => {
    state.animationId = requestAnimationFrame(animate)

    // 开启阻尼后,必须每帧更新轨道控制器
    state.controls.update()

    // 渲染场景(将场景通过相机投射到页面)
    state.renderer.render(state.scene, state.camera)
  }
  animate()
}

// 5. 窗口自适应(避免窗口缩放导致场景拉伸)
const handleWindowResize = () => {
  if (!sceneRef.value || !state.renderer || !state.camera) return

  const { clientWidth: width, clientHeight: height } = sceneRef.value

  // 更新相机宽高比
  state.camera.aspect = width / height
  state.camera.updateProjectionMatrix() // 必须更新投影矩阵,否则视角会拉伸

  // 更新渲染器尺寸
  state.renderer.setSize(width, height)
}

// 6. 资源清理(组件销毁时调用,避免内存泄漏)
const cleanSceneResources = () => {
  // 停止动画循环
  cancelAnimationFrame(state.animationId)

  // 销毁渲染器与控制器
  state.renderer.dispose()
  state.controls.dispose()

  // 移除窗口resize监听
  window.removeEventListener('resize', handleWindowResize)

  // 释放模型资源(GPU资源需手动释放,否则会内存泄漏)
  if (state.model) {
    state.scene.remove(state.model)
    // 遍历模型,释放几何体、材质、纹理
    state.model.traverse((child) => {
      if (child.isMesh) {
        child.geometry.dispose() // 释放几何体
        if (child.material.map) { // 释放纹理(若有)
          child.material.map.dispose()
        }
        child.material.dispose() // 释放材质
      }
    })
  }
}

// Vue生命周期:初始化场景
onMounted(() => {
  initBasicScene()
  addSceneLights()
  loadStaticModel()
  startAnimationLoop()
  window.addEventListener('resize', handleWindowResize)
})

// Vue生命周期:清理资源
onUnmounted(() => {
  cleanSceneResources()
})
</script>

<style scoped>
.model-scene {
  position: relative;
  width: 100vw;
  height: 80vh;
  margin-top: 20px;
}

.three-container {
  width: 100%;
  height: 100%;
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 20px;
  color: #333;
  background-color: rgba(255, 255, 255, 0.8);
  padding: 10px 20px;
  border-radius: 4px;
  z-index: 100; // 确保加载提示在场景上方
}
</style>

3. 运行效果​

启动项目后,将看到:​

  • 浅灰色背景下的家具模型(如椅子),支持鼠标旋转、缩放、平移;
  • 加载过程中显示进度百分比;
  • 窗口缩放时,场景自动适配,无拉伸变形;
  • 模型投射清晰阴影,立体感强。

三、实战 2:加载带骨骼动画的 glTF 模型(人物示例)​

glTF 的核心优势是支持骨骼动画,下面加载一个带 "行走""挥手" 等多动画的人物模型,实现动画播放、暂停、切换功能。​

1. 前置准备​

  • 动画模型资源:Mixamo下载免费人物动画(选择 "glTF" 格式,可同时下载多个动画并通过 Blender 合并为一个.glb文件),将模型文件(如character.glb)放入public/models/character/;
  • **核心依赖:**使用 Three.js 内置的AnimationMixer(动画混合器)管理模型动画,无需额外安装,直接通过THREE.AnimationMixer调用。

2. 完整代码实现(Vue3 + Vite)​

在src/components新建AnimatedModelLoader.vue,核心新增动画控制逻辑:

javascript 复制代码
<template>
  <div class="character-scene">
    <div class="three-container" ref="sceneRef"></div>
    <!-- 加载提示 -->
    <div class="loading" v-if="isLoading">{{ loadProgress }}%</div>
    <!-- 动画控制面板(加载完成后显示) -->
    <div class="control-panel" v-if="!isLoading && animNames.length">
      <label>选择动画:</label>
      <select v-model="selectedAnim" @change="switchAnimation">
        <option v-for="name in animNames" :key="name" :value="name">
          {{ name }}
        </option>
      </select>
      <button @click="togglePlayPause">
        {{ isPlaying ? '暂停动画' : '播放动画' }}
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'

// 核心状态与DOM引用
const sceneRef = ref(null)
const selectedAnim = ref('') // 当前选中的动画名称
const isPlaying = ref(true)  // 动画播放状态
const animNames = ref([])    // 所有动画名称列表(用于下拉框)

const state = reactive({
  scene: null,
  camera: null,
  renderer: null,
  controls: null,
  model: null,
  mixer: null,         // 动画混合器(管理所有动画)
  animationClips: [],  // 所有动画片段(从模型中提取)
  animationId: null,
  isLoading: true,
  loadProgress: 0
})

// 1. 初始化基础场景(适配人物视角)
const initBasicScene = () => {
  state.scene = new THREE.Scene()
  state.scene.background = new THREE.Color(0xf8f8f8)

  const { clientWidth: width, clientHeight: height } = sceneRef.value

  // 相机:模拟人眼高度(约1.5单位),正前方观察人物
  state.camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100)
  state.camera.position.set(0, 1.5, 3)

  // 渲染器与控制器(同静态模型,略)
  state.renderer = new THREE.WebGLRenderer({ antialias: true })
  state.renderer.setSize(width, height)
  state.renderer.shadowMap.enabled = true
  sceneRef.value.appendChild(state.renderer.domElement)

  state.controls = new OrbitControls(state.camera, state.renderer.domElement)
  state.controls.enableDamping = true
  state.controls.target.set(0, 1, 0) // 对准人物腰部
}

// 2. 添加灯光(同静态模型,略)
const addSceneLights = () => {
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  state.scene.add(ambientLight)

  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
  directionalLight.position.set(2, 10, 5)
  directionalLight.castShadow = true
  directionalLight.shadow.mapSize.set(1024, 1024)
  state.scene.add(directionalLight)
}

// 3. 加载带骨骼动画的glTF模型(核心新增动画逻辑)
const loadAnimatedModel = () => {
  const loader = new GLTFLoader()

  loader.onProgress = (xhr) => {
    state.loadProgress = Math.round((xhr.loaded / xhr.total) * 100)
  }

  loader.load(
    '/models/character/character.glb',
    (gltf) => {
      state.model = gltf.scene
      // 提取模型中的所有动画片段
      state.animationClips = gltf.animations
      state.isLoading = false

      // 模型适配(同静态模型,略)
      state.model.position.set(0, 0, 0)
      state.model.scale.set(1, 1, 1)
      state.model.traverse((child) => {
        if (child.isMesh) {
          child.castShadow = true
          child.receiveShadow = true
        }
      })

      // 初始化动画混合器(关键:关联模型)
      state.mixer = new THREE.AnimationMixer(state.model)

      // 提取动画名称,填充下拉框
      animNames.value = state.animationClips.map(clip => clip.name)
      if (animNames.value.length) {
        selectedAnim.value = animNames.value[0] // 默认选中第一个动画
        switchAnimation() // 自动播放第一个动画
      }

      state.scene.add(state.model)
    },
    undefined,
    (error) => {
      console.error('动画模型加载失败:', error)
      state.isLoading = false
    }
  )
}

// 4. 切换动画(核心:停止当前动画,播放选中动画)
const switchAnimation = () => {
  if (!state.mixer || !state.animationClips.length) return

  // 停止当前所有动画
  state.mixer.stopAllAction()

  // 找到选中的动画片段并播放
  const targetClip = state.animationClips.find(
    clip => clip.name === selectedAnim.value
  )
  if (targetClip) {
    const action = state.mixer.clipAction(targetClip)
    action.loop = THREE.LoopRepeat // 循环播放(可选:THREE.LoopOnce单次播放)
    action.play()
    isPlaying.value = true // 重置为播放状态
  }
}

// 5. 播放/暂停切换
const togglePlayPause = () => {
  if (!state.mixer) return
  isPlaying.value = !isPlaying.value
  // 调用混合器的resume/pause方法控制播放状态
  isPlaying.value ? state.mixer.resume() : state.mixer.pause()
}

// 6. 动画循环(新增时间差计算,用于动画更新)
const startAnimationLoop = () => {
  const clock = new THREE.Clock() // Three.js时间管理器,计算帧间隔

  const animate = () => {
    state.animationId = requestAnimationFrame(animate)

    // 关键:更新动画混合器(需传入帧间隔时间)
    if (state.mixer && isPlaying.value) {
      const deltaTime = clock.getDelta() // 获取两帧之间的时间差(秒)
      state.mixer.update(deltaTime)
    }

    state.controls.update()
    state.renderer.render(state.scene, state.camera)
  }
  animate()
}

// 7. 窗口自适应与资源清理(同静态模型,略)
const handleWindowResize = () => { /* 同前序代码 */ }
const cleanSceneResources = () => { /* 同前序代码,需额外销毁mixer */ }

// 生命周期(同前序代码)
onMounted(() => {
  initBasicScene()
  addSceneLights()
  loadAnimatedModel()
  startAnimationLoop()
  window.addEventListener('resize', handleWindowResize)
})

onUnmounted(() => {
  cleanSceneResources()
})
</script>

<style scoped>
/* 基础样式同静态模型,新增控制面板样式 */
.control-panel {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
  padding: 10px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 4px;
}

select, button {
  padding: 6px 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

button {
  background: #409eff;
  color: white;
  border: none;
}
</style>

3. 运行效果​

  • 人物模型居中显示,支持视角控制;
  • 下拉框可选择不同动画(如 "Walk""Wave"),点击按钮切换播放 / 暂停;
  • 动画播放流畅,人物骨骼运动自然(如行走时四肢协调);
  • 模型投射阴影,与场景融合度高。

四、优化技巧:提升模型加载与渲染性能​

实际项目中,模型体积过大或数量过多会导致加载慢、卡顿,需从 "模型预处理→加载→渲染" 全链路优化。​

1. 模型预处理优化(建模端 + 工具)​

  • 格式转换与压缩:​
    • glTF-Transform将 OBJ/FBX 转为 glb,并压缩体积:

      bash 复制代码
      # 安装工具
      npm install --global @gltf-transform/cli
      # 转换并压缩(减少50%+体积)
      gltf-transform optimize input.obj output.glb --compress
    • 启用 Draco 压缩(Three.js 支持):在 Blender 导出时勾选 "Draco 压缩",进一步减小几何体体积。

  • 几何简化:​
    • Blender 中添加 "Decimate Modifier"(简化修改器),降低多边形数量(如将 10 万面模型简化为 2 万面,视觉差异小);
    • 避免不必要的细节(如家具背面无需高面数)。
  • 纹理优化:​
    • 尺寸:将纹理调整为 2 的幂次方(如 512×512、1024×1024),避免非标准尺寸导致 GPU 额外计算;
    • 格式:用 WebP 替代 PNG/JPG,压缩率提升 30%+,质量接近;
    • 合并:用纹理图集(Texture Atlas)将多个小纹理合并为一张,减少纹理切换开销。

2. 加载优化(前端端)​

  • 懒加载: 仅在模型进入视口时加载(用 Intersection Observer API):

    javascript 复制代码
    ​// 示例:监听模型容器可见性
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting && !state.isLoading) {
          loadAnimatedModel() // 进入视口时加载
          observer.unobserve(entry.target)
        }
      })
    })
    observer.observe(sceneRef.value)
    
    ​
  • 预加载关键模型: 首屏模型提前加载,非首屏模型延迟加载:

    javascript 复制代码
    // 应用启动时预加载首屏模型
    const preloadModel = (url) => {
      return new Promise((resolve) => {
        const loader = new GLTFLoader()
        loader.load(url, (gltf) => resolve(gltf))
      })
    }
    // 预加载并缓存
    preloadModel('/models/main.glb').then(gltf => {
      modelCache.set('main', gltf)
    })
  • **分块加载:**大型场景(如城市模型)拆分为多个子模型,根据相机位置动态加载(用 Three.js 的BufferGeometryUtils拆分)。

3. 渲染优化(前端端)​

  • 层级细节(LOD): 根据相机距离切换不同细节的模型,远距显示低面数模型:

    javascript 复制代码
    const lod = new THREE.LOD()
    // 0-10米:高细节模型
    lod.addLevel(highDetailModel, 0)
    // 10-30米:中细节模型
    lod.addLevel(mediumDetailModel, 10)
    // 30米以上:低细节模型
    lod.addLevel(lowDetailModel, 30)
    state.scene.add(lod)
  • 实例化渲染: 重复模型(如树木、路灯)用InstancedMesh,减少绘制调用(1 次调用渲染 1000 个树木,而非 1000 次调用):

    javascript 复制代码
    // 示例:创建1000个重复立方体
    const geometry = new THREE.BoxGeometry(1, 1, 1)
    const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
    const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000)
    
    // 设置每个实例的位置
    const matrix = new THREE.Matrix4()
    for (let i = 0; i < 1000; i++) {
      matrix.setPosition(Math.random()*100, 0, Math.random()*100)
      instancedMesh.setMatrixAt(i, matrix)
    }
    state.scene.add(instancedMesh)
  • 阴影优化:​

    • 降低阴影分辨率(如 512×512,而非 2048×2048);
    • 限制阴影距离(directionalLight.shadow.camera.far = 30);
    • 静态模型的阴影烘焙到纹理(Blender 中烘焙,前端直接使用纹理,无需实时计算阴影)。

五、常见问题与解决方案(基于实战经验)​

1. 模型加载失败​

  • **路径问题:**检查路径是否基于public目录(如/models/chair.glb,而非../public/models/chair.glb);
  • 跨域问题: 开发环境在vite.config.js配置跨域,生产环境确保服务器允许 CORS:

    javascript 复制代码
    // vite.config.js
    export default defineConfig({
      server: {
        headers: { 'Access-Control-Allow-Origin': '*' }
      }
    })
  • 文件损坏: 重新导出模型,导出时勾选 "嵌入纹理"(避免纹理路径依赖),或用glTF Validator检查模型完整性;

  • 加载器依赖缺失: 如 FBXLoader 需额外导入inflate.min.js:

    javascript 复制代码
    import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'
    import inflate from 'three/addons/libs/inflate.min.js'

2. 纹理丢失或显示异常​

  • **纹理路径错误:**用loader.setResourcePath('纹理目录路径')指定基础路径;

  • **UV 映射问题:**Blender 中检查模型是否有 UV 展开(无 UV 会导致纹理无法显示,需重新展开 UV);

  • 材质不兼容: 部分建模软件导出的材质(如 Blender 的 Principled BSDF)Three.js 支持有限,需手动替换材质:

    javascript 复制代码
    // 遍历模型,替换为Three.js支持的材质
    state.model.traverse((child) => {
      if (child.isMesh) {
        child.material = new THREE.MeshStandardMaterial({
          map: child.material.map, // 保留原纹理
          color: child.material.color // 保留原颜色
        })
      }
    })

3. 动画不播放或异常

  • **混合器未更新:**确保动画循环中调用mixer.update(deltaTime),且传入正确的时间差;
  • **动画片段为空:**检查gltf.animations是否有数据(导出时需选择 "包含动画");
  • **骨骼权重问题:**建模软件中检查骨骼权重(无权重会导致动画无效果,需重新绑定骨骼);
  • **循环模式错误:**设置action.loop = THREE.LoopRepeat(循环)或THREE.LoopOnce(单次),避免默认的THREE.LoopPingPong(来回播放)不符合预期。
  1. 性能卡顿​
  • **绘制调用过多:**用state.renderer.info.render.calls查看调用次数,超过 1000 需优化(合并模型、用实例化渲染);
  • **三角形数量过大:**用state.renderer.info.render.triangles查看面数,超过 10 万需简化几何体;
  • **纹理内存过高:**用state.renderer.info.memory.textures查看纹理数量,压缩纹理或降低尺寸。

六、专栏预告​

下一篇将讲解 Three.js 的物理引擎集成,内容包括:​

  • 常见物理引擎(Ammo.js、Cannon.js)的选型与集成;
  • 实现碰撞检测(如人物碰撞墙壁、物体落地);
  • 模拟重力、摩擦力、弹力等真实物理效果;
  • 实战:创建可交互的物理场景(如小球弹跳、箱子堆叠)。
相关推荐
杀生丸学AI2 小时前
【无标题】SceneSplat:基于视觉-语言预训练的3DGS场景理解
3d·aigc·slam·语义分割·三维重建·视觉大模型·空间智能
Zuckjet_5 小时前
开启 3D 之旅 - 你的第一个 WebGL 三角形
前端·javascript·3d·webgl
2401_863801465 小时前
探索 12 种 3D 文件格式:综合指南
前端·3d
珍宝商店7 小时前
前端老旧项目全面性能优化指南与面试攻略
前端·面试·性能优化
bitbitDown7 小时前
四年前端分享给你的高效开发工具库
前端·javascript·vue.js
YAY_tyy7 小时前
【JavaScript 性能优化实战】第六篇:性能监控与自动化优化
javascript·性能优化·自动化
gnip8 小时前
实现AI对话光标跟随效果
前端·javascript
脑花儿9 小时前
ABAP SMW0下载Excel模板并填充&&剪切板方式粘贴
java·前端·数据库
闭着眼睛学算法9 小时前
【华为OD机考正在更新】2025年双机位A卷真题【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】
java·c语言·javascript·c++·python·算法·华为od