尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue3 + Three.js 3D项目模版:从初始化到粒子动画的完整实践

Vue3 + Three.js 3D项目模版:从初始化到粒子动画的完整实践 简介基于Vue与Three.js的3D项目模板面向Web前端开发者和Three.js初学者旨在解决3D场景从零搭建的繁琐问题。模板整合Vue的组件化开发与Three.js的渲染能力包含场景、相机、灯光、几何体等核心模块并提供数据绑定与事件交互的示例开发者可在此基础上快速定制自己的3D应用。资源共303个文件以png贴图素材、js交互脚本、vue组件、obj与mtl三维模型文件为主辅以配置文件、环境变量和说明文档整体仅6.67MB便于直接导入工程使用。压缩包目录结构清晰涵盖静态资源、组件封装和入口文件学习时能对照代码理解Vue生命周期与Three.js渲染循环的配合方式。目前已有205人学习下载适合希望快速上手Vue与Three.js、构建数据可视化或产品展示场景的开发者。 做vue开发的人迟早会遇到一个需求页面里要放一个3D场景模型要转、粒子要飘、动画要跑。这时候就得往项目里引three.js然后你会发现每次从零搭一套渲染基础环境——场景、相机、渲染器、动画循环、窗口自适应——代码几乎一样但每个项目都得重新复制一遍改起来还容易漏。我整理了一份基于vue和three.js的3d项目模版把最常用、最容易出错的部分一次性封装好平时开新项目直接拿来用省掉的不只是半小时而是很多次“为什么转起来画面是黑的”的深夜排查。这份模版适合谁适合vue技术栈里想快速落地3D场景的同学重点解决下面几类问题three.js初始化代码太重复、vue组件和three对象之间的生命周期管理混乱、窗口缩放后画面拉伸变形、以及不同机型上渲染模糊或卡顿。如果你只是想在vue项目里放个能转的3D模型或者做粒子特效、产品展示、数据可视化场景这份模版可以直接作为起点。1. 模版的设计思路先搞清楚为什么要做成“模版”1.1 核心痛点vue项目里接three.js为什么总要重来一遍单独用three.js写一个小demo很简单官网例子复制下来就能跑。但放进vue项目里情况完全不同。第一vue组件有生命周期组件销毁时renderer、scene、动画帧这些资源要手动释放否则页面切走之后再切回来会多出一堆隐藏动画继续消耗GPU。第二vue的响应式数据和three.js的更新机制是两套系统你把相机位置塞进ref里改了一下数值不会自动触发three.js重绘必须自己搞清楚哪些数据该用响应式、哪些不该用。第三窗口resize、设备像素比、OrbitControls的阻尼更新这些细节单独写每项都简单但每次新项目都要重新组合一遍忘一个就出问题。我统计了一下自己项目里重复写的初始化代码场景、相机、渲染器、灯光、controls、动画循环、resize监听加起来稳定在200行以上。这200行基本不用改但每个项目都要写一遍。模版的核心思路就是把这200行基础设施抽象成可复用的模块让后续开发只需要关注“这个页面要展示什么模型、做什么动画”。1.2 方案选型vue3 vite three.js 的组合逻辑技术栈我选了vue3的组合式API而不是vue2的选项式有两个实际原因。一是组合式API在逻辑复用上更顺手一段3D初始化逻辑可以抽成useThree这样的hook多个组件共享时不会像mixin那样让人搞不清数据来源。二是vue3生态里和vite配合也更成熟而vite的按需编译和热更新对three.js这种大库来说体验很好——改一行代码浏览器里几乎秒刷。three.js的引入方式我推荐直接用npm包而不是通过CDN标签。npm方式版本可控配合vite能做tree-shaking摇树优化打包后只包含实际用到的模块能明显减小体积。我的模版里用的是当前较新的稳定版本展示型项目不建议追最新版因为three.js版本迭代快某些API和旧版不兼容换个大版本可能要改不少代码。锁定一个测试过的版本号比一直升级到最新版省心得多。1.3 目录结构模版怎么组织才不会乱模版采用vue3标准工程结构3D相关的核心代码放在src/three目录下和页面组件区分开src/three/core/renderer.js创建渲染器配置像素比、阴影、输出编码src/three/core/sceneManager.js管理场景中的公共元素如环境光、背景色、地面网格src/three/core/cameraManager.js负责相机初始化和视图控制src/three/useThree.js组合式API的入口把上面的模块整合成可调用hooksrc/components/ThreeContainer.vue承载3D场景的容器组件处理生命周期绑定组件层里的ThreeContainer.vue是页面和three.js之间的桥梁。它只做两件事挂载时调用useThree初始化场景卸载时销毁所有webgl资源。页面里使用容器组件时只传需要展示的模型、动画配置等“内容层”参数不关心渲染底层。2. 基础环境搭建与核心配置2.1 创建项目并安装依赖模版基于vite创建执行npm create vitelatest my-3d-app -- --template vue即可得到一个干净的vue3项目。然后安装three.js及其类型定义npm install three npm install -D types/three如果只需要核心渲染能力这两个依赖就够了。OrbitControls等附加模块都从three/examples/jsm里按需导入不用额外安装其他包。这里有个小提醒一定要装types/three否则在vue里写three.js代码时ide的智能提示会弱很多很多属性都要靠记忆开发效率大打折扣。2.2 渲染器、场景、相机的初始化细节下面这段是模版里最核心的部分我把每个对象的创建过程和参数选择都解释一下// src/three/core/renderer.js import * as THREE from three export function createRenderer(container) { const renderer new THREE.WebGLRenderer({ antialias: true, // 开启抗锯齿直线边缘更平滑 alpha: true, // 背景透明方便和页面CSS背景融合 powerPreference: high-performance // 优先使用独立显卡 }) renderer.setSize(container.clientWidth, container.clientHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.shadowMap.enabled true renderer.shadowMap.type THREE.PCFSoftShadowMap renderer.outputColorSpace THREE.SRGBColorSpace container.appendChild(renderer.domElement) return renderer }setPixelRatio这里用Math.min(devicePixelRatio, 2)做一个上限是针对高分屏的必要处理。如果不设上限iPhone、MacBook这类3倍像素比的设备上渲染分辨率会变成实际CSS尺寸的3倍GPU压力直接翻好几倍帧率瞬间掉下来。而2倍像素比在绝大多数屏幕上视觉差异已经很难察觉算是一个画质和性能的平衡点。输出色彩空间要特别注意。新版本three.js默认走线性工作流如果不显式设置outputColorSpace为SRGBColorSpace贴图颜色可能偏暗或偏灰特别是颜色较鲜艳的材质看起来总觉得“不对”。这是three.js升级到r152版本之后的一个坑老代码里很多直接用默认值的切版本后颜色全变了排查半天才发现。2.3 轨道控制和坐标辅助调试期离不了的家伙场景、相机、渲染器是3D世界的三根柱子但光有柱子没法感知位置关系。模版里默认加了两样调试利器OrbitControls和AxesHelper。import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js export function createControls(camera, renderer) { const controls new OrbitControls(camera, renderer.domElement) controls.enableDamping true // 惯性效果无缝平滑 controls.dampingFactor 0.08 // 阻尼系数越小越滑 controls.minDistance 2 controls.maxDistance 50 controls.maxPolarAngle Math.PI / 2 // 限制视角不钻到地面以下 return controls }轨道控制最大的作用是降低调试成本——写3D代码时经常需要从不同角度观察模型如果每次都要改相机坐标来换视角效率太低了。建议开发环境里保留controls生产环境根据业务决定要不要去掉。另外场景里加一个AxesHelper坐标轴辅助器放在原点能直观看到红绿蓝三色坐标轴对比模型朝向和位移方向排查“为什么模型跑到了奇怪位置”的速度会快很多。3. 实现可复用的3D渲染核心3.1 动画循环与生命周期绑定three.js渲染不是一次性的模型旋转、粒子运动、相机缓动都需要持续重绘。官方常见做法是用requestAnimationFrame递归调用配合clock计算帧间隔// src/three/core/useThree.js import { onMounted, onUnmounted } from vue export function useThree(containerRef) { let renderer, scene, camera, controls let animationId null const animate () { animationId requestAnimationFrame(animate) const delta clock.getDelta() // 在这里统一执行所有动画更新 controls.update() renderer.render(scene, camera) } onMounted(() { // 用nextTick确保容器DOM已渲染完成 nextTick(() { renderer createRenderer(containerRef.value) // 其他初始化... animate() }) }) onUnmounted(() { cancelAnimationFrame(animationId) renderer.dispose() // 清理场景内所有几何体、材质、纹理 }) }这里有几处容易踩坑的地方。动画循环一定要保存requestAnimationFrame返回的id并在组件卸载时cancel掉否则组件销毁后动画还在跑一个页面切好几次就会积压多个隐藏循环。另外onMounted里初始化renderer之前必须先nextTick确认container的DOM已经插入文档且有了实际尺寸。如果过早初始化container.clientWidth可能是0渲染器宽度变成0场景就“消失了”。3.2 场景、模型与组件的解耦模版里做了一个关键设计场景里的“内容”和底层的“渲染环境”分开管理。容器组件ThreeContainer.vue只负责生命周期和渲染循环页面里想放什么模型、做什么交互都在另一层传入的配置或插槽里实现。template div refcontainerRef classthree-container/div /template script setup const props defineProps({ sceneSetup: Function // 接收scene往里添加模型、灯光等 }) /script比如你在某个页面要展示一个旋转的立方体只需要在父组件里传一个sceneSetup函数函数接收到scene后往里添加立方体即可。这样做的好处是渲染循环、renderer配置这些“基础设施”不用每个业务组件各写一遍模型管理集中在各自业务里互不干扰。如果一个项目里多个页面都需要3D展示每个页面只是往里添加不同模型复用成本极低。3.3 设备适配与性能基线前面提到的resize监听是设备适配的重点。容器尺寸变化时要同时更新renderer的尺寸和相机的aspect比例否则画面会变形window.addEventListener(resize, () { const width container.clientWidth const height container.clientHeight camera.aspect width / height camera.updateProjectionMatrix() renderer.setSize(width, height) })在移动端还有一个容易被忽略的点iOS Safari地址栏收起、展开会触发resize事件如果用window.innerHeight计算高度场景底部会被裁切或出现黑边。模版里统一用container.clientWidth和clientHeight让3D画布始终填满容器而不是视口从根源上规避这类问题。如果你有特殊需求要响应整个视口建议用visualViewport的API配合处理。性能基线方面模版里默认开启渲染器统计功能可通过扩展显示fps和draw calls方便开发期观察性能。生产环境关闭统计避免额外开销。4. 让模版跑起来从粒子效果到模型加载4.1 场景一一个基础粒子系统粒子玫瑰思路延伸有了模版写一个粒子特效就简单很多。很多开发者在找的“粒子玫瑰”本质上是大量粒子按特定函数分布并做运动效果。用three.js实现核心思路是先用BufferGeometry生成一批粒子位置再用Points材质渲染function initParticles(scene) { const geometry new THREE.BufferGeometry() const count 20000 const positions new Float32Array(count * 3) const colors new Float32Array(count * 3) for (let i 0; i count; i) { // 根据目标形状计算每个粒子的初始坐标比如按玫瑰曲线取值 positions[i * 3] x positions[i * 3 1] y positions[i * 3 2] z // 颜色可基于位置渐变 } geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)) geometry.setAttribute(color, new THREE.BufferAttribute(colors, 3)) const material new THREE.PointsMaterial({ size: 0.1, vertexColors: true, transparent: true, opacity: 0.8 }) const points new THREE.Points(geometry, material) scene.add(points) return points }粒子系统的性能要点在于合并绘制。把几万个粒子放到一个Points对象中GPU只需要一次draw call就能渲染完比创建几万个Mesh快好几个数量级。这一点在移动端尤其重要。如果想做粒子飘动、扩散这类动态效果可以在动画循环里通过更新attribute来实现注意用needsUpdate标记告诉three.js重新上传数据。4.2 场景二加载gltf模型并接入动画从网络上下载或者美术同事交付的模型主流格式是glTF.glb/.gltf。three.js通过GLTFLoader加载加载成功后把模型添加到场景里import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js const loader new GLTFLoader() loader.load(/models/robot.glb, (gltf) { const model gltf.scene scene.add(model) // 如果模型自带动画获取并播放 const mixer new THREE.AnimationMixer(model) const action mixer.clipAction(gltf.animations[0]) action.play() })这里有个Loader和渲染器的配合问题要注意GLTFLoader加载是异步的而且内部可能涉及图片纹理、bin资源等多文件请求。在组件卸载时如果加载还没完成回调里又去操作已经销毁的scene会报一堆错。模版里做一个加载状态标记组件卸载时把标记置为false回调里判断这个标记再决定是否继续操作。模型放进来之后经常需要统一比例。不同来源的模型尺寸差异很大有的导进来有几十米高有的小到看不见。在模版里提供一个normalizeModel函数把模型整体遍历一遍计算包围盒然后缩放到一个设定的目标尺寸比如最大边2米这样不管原始模型多大进场景后都在一个可调控的范围内。4.3 场景三火箭发射动画的简单实现思路火箭发射这类刺激的动画核心其实就是“位移 粒子尾焰 音效”的组合three.js完全能实现。火箭本身是一个glb模型尾焰用粒子系统模拟发射过程通过requestAnimationFrame更新火箭的position和粒子发射位置。关键点在于“跟随火箭的尾焰”怎么实现。我的做法是粒子系统不完全绑定在火箭子节点下否则火箭移动时粒子只能相对固定而是让粒子系统在火箭底部持续生成新粒子已经生成的粒子留在原地并逐渐缩小、变透明甚至下落模拟尾焰拖尾效果。这样一个简单的发射动画在模版基础上增加100行左右代码就能跑出相对震撼的视觉效果。5. 常见问题与避坑清单5.1 vue响应式系统对three.js的干扰vue的响应式代理对普通对象、数组拦截是反直觉的比如把three.js的Vector3对象放进ref后内部的x、y、z可能被Proxy代理导致某些数学运算尤其是大量循环里的更新操作变慢极端情况下还会出现循环依赖或类型判断异常。所以我的模版里约定three.js场景中的实体对象一律放在普通变量里不放进ref/reactive响应式对象需要响应式的参数比如控制面板上的滑块值单独存ref动画循环中主动读取并应用。5.2 纹理加载与跨域问题从外部URL加载纹理如果服务器没配CORSWebGL会报安全错误。本地开发常用vite的proxy代理来解决生产环境一般把资源放同域名下。还有一点从网络加载纹理是异步的一开始渲染时材质可能还是黑色等纹理加载完需要手动把needsUpdate设为true或者用LoadingManager统一管理加载进度。模版提供一个loading状态展示避免页面打开后几秒里场景一片黑用户以为渲染坏了。5.3 性能排查帧率、内存与垃圾回收第一次跑模版项目如果遇到卡顿先打开Performance面板看FPS和GPU占用。常见原因有几个一是像素比没限制二是阴影贴图分辨率过高三是每帧都创建临时对象Vector3、Matrix4等造成频繁的内存分配和垃圾回收。three.js文档对临时对象有个建议“尽量复用”我把这个写进模版的编码约定里动画循环里优先使用模块级临时变量避免每帧new对象。还有一个容易忽略的资源泄漏点从场景里移除mesh时它的geometry和material不会自动释放。模版里的disposeScene函数会递归遍历场景所有对象对geometry、material、texture逐个调用dispose方法。写这个函数花了我不少时间但确实很值得特别是单页应用里频繁切换3D页面时内存能稳定住。我个人在实际操作里还有一个体会3D项目里“能跑”和“稳定跑”之间的距离往往就是这些边界细节。这份模版收敛了大部分常规项目会遇到的坑真正上手做业务时剩下的工作基本集中在内容创意和交互打磨而不是和底层API的细节搏斗。如果你打算在vue项目里尝试3D不妨以这份模版作为起始点跑两个demo感受一下再按自己的业务习惯去调整。本文还有配套的精品资源点击获取
返回列表