Vue3 + Three.js 入门实战:从 0 到 1 搭建可交互的 3D 场景(含模型加载与性能优化)

Vue3 + Three.js 入门实战:从 0 到 1 搭建可交互的 3D 场景(含模型加载与性能优化)

一、为什么是 Vue3 + Three.js

1.1 背景与目标

在前端可视化场景里,2D 图表已经很成熟,但在产品演示、数字孪生、3D 展示页、营销互动页中,3D 表达的需求越来越常见。Three.js 是一个对 WebGL 的上层封装库,能让我们用更低门槛的方式在浏览器中渲染 3D 内容;Vue3 则负责组件化组织与状态管理,让工程更易维护。

这篇文章的目标非常明确:用 Vue3 + Vite + Three.js 搭建一个可交互的 3D 页面,并完成以下关键能力:

初始化场景(Scene)、相机(Camera)、渲染器(Renderer)

添加光照和基础几何体

接入 OrbitControls 实现鼠标交互

使用 GLTFLoader 加载 glTF 模型

实现动画循环、窗口自适应和资源释放

掌握常见报错排查与性能优化思路

术语速记:

Scene(场景):3D 世界容器,所有物体都放在里面。

Camera(相机):决定你从哪个角度观察场景。

Renderer(渲染器):把场景+相机渲染成屏幕上的图像。

glTF:Web 端常用 3D 模型传输格式,加载快、兼容好。

本节你学会了什么:明确了技术选型原因、文章目标和核心术语,知道最终要实现的完整功能路径。

二、项目初始化与依赖安装

2.1 使用 Vite 创建 Vue3 项目

先创建项目(JavaScript 风格,降低初学门槛):

npm create vite@latest vue3-threejs-starter -- --template vue

cd vue3-threejs-starter

npm install

npm install three

npm run dev

Vite 启动后,浏览器访问本地地址确认项目可运行。

2.2 安装完成后的目录规划

建议先规划结构,后续维护更清晰:

src/components/ThreeScene.vue:Three 场景组件容器

src/utils/initThree.js:Three 初始化与生命周期管理逻辑

src/assets/models/:模型资源目录

src/App.vue:页面入口,挂载场景组件

这个分层的好处是:Vue 负责页面与组件,Three 逻辑集中在工具模块,不会把 App.vue 写成"巨石文件"。

本节你学会了什么:可以独立创建 Vue3 + Vite + Three.js 项目,并掌握一套可维护的目录结构。

三、从 0 搭建 Three.js 基础场景

3.1 创建 Scene、Camera、Renderer

我们先搭建最小可运行骨架。步骤是固定的:

创建 scene

创建 camera

创建 renderer

把 renderer.domElement 挂到 Vue 容器

相机选择 PerspectiveCamera(透视相机),更符合人眼观察效果。渲染器建议开启抗锯齿 antialias: true,画面更平滑。

3.2 添加光照与基础几何体

没有光,标准材质会发黑。这里使用两种光:

AmbientLight(环境光):提供全局基础亮度

DirectionalLight(平行光):模拟太阳光方向感

几何体使用 BoxGeometry 和 MeshStandardMaterial。这是 Three.js 最常见的入门组合,能快速看到材质受光照变化的效果。

3.3 挂载到 Vue 组件并处理生命周期

在 Vue 里,Three 的初始化应放在 onMounted,销毁逻辑放在 onBeforeUnmount。

如果不做销毁,路由切换后可能出现内存泄漏或重复渲染。

本节你学会了什么:掌握 Three.js 最小场景搭建流程,以及在 Vue 生命周期中正确挂载和销毁 3D 场景。

四、实现交互能力:OrbitControls

OrbitControls 是 Three.js 官方示例里最常用的控制器,可以用鼠标实现:

左键旋转

滚轮缩放

右键平移(取决于配置)

在初始化控制器后,建议开启阻尼效果:

enableDamping = true

dampingFactor = 0.05

