返回 导航

Vue.js

hangge.com

Vue.js - 集成Three.js构建三维可视化场景教程5(加载3DS模型)

作者:hangge | 2026-05-20 08:40
    有时项目中会遇到大量的老格式资产,其中 .3ds3D 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)

回到顶部