返回 导航

Vue.js

hangge.com

Vue.js - 集成Three.js构建三维可视化场景教程1(安装配置、基本用法)

作者:hangge | 2026-05-14 08:40
    Three.js 是一个强大的 WebGL 库,能够帮助开发者轻松创建和展示 3D 图形。结合 Vue.js 的响应式特性,我们可以构建出既美观又交互性强的 3D Web 应用。本文将演示如何在 Vue.js 项目中集成 Three.js,并创建一个简单的 3D 场景。

一、安装配置与基本用法

1,安装配置

进入 Vue 项目目录,执行如下命令安装 Three.js 核心库。
npm install three

2,基本用法

(1)下面是一个简单的使用样例,里面包含场景、相机、渲染器、光源、一个旋转立方体、OrbitControls、窗口缩放响应、资源销毁与动画循环管理等内容。
<template>
    <!-- 
      Three.js 渲染容器
      WebGLRenderer 创建的 canvas 会被手动 append 到这个 div 中
      Vue 本身不直接管理 canvas
    -->
    <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'

export default {
    name: 'ThreeScene',

    // ====== 组件参数 ======
    props: {
        // 是否启用 OrbitControls
        enableControls: {
            type: Boolean,
            default: true
        },
        // 指定渲染宽度(可选)
        // 如果不传,则自动使用容器尺寸
        width: {
            type: Number,
            default: null
        },
        // 指定渲染高度(可选)
        height: {
            type: Number,
            default: null
        }
    },

    // ====== Three.js 核心对象(不做响应式) ======
    data() {
        return {
            scene: null,     // Three.js 场景
            camera: null,    // 相机
            renderer: null,  // WebGL 渲染器
            controls: null,  // 轨道控制器
            rafId: null,     // requestAnimationFrame 返回的 ID,用于停止动画
            cube: null       // 示例立方体 Mesh
        }
    },

    // ====== 组件挂载完成 ======
    mounted() {
        // 确保 DOM 已经渲染完成
        this.$nextTick(() => {
            this.initThree() // 初始化 Three.js 场景
            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)
        // 释放 Three.js 所有资源(非常关键)
        this.disposeThree()
    },

    methods: {
        /**
         * 初始化 Three.js 场景
         * 包含:场景、相机、渲染器、光源、物体、控制器
         */
        initThree() {
            const container = this.$refs.container

            // ===== 1) 创建场景 =====
            this.scene = new THREE.Scene()
            // 设置背景色(深色背景更适合 3D)
            this.scene.background = new THREE.Color(0x111111)

            // ===== 2) 创建相机 =====
            // 透视相机(人眼视角)
            const aspect = this.getWidth() / this.getHeight()
            this.camera = new THREE.PerspectiveCamera(
                60,     // 视野角度(FOV)
                aspect, // 宽高比
                0.1,    // 近裁剪面
                1000    // 远裁剪面
            )
            // 设置相机位置
            this.camera.position.set(0, 5, 5)

            // ===== 3) 创建渲染器 =====
            this.renderer = new THREE.WebGLRenderer({
                antialias: true // 抗锯齿
            })
            // 设置像素比,防止高分屏过度消耗性能
            this.renderer.setPixelRatio(
                Math.min(window.devicePixelRatio || 1, 2)
            )
            // 设置渲染尺寸
            this.renderer.setSize(this.getWidth(), this.getHeight())
            // 使用 sRGB 颜色空间,避免颜色偏暗
            this.renderer.outputEncoding = THREE.sRGBEncoding
            // 开启阴影支持
            this.renderer.shadowMap.enabled = true
            // 将 canvas 插入到容器中
            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(5, 5)
            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) 示例物体(立方体) =====
            const geo = new THREE.BoxGeometry(1, 1, 1)
            const mat = new THREE.MeshStandardMaterial({
                color: 0x2194ce
            })
            this.cube = new THREE.Mesh(geo, mat)
            this.cube.castShadow = true
            this.scene.add(this.cube)

            // ===== 7) 轨道控制器 =====
            if (this.enableControls) {
                this.controls = new OrbitControls(
                    this.camera,
                    this.renderer.domElement
                )
                // 开启阻尼(惯性效果)
                this.controls.enableDamping = true
            }
        },

        /**
         * 渲染循环(每一帧都会执行)
         */
        animate() {
            // 示例动画:立方体自转
            if (this.cube) {
                this.cube.rotation.x += 0.01
                this.cube.rotation.y += 0.015
            }
            // 更新控制器(阻尼效果必须)
            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
            }
        },

        /**
         * 获取渲染宽度
         * 优先使用 props,其次使用容器尺寸
         */
        getWidth() {
            if (this.width) return this.width
            return this.$refs.container.clientWidth || window.innerWidth
        },

        /**
         * 获取渲染高度
         */
        getHeight() {
            if (this.height) return this.height
            return 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)
            )
        },

        /**
         * 页面可见性变化处理
         * 切换标签页时暂停渲染,节省 CPU / GPU
         */
        onVisibilityChange() {
            if (document.hidden) {
                this.stop()
            } else {
                this.start()
            }
        },

        /**
         * 彻底释放 Three.js 资源(防止内存 / 显存泄露)
         */
        disposeThree() {
            // 停止动画
            this.stop()
            // 释放控制器
            if (this.controls) {
                this.controls.dispose()
                this.controls = null
            }
            // 遍历场景,释放 Mesh 资源
            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.cube = null
        }
    }
}
</script>

<style scoped>
/* Three.js 容器样式 */
.three-container {
    width: 100%;
    height: 100%;
    min-height: 360px;
    position: relative;
    overflow: hidden;
}
</style>

(2)运行效果如下,立法体会在场景中自动不断地自转。

(3)同时由于样例中启用了 OrbitControls,我们可以通过鼠标对三维场景进行直观交互(整体交互体验接近专业三维软件(如 CAD、建模工具)的操作方式):
  • 按住鼠标左键拖动可围绕目标物体旋转视角,从不同角度观察模型。
  • 滚动鼠标滚轮可实现视图的放大与缩小,即相机向目标点靠近或远离。
  • 按住鼠标右键拖动可平移视角,使整个场景在屏幕中上下左右移动。
  • 同时开启了阻尼效果,松开鼠标后相机会产生轻微的惯性滑动并逐渐停止。
评论

全部评论(0)

回到顶部