阻尼的意思是"惯性过渡",交互会更顺滑,不会突然停住。

注意:开启阻尼后,动画循环里必须调用 controls.update(),否则不生效。

你还可以限制交互边界,例如:

minDistance / maxDistance:限制缩放距离

maxPolarAngle:限制俯仰角,防止镜头翻转到地面以下

本节你学会了什么:能为场景加入可用的鼠标交互,并理解 OrbitControls 的核心参数与使用注意点。

五、加载 glTF 模型

5.1 GLTFLoader 基本用法

安装 three 后,GLTFLoader 从 three/examples/jsm/loaders/GLTFLoader.js 导入。

模型建议放在 public/models/ 或 src/assets/models/。为了路径稳定,本文示例采用 public/models/duck.glb,加载路径写 /models/duck.glb。

加载核心流程:

const loader = new GLTFLoader()

loader.load(url, onLoad, onProgress, onError)

在 onLoad 里拿到 gltf.scene 并 scene.add(model)

5.2 模型位置、缩放与阴影

不同来源模型尺寸差异很大,常见操作:

model.scale.set(0.01, 0.01, 0.01) 调整比例

model.position.set(0, 0, 0) 调整位置

遍历子节点设置 castShadow/receiveShadow

如果模型"加载成功但看不见",优先检查:

相机位置是否太近/太远

模型比例是否过大或过小

灯光是否足够

模型是否在视锥体外

5.3 素材来源与授权(必须合规)

本文示例模型建议使用 Khronos glTF Sample Models,其中部分模型以 CC0 / 宽松授权提供;使用前请查看具体模型目录内的 license 说明。

贴图素材可使用 Poly Haven(CC0),图标可使用自有或可商用素材。

请勿直接搬运未知版权模型到生产项目。

本节你学会了什么:掌握 GLTFLoader 的标准加载方式,能够处理模型变换、显示问题和素材合规要求。

六、动画循环与窗口自适应

6.1 requestAnimationFrame 动画循环

Three 场景更新通常放在 animate 函数中,使用 requestAnimationFrame 递归。

这个循环里一般做三件事:

更新控制器 controls.update()

执行动画(如旋转、位移)

渲染 renderer.render(scene, camera)

6.2 resize 事件与像素比优化

浏览器窗口变化时,需要同步更新:

camera.aspect = width / height

camera.updateProjectionMatrix()

renderer.setSize(width, height)

同时建议限制像素比,避免高 DPI 设备渲染压力过高:

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

6.3 组件卸载时资源释放

性能优化不仅是"跑得快",还包括"退出干净"。在组件销毁时应:

移除 resize 监听

取消动画帧

controls.dispose()

遍历场景释放 geometry/material/texture

renderer.dispose()

这样可以避免多次进入页面后显存持续上涨。

本节你学会了什么:可以构建稳定的渲染循环,处理窗口自适应,并完成规范的资源回收。

七、常见报错与排查思路

7.1 模型 404

报错特征:GET /models/duck.glb 404 (Not Found)

排查步骤:

确认文件实际存在于 public/models/duck.glb

路径是否以 /models/... 开头

文件名大小写是否一致(Linux 环境区分大小写)

7.2 CORS 或 file 协议问题

