Vue.js - 集成Three.js构建三维可视化场景教程4(加载FBX模型)
作者:hangge | 2026-05-19 08:42
FBX 格式的模型在工业/旧导出管线里很常见,但由于 FBX 文件通常较大,若用于 Web 还是推荐将 FBX 转为 glTF/glb(更适合 Web、体积小且加载友好)。当然 Three.js 对 FBX 格式的模型也是提供了支持,下面通过样例进行演示。


四、加载 FBX 模型
1,模型准备
我们将需要加载的 FBX 模型文件放置项目的 public/models/fbx 目录下。

2,样例代码
下面代码我使用 Three.js 内置的 FBXLoader 加载这个 glb 格式的房屋模型。
<template>
<div class="three-container" ref="container"></div>
</template>
<script>
// 核心 Three.js
import * as THREE from 'three'
// 控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// FBX 加载器
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
// 如需支持 TGA 贴图(如果 FBX 的贴图是 .tga),取消下面注释并安装支持库
// import { TGALoader } from 'three/examples/jsm/loaders/TGALoader.js'
export default {
name: 'ThreeFBXScene',
props: {
enableControls: { type: Boolean, default: true },
width: { type: Number, default: null },
height: { type: Number, default: null }
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
rafId: null,
model: null,
mixers: [], // 存放 AnimationMixer(若模型包含动画)
clock: new THREE.Clock()
}
},
mounted() {
this.$nextTick(() => {
this.initThree()
// 绑定 this 到 animate,确保 requestAnimationFrame 中的上下文正确
this.animate = this.animate.bind(this)
this.start()
window.addEventListener('resize', this.onWindowResize, { passive: true })
document.addEventListener('visibilitychange', this.onVisibilityChange)
})
},
beforeDestroy() {
this.stop()
window.removeEventListener('resize', this.onWindowResize)
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.disposeThree()
},
methods: {
getWidth() {
return this.width || this.$refs.container.clientWidth || window.innerWidth
},
getHeight() {
return this.height || this.$refs.container.clientHeight || window.innerHeight
},
initThree() {
const container = this.$refs.container
// 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x111111)
// 相机
const aspect = this.getWidth() / this.getHeight()
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000)
this.camera.position.set(0, 20, 20)
// 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
this.renderer.setSize(this.getWidth(), this.getHeight())
this.renderer.outputEncoding = THREE.sRGBEncoding
this.renderer.shadowMap.enabled = true
container.appendChild(this.renderer.domElement)
// 光源(调整以让模型不显得很暗)
this.scene.add(new THREE.AmbientLight(0xffffff, 1.0)) // 环境光(核心)
const dir = new THREE.DirectionalLight(0xffffff, 2.0) // 主光
dir.position.set(10, 20, 10)
dir.castShadow = true
dir.shadow.mapSize.width = 1024
dir.shadow.mapSize.height = 1024
this.scene.add(dir)
const back = new THREE.DirectionalLight(0xffffff, 0.5) // 侧逆光(提轮廓)
back.position.set(-10, 10, -10)
this.scene.add(back)
// 地面
const groundGeo = new THREE.PlaneGeometry(15, 15)
const groundMat = new THREE.MeshStandardMaterial({ color: 0xcccccc })
const ground = new THREE.Mesh(groundGeo, groundMat)
ground.rotation.x = -Math.PI / 2
ground.position.y = -1.0
ground.receiveShadow = true
this.scene.add(ground)
// 控制器
if (this.enableControls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
}
// 加载 FBX 模型
this.loadFBXModel('/models/fbx/house.fbx')
},
loadFBXModel(url) {
const loader = new FBXLoader()
// 如果你有外部贴图且是 .tga,可以提供 TGALoader:
// const tgaLoader = new TGALoader()
// loader.setTexturePath('/models/textures/')
// loader.setResourcePath('/models/')
loader.load(
url,
fbx => {
// FBX 可能自带多个层级
this.model = fbx
// 若包含动画,创建 AnimationMixer 并播放第一个 animation
if (fbx.animations && fbx.animations.length) {
const mixer = new THREE.AnimationMixer(fbx)
this.mixers.push(mixer)
// 默认播放全部 clip(或选择特定 clip)
fbx.animations.forEach(clip => {
const action = mixer.clipAction(clip)
action.play()
})
}
// 遍历网格,启用阴影并调整贴图编码(如果有贴图)
fbx.traverse(node => {
if (node.isMesh) {
node.castShadow = true
node.receiveShadow = true
// 贴图色彩空间处理:贴图通常应为 sRGB
if (node.material) {
const mat = node.material
// 处理数组材质或单一材质
const mats = Array.isArray(mat) ? mat : [mat]
mats.forEach(m => {
if (m.map) {
// 将贴图设置为 sRGB(颜色贴图)
m.map.encoding = THREE.sRGBEncoding
m.needsUpdate = true
}
// 某些 FBX 导出会把法线当作颜色贴图,需要识别并设置正确
if (m.normalMap) {
m.normalMap.encoding = THREE.LinearEncoding
m.needsUpdate = true
}
})
}
}
})
// 根据模型尺寸自适应缩放与居中
const box = new THREE.Box3().setFromObject(fbx)
const size = box.getSize(new THREE.Vector3()).length()
const center = box.getCenter(new THREE.Vector3())
// 把模型移动到原点附近
fbx.position.x += (fbx.position.x - center.x)
fbx.position.z += (fbx.position.z - center.z)
this.scene.add(fbx)
},
xhr => {
if (xhr.lengthComputable) {
console.log(`模型加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
} else {
console.log(`已加载 ${xhr.loaded} 字节`)
}
},
err => {
console.error('FBX 加载失败:', err)
}
)
},
// 渲染循环:更新动画混合器
animate() {
const delta = this.clock.getDelta()
if (this.mixers.length) {
this.mixers.forEach(m => m.update(delta))
}
if (this.controls) this.controls.update()
this.renderer.render(this.scene, this.camera)
this.rafId = requestAnimationFrame(this.animate)
},
start() {
if (!this.rafId) this.rafId = requestAnimationFrame(this.animate)
},
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
},
onWindowResize() {
if (!this.camera || !this.renderer) return
const w = this.getWidth()
const h = this.getHeight()
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.renderer.setSize(w, h)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
},
onVisibilityChange() {
if (document.hidden) {
this.stop()
} else {
this.start()
}
},
disposeThree() {
this.stop()
if (this.controls) {
this.controls.dispose()
this.controls = null
}
// 清理动画 mixer
this.mixers.forEach(m => {
// 断开所有动作
m.uncacheRoot(m.getRoot && m.getRoot())
})
this.mixers = []
if (this.scene) {
this.scene.traverse(obj => {
if (!obj.isMesh) return
if (obj.geometry) {
obj.geometry.dispose()
}
if (obj.material) {
const mat = obj.material
if (Array.isArray(mat)) {
mat.forEach(m => {
if (m.map) m.map.dispose()
if (m.lightMap) m.lightMap.dispose && m.lightMap.dispose()
if (m.envMap) m.envMap.dispose && m.envMap.dispose()
m.dispose()
})
} else {
if (mat.map) mat.map.dispose()
if (mat.lightMap) mat.lightMap.dispose && mat.lightMap.dispose()
if (mat.envMap) mat.envMap.dispose && mat.envMap.dispose()
mat.dispose()
}
}
})
}
if (this.renderer) {
this.renderer.forceContextLoss()
this.renderer.domElement && this.renderer.domElement.remove()
this.renderer = null
}
this.scene = null
this.camera = null
this.model = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
3,运行测试
(1)页面加载后除了会显示地面外,还会加载显示一个 fbx 格式的房屋模型。

(2)按住鼠标左键拖动可以将视图绕模型旋转,使用滚轮可以缩放视图,按住右键拖动则可平移视角。

全部评论(0)