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/
- 关键代码片段(按章节整理,保证可运行)
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。运行后即可看到方块与模型同时渲染。
- 资源包目录结构(含每个文件作用)
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 配置(默认即可) - 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 - 发布前检查清单(合规/版权/可运行)
标题包含关键词:Vue3、Three.js、入门、实战(至少自然覆盖)
本文代码已在本地执行 npm install && npm run dev 验证可运行
模型路径与文件名正确,/models/duck.glb 可访问
文章中所有外部素材已标注来源与授权方式(CC0/可商用/自有)
未使用来源不明图片/模型,未搬运或洗稿
引用文档已附出处链接(Three.js docs、MDN、Khronos)
关键截图不含敏感信息(本地路径、隐私账号、密钥等)
结尾含互动引导,首屏摘要完整且可读
标签数量与主题一致,不堆砌无关关键词
代码块排版清晰,复制后可直接运行
- 发布后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 的系统化优化清单(含监控指标)