如果直接双击 HTML 打开(file://),资源加载可能失败。

正确方式是用 Vite 开发服务器(npm run dev)或部署在 Web 服务器上。

7.3 黑屏 / 模型不显示 / 画面卡顿

黑屏:通常是相机没对准、渲染器尺寸为 0、容器没高度

模型不显示:比例异常、在视野外、材质或灯光问题

锯齿明显:未开抗锯齿、像素比设置不合理

卡顿:阴影过重、模型面数过高、像素比过高

建议在关键节点打印信息:容器尺寸、相机位置、模型包围盒(Box3)等,定位会快很多。

本节你学会了什么:建立了常见问题的"症状->原因->排查顺序",能更高效地解决初学阶段的大部分故障。

八、性能优化建议(至少 5 条,实战可落地)

限制像素比:Math.min(devicePixelRatio, 2),高分屏收益明显。

减少阴影开销:仅核心物体开启阴影,必要时降低 shadow.mapSize。

控制模型面数与贴图尺寸:优先 glTF + Draco 压缩(进阶可加 DRACOLoader)。

避免频繁创建对象:动画循环中复用 Vector3 等临时对象,减少 GC 抖动。

按需渲染策略:静态场景可在交互/数据变化时触发渲染,减少空转帧。

及时释放资源:路由切换必须 dispose,防止显存泄漏。

减少透明材质与后处理链路:透明排序和多通道后处理成本高。

使用性能监控工具:可接入 stats.js 观察 FPS,快速验证优化效果。

这些建议里,前 4 条是最容易马上见效的,建议优先落地。

本节你学会了什么:掌握了 8 条可执行优化策略,知道该从哪些点优先下手提升帧率和稳定性。

九、总结与下篇预告

本文从工程角度走完了 Vue3 + Three.js 入门闭环:项目初始化、基础场景、光照、交互控制、glTF 模型加载、动画循环、自适应、异常排查和性能优化。你现在已经可以独立搭建一个"能看、能动、可扩展"的 3D 页面雏形。

下篇我会继续做实战升级:射线拾取(Raycaster)+ 点击高亮 + 动画过渡(GSAP),让场景真正具备业务交互能力(例如点击设备弹出信息卡片)。

本节你学会了什么:完成了首个 Vue3 + Three.js 工程化实战闭环,并明确下一步进阶方向。

参考资料(引用出处)

Three.js 官方文档:https://threejs.org/docs/

Three.js 示例(OrbitControls / GLTFLoader):https://threejs.org/examples/

MDN requestAnimationFrame:https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame

glTF 规范与生态(Khronos):https://www.khronos.org/gltf/

  1. 关键代码片段(按章节整理,保证可运行)

5.1 src/App.vue:应用入口,挂载 Three 场景组件

Vue3 + Three.js 入门实战

5.3 src/utils/initThree.js:Three 初始化、模型加载、动画循环、销毁 import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' export function createThreeApp(container) { if (!container) { throw new Error('Three container is required.') } const scene = new THREE.Scene() scene.background = new THREE.Color(0x0b1020) const camera = new THREE.PerspectiveCamera( 60, container.clientWidth / container.clientHeight, 0.1, 1000 ) camera.position.set(3, 2, 6) const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }) renderer.setSize(container.clientWidth, container.clientHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.shadowMap.enabled = true renderer.shadowMap.type = THREE.PCFSoftShadowMap container.appendChild(renderer.domElement) // 光照 const ambientLight = new THREE.AmbientLight(0xffffff, 0.45) scene.add(ambientLight) const dirLight = new THREE.DirectionalLight(0xffffff, 1.1) dirLight.position.set(5, 8, 5) dirLight.castShadow = true dirLight.shadow.mapSize.set(1024, 1024) scene.add(dirLight) // 地面 const planeGeometry = new THREE.PlaneGeometry(20, 20) const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x1e293b }) const plane = new THREE.Mesh(planeGeometry, planeMaterial) plane.rotation.x = -Math.PI / 2 plane.position.y = -1 plane.receiveShadow = true scene.add(plane) // 基础几何体 const boxGeometry = new THREE.BoxGeometry(1, 1, 1) const boxMaterial = new THREE.MeshStandardMaterial({ color: 0x38bdf8 }) const cube = new THREE.Mesh(boxGeometry, boxMaterial) cube.position.set(-1.6, -0.2, 0) cube.castShadow = true scene.add(cube) // 坐标轴辅助(调试可开关) const axesHelper = new THREE.AxesHelper(5) scene.add(axesHelper) // 控制器 const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true controls.dampingFactor = 0.05 controls.minDistance = 2 controls.maxDistance = 20 controls.maxPolarAngle = Math.PI / 2 // glTF 模型加载 let model = null const loader = new GLTFLoader() loader.load( '/models/duck.glb', (gltf) => { model = gltf.scene model.position.set(1.6, -1, 0) model.scale.set(0.02, 0.02, 0.02) model.traverse((obj) => { if (obj.isMesh) { obj.castShadow = true obj.receiveShadow = true } }) scene.add(model) }, undefined, (error) => { console.error('Model load failed:', error) } ) let rafId = 0 const clock = new THREE.Clock() function animate() { const delta = clock.getDelta() cube.rotation.x += delta * 0.5 cube.rotation.y += delta * 0.8 if (model) { model.rotation.y += delta * 0.6 } controls.update() renderer.render(scene, camera) rafId = window.requestAnimationFrame(animate) } function onResize() { const width = container.clientWidth const height = container.clientHeight if (!width || !height) return camera.aspect = width / height camera.updateProjectionMatrix() renderer.setSize(width, height) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) } function disposeMaterial(material) { if (!material) return for (const key in material) { const value = material[key] if (value && value.isTexture) value.dispose() } material.dispose() } function destroyScene() { scene.traverse((obj) => { if (!obj.isMesh) return if (obj.geometry) obj.geometry.dispose() if (Array.isArray(obj.material)) { obj.material.forEach(disposeMaterial) } else { disposeMaterial(obj.material) } }) } window.addEventListener('resize', onResize) return { start() { animate() }, destroy() { window.removeEventListener('resize', onResize) window.cancelAnimationFrame(rafId) controls.dispose() destroyScene() renderer.dispose() if (renderer.domElement && renderer.domElement.parentNode) { renderer.domElement.parentNode.removeChild(renderer.domElement) } } } } 5.4 模型准备说明(public/models/duck.glb) 用途:提供可加载的 glTF 示例模型。 你可以从 Khronos Sample Models 获取 Duck.glb 放到 public/models/duck.glb。运行后即可看到方块与模型同时渲染。

  1. 资源包目录结构(含每个文件作用)
    vue3-threejs-starter/
    ├─ README.md # 项目说明、运行步骤、依赖版本、截图占位说明
    ├─ package.json # 项目依赖与脚本(vite/three/vue)
    ├─ LICENSE # 开源协议模板(建议 MIT)
    ├─ public/
    │ └─ models/
    │ ├─ duck.glb # 示例模型(需标注来源与授权)
    │ └─ README.md # 模型来源、作者、授权、替换说明
    ├─ src/
    │ ├─ main.js # Vue 应用入口
    │ ├─ App.vue # 页面入口,挂载 ThreeScene
    │ ├─ components/
    │ │ └─ ThreeScene.vue # 3D 容器组件,处理挂载与销毁
    │ ├─ utils/
    │ │ └─ initThree.js # Three 场景初始化、渲染循环、模型加载与资源释放
    │ └─ assets/
    │ └─ models/
    │ └─ .gitkeep # 资源目录占位(可按需迁移模型到 public)
    └─ vite.config.js # Vite 配置(默认即可)
  2. CSDN发布素材(标签8个、封面短句3条、结尾互动引导3条)
    7.1 标签(8个)
    Vue3
    Three.js
    WebGL
    前端可视化
    3D
    Vite
    glTF
    JavaScript
    7.2 封面短句(3条)
    Vue3 + Three.js 从 0 到 1,搭出你的第一个 3D 场景
    前端 3D 入门实战:模型加载、交互控制、性能优化一次搞定
    告别只会看 Demo:手把手搭建可交互 Three.js 工程
    7.3 结尾互动引导(3条)
    你在加载 glTF 模型时遇到过最棘手的问题是什么?评论区贴报错我帮你定位。
    如果你希望我下一篇写"点击模型弹窗 + 动画过渡",点赞过 100 我就加速更新。
    你更想看哪种实战:3D 数据大屏、产品展示页,还是数字孪生场景?欢迎投票。
    7.4 建议发布时间(SEO与阅读时段)
    工作日:周二或周四 20:00-21:30
    周末:周六 10:00-11:30
  3. 发布前检查清单(合规/版权/可运行)

标题包含关键词:Vue3、Three.js、入门、实战(至少自然覆盖)

本文代码已在本地执行 npm install && npm run dev 验证可运行

模型路径与文件名正确,/models/duck.glb 可访问

文章中所有外部素材已标注来源与授权方式(CC0/可商用/自有)

未使用来源不明图片/模型,未搬运或洗稿

引用文档已附出处链接(Three.js docs、MDN、Khronos)

关键截图不含敏感信息(本地路径、隐私账号、密钥等)

结尾含互动引导,首屏摘要完整且可读

标签数量与主题一致,不堆砌无关关键词

代码块排版清晰,复制后可直接运行

  1. 发布后3天复盘模板(指标+阈值+优化动作)

9.1 3天数据复盘表(可直接复制)

维度 指标 Day1 Day2 Day3 阈值 结论 优化动作

曝光

阅读量

< 500

优化标题关键词顺序,重写首屏摘要前两句

曝光

展示点击率(CTR)

< 5%

封面短句替换为结果导向,补"可运行代码"卖点

互动

点赞数

< 20

在文首增加"你将获得什么"三条清单

互动

收藏数

收藏率 < 3%

增强可复用代码价值:增加配置项与注释

互动

评论数

< 10

结尾增加具体问题引导(如"你卡在哪一步")

互动

转发数

< 5

增加"项目目录+完整源码"提示,降低转发门槛

转化

主页访问

< 50

文末增加作者卡片和系列文章导航

转化

粉丝新增

< 20

增加"下一篇预告+关注提醒"

转化

资源下载点击

< 30

在正文中段和结尾各放一次资源入口

9.2 阈值触发后的动作规则(简版)

阅读量 < 500:优先改标题与摘要,24 小时内二次分发

收藏率 < 3%:补"可复制模块代码"和"参数调优表"

评论 < 10:强化问题式结尾,增加投票型互动

CTR < 5%:更换封面文案,突出"从 0 到 1 可运行"

9.3 下一篇联动选题建议(2个)

Vue3 + Three.js 进阶实战:Raycaster 点击拾取 + 信息面板联动

Three.js 性能实战:从 30FPS 到 60FPS 的系统化优化清单(含监控指标)

相关推荐
qq_12084093712 小时前
Vue3 + Three.js 实战入门:从零搭建可交互3D场景(含模型加载与性能优化)
javascript·3d·vue3·交互
1314lay_10072 小时前
axios的Post方法和Delete方法的参数个数和位置不同,导致415错误
前端·javascript·vue.js·elementui
动恰客流管家2 小时前
动恰3DV3丨客流统计方案:赋能文旅街区 / 古镇智慧化运营升级
大数据·人工智能·3d
iReachers2 小时前
HTML打包EXE工具四种弹窗方式图文详解 - 单窗口/新窗口/标签页/浏览器打开
前端·javascript·html·弹窗·html打包exe·html转程序
好家伙VCC2 小时前
# ARCore+ Kotlin 实战:打造沉浸式增强现实交互应用在
java·python·kotlin·ar·交互
耗子君QAQ2 小时前
🔧 Rattail | 面向 Vite+ 和 AI Agent 的前端工具链
前端·javascript·vue.js
AI先驱体验官2 小时前
BotCash:AI智能体知识管理新基建,GitNexus带来的技术范式转移
大数据·运维·人工智能·aigc·交互
萑澈10 小时前
Windows 7 运行 Electron 安装包报“不是有效的 Win32 应用程序”怎么办
javascript·windows·electron
W.A委员会11 小时前
JS原型链详解
开发语言·javascript·原型模式