Vue.js - 集成Three.js构建三维可视化场景教程3(加载OBJ模型)
作者:hangge | 2026-05-18 09:50
前文我演示了如何使用 Three.js 加载 GLTF/GLB 模型(点击查看)。除了 GLTF/GLB 模型外,OBJ + MTL 是仍然常见的一种模型格式(尤其是在一些老的模型资源或 CAD 导出中)。下面我将通过样例演示如何加载 .obj 格式的模型。


三、加载 OBJ 模型
1,模型准备
(1)我们将模型文件放置项目的 public/models/obj 目录下,其中 textures 子文件家中放置了 MTL 引用的纹理图片。

(2)打开 MTL 文件,查看纹理材质文件的相对路径设置是否正确。
2,样例代码
下面代码我使用 Three.js 内置的 OBJLoader 和 MTLLoader 将这个 obj 格式的木桌模型加载并展示。
<template>
<!-- Three.js 渲染容器 -->
<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'
// OBJ / MTL 加载器
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
export default {
name: 'ThreeScene',
props: {
enableControls: { type: Boolean, default: true },
width: { type: Number, default: null },
height: { type: Number, default: null },
// 模型路径与可选 MTL 路径(可通过 props 传入)
objPath: { type: String, default: '/models/obj/Wood_Table.obj' },
mtlPath: { type: String, default: '/models/obj/Wood_Table.mtl' }
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
rafId: null,
modelGroup: null // 存放加载进来的模型组(方便统一处理与释放)
}
},
mounted() {
this.$nextTick(() => {
this.initThree()
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: {
/**
* 初始化 Three.js 场景
*/
initThree() {
const container = this.$refs.container
// 1) 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x111111)
// 2) 相机
const aspect = this.getWidth() / this.getHeight()
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000)
this.camera.position.set(0, 15, 15)
// 3) 渲染器
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)
// 4) 光源
this.scene.add(new THREE.AmbientLight(0xffffff, 2.0)) // 环境光(核心)
const dir = new THREE.DirectionalLight(0xffffff, 2.8) // 主光
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)
// 5) 地面
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)
// 6) 控制器
if (this.enableControls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
}
// 7) 初始化模型容器(用于后续统一 transform / dispose)
this.modelGroup = new THREE.Group()
this.scene.add(this.modelGroup)
// 8) 开始加载 OBJ(如果同时提供 MTL,会优先使用 MTL)
this.loadOBJModel(this.objPath, this.mtlPath)
},
/**
* 加载 OBJ 模型(支持可选的 MTL)
* @param {string} objUrl - .obj 文件路径(如 /models/model.obj)
* @param {string|null} mtlUrl - 可选 .mtl 文件路径(如 /models/model.mtl),若无传 null
*/
loadOBJModel(objUrl, mtlUrl = null) {
// LoadingManager 可以用于管理纹理路径或进度
const manager = new THREE.LoadingManager()
// 可选:修正资源路径(当 .mtl 中引用了相对纹理路径时)
// manager.setURLModifier((url) => {
// // 如果你的 mtl 中的纹理路径乱,可以在这里做规则替换
// return url
// })
// 如果有 MTL,则先加载 MTL,再加载 OBJ 并绑定材质
if (mtlUrl) {
const mtlLoader = new MTLLoader(manager)
// 指定 MTL 内纹理的基本路径(如果 MTL 中写的是相对路径)
// 例如把纹理放 public/models/textures/ 下,就 setTexturePath('/models/textures/')
// mtlLoader.setTexturePath('/models/textures/') // 如有需要可启用
mtlLoader.load(
mtlUrl,
materials => {
materials.preload() // 预处理材质
// OBJLoader 并将材质应用上去
const objLoader = new OBJLoader(manager)
objLoader.setMaterials(materials)
objLoader.load(
objUrl,
obj => this.onModelLoaded(obj),
xhr => {
// 加载进度(作为参考)
if (xhr.lengthComputable) {
console.log('OBJ 加载中: ' +
`${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
}
},
err => {
console.error('OBJ 加载失败:', err)
}
)
},
xhr => {
if (xhr.lengthComputable) {
console.log(`MTL 加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
}
},
err => {
console.warn('MTL 加载失败,尝试直接加载 OBJ(无材质):', err)
// 回退:直接加载 OBJ(无材质)
const objLoader = new OBJLoader(manager)
objLoader.load(objUrl, obj => this.onModelLoaded(obj))
}
)
} else {
// 直接加载 OBJ(无 MTL)
const objLoader = new OBJLoader(manager)
objLoader.load(
objUrl,
obj => this.onModelLoaded(obj),
xhr => {
if (xhr.lengthComputable) {
console.log('OBJ 加载中: '
+ `${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
}
},
err => {
console.error('OBJ 加载失败:', err)
}
)
}
},
/**
* 模型加载后的通用处理(中心化、缩放、阴影、加入场景)
* @param {THREE.Object3D} obj - 加载得到的对象(Group)
*/
onModelLoaded(obj) {
// 先清除之前的模型(如果需要支持热替换)
if (this.modelGroup) {
// 移除并 dispose 旧模型(保留 modelGroup 本身)
while (this.modelGroup.children.length) {
const child = this.modelGroup.children[0]
this.modelGroup.remove(child)
// 不完全 dispose 在这里做基础释放(更彻底的释放在 disposeThree)
}
}
// 将 obj 放入 modelGroup(方便后续统一变换)
this.modelGroup.add(obj)
// 遍历网格,开启投/受阴影,并为无法正常显示的材质做兜底
obj.traverse(node => {
if (node.isMesh) {
node.castShadow = true
node.receiveShadow = true
// 如果材质没有法线,需要 computeVertexNormals(有些 OBJ 导出时会丢失法线)
if (!node.geometry.hasAttribute('normal')) {
node.geometry.computeVertexNormals()
}
// 若材质不可见或过暗,可临时替换为基础材质作为调试
// if (!node.material) {
// node.material = new THREE.MeshStandardMaterial({ color: 0x999999 })
// }
}
})
// 自动居中 + 缩放模型以适配相机视野
this.frameModelToView(obj)
console.log('OBJ 模型加载完成')
},
/**
* 自动居中和缩放模型,使其在视图中合适展示
* @param {THREE.Object3D} object3d
*/
frameModelToView(object3d) {
// 计算包围盒
const box = new THREE.Box3().setFromObject(object3d)
const size = new THREE.Vector3()
box.getSize(size)
const center = new THREE.Vector3()
box.getCenter(center)
// 将模型移动到原点(以 center 为参考)
object3d.position.x += (object3d.position.x - center.x)
object3d.position.y += (object3d.position.y - center.y)
object3d.position.z += (object3d.position.z - center.z)
// 根据最大轴向尺寸计算缩放比例(期望模型占据画面的一定比例)
const maxDim = Math.max(size.x, size.y, size.z)
if (maxDim === 0) return
const fitSize = 8.0 // 期望模型最大边长为多少(可调整)
const scale = fitSize / maxDim
object3d.scale.setScalar(scale)
},
/**
* 渲染循环(模型保持静止,不旋转)
*/
animate() {
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
}
},
getWidth() {
return this.width || this.$refs.container.clientWidth || window.innerWidth
},
getHeight() {
return this.height || this.$refs.container.clientHeight || window.innerHeight
},
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
}
// 遍历全场景并释放几何体/材质/纹理
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()
m.dispose()
})
} else {
if (mat.map) mat.map.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.modelGroup = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
3,运行测试
(1)页面加载后除了会显示地面外,还会加载显示一个 obj 格式的木桌模型。

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

全部评论(0)