集菜谱分享、社交互动、电商交易、会员体系、AI 智能客服、Three.js 3D 视觉体验于一体的全栈美食社区平台。
📑 目录
- 1. 项目概述
- 2. 技术栈详解
- 3. Three.js 3D 效果实现原理
- 3.1 架构设计
- 3.2 场景初始化
- 3.3 光照系统
- 3.4 相机与交互控制
- 3.5 3D 模型构建
- 3.6 动画系统
- 3.7 粒子特效
- 3.8 资源管理与性能优化
- 3.9 GLB 模型加载
- 3.10 Vue 组件集成
- 4. 总结
1. 项目概述
小厨暖光 (Yummly) 是一套面向美食爱好者的全栈社区平台,采用前后端分离架构。项目名称取意”小小厨房里温暖的光”,希望为每一个热爱烹饪的人打造一个分享与交流的线上家园。

核心能力一览
| 模块 | 功能描述 |
|---|---|
| 🍲 菜谱系统 | 发布/浏览/搜索/收藏菜谱,支持封面图、食材清单、分步教程、SKU 多规格 |
| 💬 社交互动 | 评论、点赞、收藏、关注、好友私信、美食圈动态 Feed |
| 🛒 电商交易 | 购物车、下单支付、订单流转、配送跟踪、钱包支付 |
| 👑 会员体系 | 青铜/白银/黄金/钻石四档,签到、积分、成长值、排行 |
| 🎫 营销工具 | 优惠券、秒杀、拼团、积分商城、抽奖转盘 |
| 🎨 3D 视觉 | Three.js 食物粒子动画背景,每页不同场景,鼠标跟随交互 |
| 🤖 AI 客服 | 基于 DeepSeek 的智能问答,支持文字对话 + 图片识别 + 菜谱推荐 |
| 🖥️ 管理后台 | 用户/菜谱/订单/库存/配送/营销/统计/日志全面管理 |
项目规模
| 指标 | 数量 |
|---|---|
| Java 源文件 | 95 |
| Vue/JS 文件 | 130 |
| 数据表 | 42 张 |
| API 端点 | 90+ |
| 前端页面 | 42 |
| 3D 场景类型 | 7 种 |
2. 技术栈详解
整体架构
┌──────────────────────────────────────────────────────────────┐
│ 浏览器 / 移动端 │
├──────────────────────────────────────────────────────────────┤
│ 前端 (Vue 2 + Element UI + Three.js) │
│ ┌──────────┬──────────┬──────────┬──────────┬───────────┐ │
│ │ Vue CLI │ Router │ Vuex │ Axios │ Three.js │ │
│ └──────────┴──────────┴──────────┴──────────┴───────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 后端 (Spring Boot 2.5.14) │
│ ┌──────────┬──────────┬──────────┬──────────┬───────────┐ │
│ │ Security │ JWT │ MyBatis+ │ Hutool │ Knife4j │ │
│ │ + CORS │ Token │ 分页 │ FastJSON│ Swagger │ │
│ └──────────┴──────────┴──────────┴──────────┴───────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 数据层 │
│ ┌────────────────────┬──────────────────────────────────┐ │
│ │ MySQL 8.0 │ 文件存储 (本地 upload/) │ │
│ └────────────────────┴──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
前端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue 2 | 2.6.14 | 渐进式前端框架,组件化构建 UI |
| Vue Router | 3.5.3 | SPA 路由管理,含导航守卫实现权限控制 |
| Vuex | 3.6.2 | 集中式状态管理,管理用户状态、权限、应用配置 |
| Element UI | 2.15.7 | 企业级 UI 组件库,提供表格、表单、弹窗等 60+ 组件 |
| Axios | 0.24.0 | HTTP 客户端,封装请求/响应拦截器处理 JWT 认证 |
| Three.js | 0.184 | WebGL 3D 渲染引擎,实现食物粒子动画背景 |
| QRCode | 1.5.4 | 二维码生成,用于菜谱分享 |
| Vue Quill Editor | 3.0.6 | 富文本编辑器,菜谱步骤编辑 |
| NProgress | 0.2.0 | 页面加载进度条 |
| SCSS | — | CSS 预处理器,编写可维护的样式 |
后端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.5.14 | Java 企业级后端框架,自动配置 + 起步依赖 |
| Spring Security | — | 认证授权框架,过滤器链处理 JWT + CORS |
| MyBatis Plus | 3.4.3.4 | ORM 增强工具,内置分页、代码生成、条件构造器 |
| JWT (jjwt) | 0.9.1 | 无状态 Token 认证,用户登录状态管理 |
| Knife4j | 3.0.3 | Swagger 增强 UI,自动生成 API 文档 |
| Hutool | 5.7.22 | Java 工具集,简化文件、加密、日期等操作 |
| FastJSON | 1.2.83 | 阿里巴巴 JSON 序列化库 |
| MySQL | 8.0 | 关系型数据库,utf8mb4 字符集 |
后端模块结构
| 模块 | 职责 |
|---|---|
yummly-common | 公共模块:AjaxResult 统一响应、BaseEntity 基类、全局异常处理、常量定义 |
yummly-framework | 框架模块:Spring Security 配置、JWT 过滤器链、MyBatis Plus 分页插件、WebMvc 跨域 |
yummly-system | 业务模块:30+ 实体类、20+ Mapper、Service 层、15+ REST 控制器(含管理端) |
yummly-admin | 启动模块:Spring Boot 入口、多环境配置文件、SQL 初始化脚本 |
部署架构
┌─────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ Frontend │ │ Backend │ │ MySQL │ │
│ │ (Nginx) │ │ (:8088) │ │ (:3306)│ │
│ │ :80 │ │ │ │ │ │
│ └────┬─────┘ └────┬─────┘ └───┬───┘ │
│ │ │ │ │
│ └─── proxy ───┘ │ │
│ │─────────────┘ │
└─────────────────────────────────────────┘
- 前端使用 Nginx Alpine 部署,配置 SPA 路由回退 + API 反向代理
- 后端使用 多阶段 Docker 构建(Maven 编译 → JRE 运行镜像)
- 前端通过
vue.config.jsdevServer proxy 在开发环境代理到后端 8088
3. Three.js 3D 效果实现原理
小厨暖光 的 3D 视觉系统是整个项目前端的一大亮点。它并非简单的第三方组件套用,而是一套完全从零构建的、面向美食主题的程序化 3D 场景系统。
设计理念
这套 3D 系统的核心设计原则是:
- 零外部资源依赖 — 所有蔬果模型(苹果、橙子、番茄、南瓜等)均通过 Three.js 原生几何体程序化生成,不依赖任何外部 3D 模型文件(除 plant 场景的 GLB 模型外)
- 场景-路由映射 — 7 种不同场景对应不同页面路由,每个页面的 3D 背景各具特色
- 性能优先 — 材质缓存、按需渲染、GPU 资源自动回收
- 交互自然 — OrbitControls 提供拖拽/缩放/平移,鼠标悬停激活控制
3.1 架构设计
整个 3D 系统封装在一个 Vue 单文件组件 ThreeBackground/index.vue 中,通过 scene prop 切换场景类型:
ThreeBackground 组件 (Vue SFC)
├── <template> — DOM 容器 + loading/hint 浮层
├── <script> — 3D 核心逻辑 (约 1500 行)
│ ├── 场景配置 — SCENE_COLORS 颜色映射
│ ├── 材质缓存 — mat() 工厂函数
│ ├── 模型工厂 — 16 种水果/蔬菜程序化建模
│ ├── 场景构建 — 7 个 buildXxx() 方法
│ ├── 粒子系统 — addParticles() 泛用粒子
│ ├── 动画循环 — requestAnimationFrame + Clock
│ ├── 交互管理 — OrbitControls + 鼠标事件
│ └── 资源清理 — beforeDestroy GPU 回收
└── <style scoped> — 容器/提示/hint 样式
3.2 场景初始化
场景初始化是整个 3D 系统的入口,在 Vue 的 mounted() 钩子中调用:
// 1. 创建 WebGL 渲染器
this.renderer = new THREE.WebGLRenderer({
alpha: true, // 透明背景,与页面 CSS 背景融合
antialias: true // 抗锯齿,边缘更平滑
})
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) // 限制像素比 ≤2
this.renderer.shadowMap.enabled = true // 启用阴影
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap // 柔和阴影
// 2. 创建场景 + 背景色 + 雾效
this.scene3d = new THREE.Scene()
const sc = SCENE_COLORS[this.scene] // 根据场景获取配色
this.scene3d.background = new THREE.Color(sc.light) // 渐变背景
this.scene3d.fog = new THREE.Fog(sc.light, 8, 28) // 远距离雾化
// 3. 创建透视相机
this.camera = new THREE.PerspectiveCamera(
50, // FOV 视场角
w / h, // 宽高比
0.1, // 近裁剪面
100 // 远裁剪面
)
this.camera.position.set(0, 1.2, 8.5) // 相机位置
this.camera.lookAt(0, -0.1, 0) // 注视目标
关键设计点
- alpha: true — 渲染器背景透明,3D 场景与页面的 CSS 背景无缝融合
- Fog 雾效 — 距离 8 单位开始出现,28 单位完全消失,营造空间深度感
- pixelRatio 限制 — 限制设备像素比不超过 2,在高 DPI 屏幕上避免性能浪费
- 每页不同配色 — 通过
SCENE_COLORS映射,7 种场景各有独特的主色和强调色 const SCENE_COLORS = {
garden: { light: 0xFFF5EE, accent: 0x228B22 }, // 菜园绿
kitchen: { light: 0xFFF5EE, accent: 0xFF6347 }, // 厨房红
dessert: { light: 0xFFFACD, accent: 0xFF69B4 }, // 甜点粉
market: { light: 0xFFFFF0, accent: 0xFFD700 }, // 市集金
cookbook: { light: 0xFFF8DC, accent: 0xD2691E }, // 菜谱棕
gathering: { light: 0xFFF5EE, accent: 0x4ECDC4 }, // 聚会青
error: { light: 0xDDDDDD, accent: 0x999999 }, // 错误灰
}
3.3 光照系统
一个逼真的 3D 场景离不开精心布置的光照。本系统采用五点光照方案:
// 1. 环境光 — 提供基础照明,消除纯黑阴影
this.scene3d.add(new THREE.AmbientLight(sc.light, 0.55))
// 2. 主光源 (Key Light) — 带阴影的方向光
const key = new THREE.DirectionalLight(0xFFF8F0, 0.9)
key.position.set(6, 8, 4) // 右上方照射
key.castShadow = true
key.shadow.mapSize.set(512, 512) // 阴影贴图分辨率
key.shadow.camera.near = 0.5
key.shadow.camera.far = 50
key.shadow.camera.left = -10 // 阴影相机视锥
key.shadow.camera.right = 10
key.shadow.camera.top = 10
key.shadow.camera.bottom = -10
key.shadow.bias = -0.0001 // 防止阴影条纹
// 3. 辅助光 (Fill Light) — 暗部补光,用场景强调色
const fill = new THREE.DirectionalLight(sc.accent, 0.35)
fill.position.set(-4, 2, -4) // 左后方
// 4. 轮廓光 (Rim Light) — 物体边缘高光
const rim = new THREE.DirectionalLight(0xFFFFFF, 0.25)
rim.position.set(0, -1, 3) // 下方前方
// 5. 半球光 (Hemisphere Light) — 天空-地面渐变光
const hemi = new THREE.HemisphereLight(0xFFFFFF, sc.accent, 0.2)
光照设计遵循三点布光法(Three-point Lighting)的扩展:
- Key Light 作为主光,携带阴影贴图,赋予场景立体感
- Fill Light 使用场景强调色,消除主光造成的暗部过黑,同时为暗面注入色彩
- Rim Light 从下方照射,分离物体与背景
- Hemisphere Light 模拟天空(白)到地面(accent色)的漫反射过渡
💡 扩展阅读:三点布光是影视和 3D 渲染中最经典的光照技术,源自好莱坞电影摄影。Three.js 通过不同类型的 Light 对象让我们能在 Web 中复现这一技术。
3.4 相机与交互控制
PerspectiveCamera — 透视相机
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 100)
this.camera.position.set(0, 1.2, 8.5)
- FOV = 50° — 略大于人眼自然视角(约 40-60°),画面不过分夸张
- Z = 8.5 — 相机距场景中心约 8.5 米,给蔬果模型留有充足空间
OrbitControls — 轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true // 阻尼惯性
this.controls.dampingFactor = 0.12 // 阻尼系数
this.controls.rotateSpeed = 1.2 // 旋转灵敏度
this.controls.zoomSpeed = 1.8 // 缩放灵敏度
this.controls.minDistance = 3.5 // 最近缩放距离
this.controls.maxDistance = 18 // 最远缩放距离
this.controls.maxPolarAngle = Math.PI * 0.68 // 限制俯仰角
this.controls.autoRotate = true // 自动旋转
this.controls.autoRotateSpeed = 0.25 // 自动旋转速度
this.controls.target.set(0, -0.2, 0) // 轨道中心
| 参数 | 作用 | 设计考量 |
|---|---|---|
enableDamping | 鼠标松开后有惯性滑动 | 模拟物理动量,手感更自然 |
minDistance=3.5 | 限制最近距离 | 防止相机穿透物体 |
maxDistance=18 | 限制最远距离 | 防止场景消失在雾中 |
maxPolarAngle | 限制俯仰角上限 | 防止翻到底部看到空白 |
autoRotate | 场景缓慢自转 | 无操作时场景也有动态感 |
鼠标悬停交互
// 鼠标进入容器 → 启用控制器 + 显示提示
activateControl() {
this.controls.enabled = true
this.hintVisible = true
}
// 鼠标离开容器 → 禁用控制器 + 淡出提示
deactivateControl() {
this.controls.enabled = false
// 800ms 后开始淡出,再 600ms 后完全隐藏
setTimeout(() => { this.hintFading = true }, 800)
setTimeout(() => { this.hintVisible = false }, 1400)
}
这样设计的好处是:用户将鼠标移动到 3D 背景区域时自动激活交互,移开时自动关闭,避免与其他页面元素的滚动/点击产生冲突。
3.5 3D 模型构建
这是整个 3D 系统最精彩的部分。所有蔬菜和水果模型完全通过 Three.js 原生几何体组合而成,不依赖任何外部模型文件。
材质缓存策略
const mats = {}
function mat(color, opts = {}) {
const k = `${color}_${JSON.stringify(opts)}`
if (!mats[k]) mats[k] = new THREE.MeshStandardMaterial({
color,
roughness: 0.35, // 默认粗糙度
...opts
})
return mats[k]
}
通过 key 对材质对象进行缓存,相同的 (color, opts) 组合只创建一次材质,多个几何体共享同一材质实例,减少 GPU 内存占用。
基础几何体工具库
| Three.js 几何体 | 用途 |
|---|---|
SphereGeometry | 番茄、橙子、葡萄粒、樱桃、桃子、蘑菇菌盖 |
CylinderGeometry | 果柄、茎干、玉米主体、蜡烛 |
ConeGeometry | 胡萝卜主体、花萼叶片 |
LatheGeometry | 苹果轮廓、梨轮廓、草莓轮廓、茄子轮廓 |
TorusGeometry | 西瓜条纹、南瓜棱纹、面包圈 |
BoxGeometry | 砧板、书页、木箱、彩椒棱 |
TubeGeometry | 香蕉弯曲体、葡萄茎曲线、樱桃茎曲线 |
IcosahedronGeometry | 橙子主体(比球体更有机) |
PlaneGeometry | 地面、花瓣、玉米外壳 |
CircleGeometry | 蘑菇菌斑 |
案例一:苹果 createApple()
苹果的建模展示了 LatheGeometry(旋转成型) 的经典用法:
function createApple(x, y, z, variant = 'red') {
const pts = []
// 通过 41 个采样点定义苹果的纵向轮廓曲线
for (let i = 0; i <= 40; i++) {
const t = (i / 40) * Math.PI // 从 0 到 π
let r = Math.sin(t) * 0.28 // 基础球形轮廓
if (t < 0.35) r *= 0.65 + 0.35 * (t / 0.35) // 底部收窄(苹果窝)
if (t > Math.PI - 0.25) r *= 0.78 + 0.22 * ... // 顶部收窄(苹果窝)
pts.push(new THREE.Vector2(r, Math.cos(t) * 0.3 + 0.02))
}
// LatheGeometry 将 2D 轮廓绕 Y 轴旋转成 3D 模型
const body = new THREE.Mesh(
new THREE.LatheGeometry(pts, 32), // 32 段旋转细分
mat(clr, { roughness: 0.25 }) // 低粗糙度模拟光滑果皮
)
// ... 添加果柄(CylinderGeometry)+ 小叶子(压扁的 SphereGeometry)
}
LatheGeometry 原理图解:
Y轴
↑
│ ● ← 顶部苹果窝(半径收窄)
│ ╱ ╲
│ ● ● ← 最宽处
│ ╲ ╱
│ ● ← 底部苹果窝
└────────→ X轴(半径)
将上方的 2D 轮廓曲线绕 Y 轴旋转 360°,生成 3D 苹果模型
这种技术的优势在于:
- 用极少的数据(41 个点)定义出复杂的有机形状
- 旋转细分(32 段)提供足够的圆滑度
- 可通过修改轮廓曲线的数学公式产生不同品种
案例二:香蕉 createBanana()
香蕉展示的是 TubeGeometry(管状几何体) 沿 3D 曲线延伸的能力:
// 1. 定义香蕉的 3D 弯曲路径
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, -0.55, 0),
new THREE.Vector3(0.12, -0.2, 0.06),
new THREE.Vector3(0.18, 0.15, 0.03),
new THREE.Vector3(0.1, 0.42, -0.04),
new THREE.Vector3(0, 0.58, 0)
])
// 2. 沿曲线生成管道(24 段细分,半径 0.065,8 边截面)
const tubeGeo = new THREE.TubeGeometry(curve, 24, 0.065, 8, false)
// 3. Z 轴压扁 0.7 倍,模拟香蕉的扁形截面
body.scale.set(1, 1, 0.7)
CatmullRomCurve3 通过 5 个控制点定义了一条光滑的 3D 样条曲线,TubeGeometry 将一个 8 边形截面沿此曲线挤出,天然形成了香蕉的月牙形弯曲。
案例三:葡萄串 createGrapes()
葡萄串展示了组合模式 + 算法化排列:
function createGrapes(x, y, z, color = 0x7B2D8B) {
const rows = [1, 3, 4, 5, 5, 4, 3] // 每行颗粒数(倒三角排列)
const grapeRadius = 0.055
for (let row = 0; row < rows.length; row++) {
const n = rows[row]
for (let i = 0; i < n; i++) {
const offsetX = (i - (n - 1) / 2) * grapeRadius * 2.2 // 水平均匀分布
const offsetZ = (Math.random() - 0.5) * 0.1 // 前后随机微移
const grape = new THREE.Mesh(
new THREE.SphereGeometry(grapeRadius, 8, 8),
mat(color, { roughness: 0.15 }) // 低粗糙度 = 光滑果皮
)
grape.position.set(offsetX, row * grapeRadius * 1.8, offsetZ)
g.add(grape)
}
}
// 再用 TubeGeometry 添加弯曲的茎
}
通过 rows 数组控制每行颗粒数量,形成自然的倒三角(上宽下窄)葡萄串形。
部分模型工厂一览
| 函数 | 核心几何体 | 特殊技巧 |
|---|---|---|
createApple | LatheGeometry | 上下收窄模拟苹果窝 |
createOrange | IcosahedronGeometry | 二十面体比球体更”有机” |
createBanana | TubeGeometry | CatmullRom 曲线 + Z轴压扁 |
createPear | LatheGeometry | 分段半径公式(窄→宽→窄) |
createWatermelon | SphereGeometry | Torus 贴球面做条纹 |
createGrapes | SphereGeometry × N | 倒三角行列排列 |
createStrawberry | LatheGeometry | 分段半径 + 种子点撒点 |
createLemon | SphereGeometry | Y轴拉伸 + 两端凸起 |
createCarrot | ConeGeometry | Torus 环纹 + 顶部叶子簇 |
createTomato | SphereGeometry | Y轴压扁 + 星形花萼 |
createBellPepper | SphereGeometry × 3 | 三瓣交叠 + 茎蒂 |
createBroccoli | SphereGeometry × 50 | 半球随机撒点模拟花球 |
createPumpkin | SphereGeometry | Y轴强烈压扁 + Box 棱纹 |
createEggplant | LatheGeometry | 下半部剧烈膨胀的轮廓 |
createMushroom | CylinderGeometry + SphereGeometry | 上半球截断做菌盖 |
createCorn | CylinderGeometry | 行列小球模拟玉米粒 |
createCherries | SphereGeometry × 2 | 双球 + 双曲线茎 |
createPeach | SphereGeometry | Y轴拉伸 + Z轴压扁 + 沟缝 + 红晕叠层 |
3.6 动画系统
动画循环使用经典的 requestAnimationFrame + THREE.Clock 方案:
animate() {
this.animationId = requestAnimationFrame(this.animate) // 递归调用
const t = this.clock.getElapsedTime() // 自场景创建以来的秒数
this.objects.forEach(o => {
// 1. 自转动画
if (o.rs) o.m.rotation.y += o.rs // 物体绕 Y 轴缓慢旋转
// 2. 浮动动画(非粒子物体)
if (o.fs && !o.isPts) {
const amp = o.m.userData?.floatAmp || o.fa // 浮动幅度
const spd = o.m.userData?.floatSpd || o.fs // 浮动速度
o.m.position.y = o.by + Math.sin(t * spd) * amp // sin 波上下浮动
}
// 3. 粒子旋转
if (o.isPts) o.m.rotation.y += 0.0008
})
this.controls.update() // 更新阻尼
this.renderer.render(this.scene3d, this.camera) // 渲染一帧
}
动画对象数据结构
每个加入场景的物体都携带一份动画元数据:
function addObj(ctx, mesh, opts = {}) {
ctx.objects.push({
m: mesh, // Three.js 对象引用
rs: 0.003, // rotation speed — 自转速度
fs: 0.5, // float speed — 浮动速度
fa: 0.06, // float amplitude — 浮动幅度
by: y, // base Y — 浮动基准 Y 坐标
isPts: false // 是否为粒子系统
})
}
每个被创建的蔬果模型也内嵌了 userData 用于个性化的动画参数:
g.userData = {
floatAmp: 0.04 + Math.random() * 0.06, // 0.04~0.10 随机浮动幅度
floatSpd: 0.5 + Math.random() * 0.5 // 0.5~1.0 随机浮动速度
}
随机化参数的意义:如果所有物体以相同的速度/幅度浮动,看起来像机械的军队。通过为每个物体赋予略微不同的 floatAmp 和 floatSpd,产生自然的、去同步化的浮动效果,模拟水中或空气中的漂浮感。
3.7 粒子特效
每个场景都布有漂浮粒子,增加氛围感:
addParticles(color, count, sx, sy, sz) {
const pos = []
// 在指定空间范围内随机分布
for (let i = 0; i < count; i++) {
pos.push(
(Math.random() - 0.5) * sx, // X: ±sx/2
Math.random() * sy - sy / 2, // Y: ±sy/2
(Math.random() - 0.5) * sz // Z: ±sz/2
)
}
const geo = new THREE.BufferGeometry()
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))
const mat = new THREE.PointsMaterial({
color,
size: 0.04,
transparent: true,
opacity: 0.55,
blending: THREE.AdditiveBlending, // 叠加混合 — 亮色叠加更亮
depthWrite: false // 不写深度缓冲,避免遮挡问题
})
const pts = new THREE.Points(geo, mat)
this.scene3d.add(pts)
this.objects.push({ m: pts, rs: 0.0015, fs: 0.7, fa: 0.02, by: 0, isPts: true })
}
| 参数 | 作用 |
|---|---|
AdditiveBlending | 叠加混合模式——粒子重叠处变得更亮,模拟光点/萤火虫效果 |
depthWrite: false | 不写入深度缓冲,粒子之间不会互相遮挡 |
transparent + opacity: 0.55 | 半透明,不喧宾夺主 |
3.8 资源管理与性能优化
3D 渲染是 GPU 密集型操作。当 Vue 组件被销毁(路由切换)时,如果不手动释放 WebGL 资源,会导致显存泄漏——每次进入页面都新增 GPU 内存占用而不释放,最终导致页面卡顿甚至崩溃。
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
if (this.controls) this.controls.dispose() // 销毁控制器
cancelAnimationFrame(this.animationId) // 停止动画循环
// 逐个清理所有场景物体
this.objects.forEach(obj => {
const mesh = obj.m || obj.mesh
if (mesh) {
mesh.traverse((node) => {
// 清理几何体(释放 GPU 顶点缓冲)
if (node.geometry) node.geometry.dispose()
// 清理材质(释放 GPU 纹理/着色器)
if (node.material) {
const mats = Array.isArray(node.material) ? node.material : [node.material]
mats.forEach(m => {
Object.keys(m).forEach(key => {
const val = m[key]
if (val && val.isTexture) val.dispose() // 清理纹理
})
m.dispose()
})
}
})
}
})
// 清理材质缓存
Object.values(mats).forEach(m => m.dispose())
// 销毁渲染器
if (this.renderer) {
this.renderer.dispose()
this.renderer.forceContextLoss() // 强制丢失 WebGL 上下文
}
}
完整的资源清理链路:
组件销毁
├── OrbitControls.dispose() — 移除事件监听
├── cancelAnimationFrame() — 停止渲染循环
├── geometry.dispose() × N — 释放 GPU 顶点/索引缓冲
├── material.dispose() × M — 释放 GPU 着色器程序
├── texture.dispose() × K — 释放 GPU 纹理内存
└── renderer.dispose()
└── .forceContextLoss() — 释放整个 WebGL 上下文
⚠️ 如果缺少这些清理代码,在 SPA 中频繁切换页面(进入→离开→进入)将导致显存持续增长。这是 Three.js 在单页应用中最常见的性能陷阱。
3.9 GLB 模型加载
除了程序化建模外,系统还支持加载 GLB 格式的外部 3D 模型(Plant 场景):
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
// 1. 配置 DRACO 解压器(大幅减小模型文件体积)
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/')
dracoLoader.setDecoderConfig({ type: 'js' })
// 2. 加载 GLB 模型
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.load(
'/models/strelitzia.glb',
(gltf) => {
// 3. 自动缩放适配场景
const box = new THREE.Box3().setFromObject(gltf.scene)
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
gltf.scene.scale.setScalar(3.5 / maxDim) // 等比缩放到 3.5 单位
// 4. 居中定位
gltf.scene.position.set(...)
this.scene3d.add(gltf.scene)
},
(progress) => { /* 加载进度回调 */ },
(error) => {
// 5. 加载失败 → 降级到程序化建模
this.buildFallbackPlant(sc)
}
)
| 技术 | 说明 |
|---|---|
| GLB 格式 | glTF 2.0 二进制格式,Three.js 原生支持,包含几何体+材质+动画 |
| DRACO 压缩 | Google 开发的 3D 网格压缩算法,可将模型文件缩小 10-25 倍 |
| Box3 自动缩放 | 读取模型的包围盒,自动缩放到适配场景的尺寸 |
| 降级策略 | 模型加载失败时自动回退到程序化建模,确保场景始终可用 |
3.10 Vue 组件集成
在页面中使用 3D 背景非常简单:
<template>
<div class="home-page">
<!-- 3D 背景层 -->
<ThreeBackground scene="garden" color="#FF6B35" />
<!-- 页面内容层(更高的 z-index) -->
<div class="page-content">
<h1>小厨暖光</h1>
<!-- ... 轮播图、菜谱卡片等 ... -->
</div>
</div>
</template>
<script>
import ThreeBackground from '@/components/ThreeBackground'
export default {
components: { ThreeBackground }
}
</script>
<style scoped>
.home-page {
position: relative;
min-height: 100vh;
}
.page-content {
position: relative;
z-index: 1; /* 内容在 3D 背景之上 */
}
</style>
ThreeBackground 组件渲染一个 position: absolute 的全屏 canvas,z-index: 0 确保它位于内容层之下。页面内容只需要设置 z-index: 1 即可覆盖其上。
7 种场景-路由对应关系
| 场景标识 | 页面 | 3D 内容 |
|---|---|---|
garden | 首页 / | 胡萝卜、番茄、南瓜、西兰花、玉米 + 菜园地面 |
kitchen | 登录 /login、注册 /register | 砧板上的苹果、橙子、柠檬、番茄、茄子、蘑菇、葡萄 |
dessert | 小吃专区 /snacks | 大篮子盛满水果,散落的苹果、橙子、葡萄 |
market | 秒杀 /seckill | 三个木箱摊位 + 西瓜、南瓜、玉米、番茄 |
cookbook | 做法大全 /encyclopedia | 打开的书本 + 环绕水果 + 蜡烛 |
gathering | 美食圈 /community/feed | 圆形地台 + 环形排列蔬果 + 中心葡萄 |
plant | 盆栽展示 | GLB 模型加载(含 DRACO 解压 + 降级方案) |
4. 总结
小厨暖光 展示了如何将 Three.js 深度集成到 Vue 单页应用中:
技术亮点回顾
- 零外部模型依赖 — 16 种蔬果模型全部通过 Three.js 原生几何体程序化生成,使用 LatheGeometry 旋转成型、TubeGeometry 曲线挤出、SphereGeometry 组合等纯数学建模技术
- 五点光照系统 — 环境光 + 主光(带阴影)+ 辅助光 + 轮廓光 + 半球光,复刻影视级三点布光法
- OrbitControls 智能交互 — 鼠标悬停激活/离开停用,避免与页面滚动冲突;阻尼惯性带来自然手感
- 去同步浮动动画 — 每个物体有独立的随机浮动参数,
Math.sin(t * spd) * amp产生自然的不规则漂浮感 - GPU 资源全生命周期管理 — 组件销毁时完整释放 geometry/material/texture/renderer,杜绝 SPA 显存泄漏
- GLB + DRACO 支持 — 外部模型加载 + Google Draco 压缩 + 加载失败降级方案
- 材质缓存策略 — 相同颜色和配置的材质只创建一次,多个几何体共享
- AdditiveBlending 粒子 — 叠加混合模式产生光点/萤火虫般的半透明粒子氛围
- Fog 雾效 + 场景配色 — 每页独立的背景色、强调色和雾效,营造各具特色的视觉氛围
技术栈总结
Vue 2.6 (组件化) + Three.js 0.184 (WebGL 渲染)
↕ RESTful API (Axios + JWT)
Spring Boot 2.5 (业务逻辑) + MyBatis Plus 3.4 (数据访问)
↕
MySQL 8.0 (持久化) + 本地文件存储 (upload/)
↕
Docker Compose (一键部署)
Made with ❤️ by Yummly Team | 小厨暖光,让美食温暖生活







