Vue.js - 集成Three.js构建三维可视化场景教程5(加载3DS模型)
作者:hangge | 2026-05-20 08:40
有时项目中会遇到大量的老格式资产,其中 .3ds(3D Studio Max 的旧格式)就十分常见。虽然 .3ds 并非为 Web 设计,但 three.js 也对其提供了支持。下面我将演示如何直接加载并展示 3DS 格式的模型。
五、加载 3DS 模型
1,模型准备
我们将需要加载的 3DS 模型文件和纹理贴图放置项目的 public/models/3ds 目录下。

2,样例代码
下面代码我使用 Three.js 内置的 TDSLoader 加载这个 3ds 格式的 CPU 模型。
<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'
// 3DS 模型加载器
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js'
export default {
name: 'ThreeScene',
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 // 3ds 模型引用
}
},
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, 20, 20)
// 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, 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)
// 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) 加载 3DS 模型
this.load3DSModel('/models/3ds/corei7.3DS')
},
/**
* 3DS 模型加载方法
* @param {string} url 模型路径
*/
load3DSModel(url) {
const loader = new TDSLoader()
// 可选:如果你的贴图放在单独目录,设置资源路径
// loader.setResourcePath('/models/textures/')
loader.load(
url,
object => {
// 3DS loader 返回一个 Object3D(可能包含多个 Mesh)
this.model = object
// 遍历并开启阴影,修正贴图编码(若有)
object.traverse(node => {
if (node.isMesh) {
node.castShadow = true
node.receiveShadow = true
const mat = node.material
if (mat) {
// 3DS 导出常见的贴图是颜色贴图(应设为 sRGB)
if (mat.map) {
mat.map.encoding = THREE.sRGBEncoding
mat.map.needsUpdate = true
}
// 法线贴图使用线性编码
if (mat.normalMap) {
mat.normalMap.encoding = THREE.LinearEncoding
mat.normalMap.needsUpdate = true
}
}
}
})
// 自适应缩放与居中:计算包围盒并归一化大小
const bbox = new THREE.Box3().setFromObject(object)
const size = bbox.getSize(new THREE.Vector3()).length()
const center = bbox.getCenter(new THREE.Vector3())
// 期望尺寸占位(按需调整),避免除以零
const desired = 10
if (size > 0) {
const scale = desired / size
object.scale.setScalar(scale)
}
// 将模型移动到原点
object.position.x += (object.position.x - center.x)
object.position.z += (object.position.z - center.z)
//object.position.y += (object.position.y - center.y)
object.position.y += size / 10
this.scene.add(object)
},
xhr => {
if (xhr.lengthComputable) {
console.log(`3DS 加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
} else {
console.log(`已加载 ${xhr.loaded} 字节`)
}
},
err => {
console.error('3DS 加载失败:', err)
}
)
},
/**
* 渲染循环(模型保持静止,不旋转)
*/
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.model = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
3,运行测试
(1)页面加载后除了会显示地面外,还会加载显示一个 3ds 格式的 CPU 模型。
(2)按住鼠标左键拖动可以将视图绕模型旋转,使用滚轮可以缩放视图,按住右键拖动则可平移视角。

全部评论(0)