【硬核浪漫】用 Three.js + MediaPipe 手搓一个“手势控制”的 3D 粒子圣诞树,女朋友感动哭了!✨
本文介绍了一个融合3D渲染、AI手势识别和动画引擎的互动式圣诞树相册项目。通过Three.js实现高质感3D圣诞树效果,MediaPipe进行实时手势识别(握拳召唤圣诞树、张手炸裂成星云、捏合查看照片),GSAP处理动画过渡。文章详细解析了物理材质渲染、粒子系统状态切换等核心技术实现,并分享了开发中的优化经验。该项目无需后端支持,纯前端实现,为节日礼物提供了独特的科技创意方案。
摘要:
圣诞节快到了,作为程序员,送礼物当然要送点不一样的!本文将带你用 Three.js(3D 渲染)、MediaPipe(AI 手势识别)和 GSAP(动画引擎),从零打造一个酷炫的互动式 3D 圣诞树相册。
握拳召唤圣诞树,张手炸裂成金色星云,捏合手指还能查看我们的独家回忆照片。拒绝千篇一律,今年过个赛博朋克风的圣诞节!🎅🚀
🎄 效果演示
先看最终效果(脑补一下画面,建议大家运行源码体验):
-
静默状态:屏幕漆黑,只有微弱的星光。
-
握拳(✊):无数金色的灯泡、红色的金属球和我们的照片从四面八方飞来,汇聚成一颗高质感的 3D 圣诞树,流光溢彩。
-
张开手(🖐):整棵树瞬间“炸裂”,化作漫天金色粒子雨(星云态),照片在太空中漂浮旋转,仿佛置身银河。
-
捏合(👌):选中空中的某张照片,它会自动飞到眼前放大,背景虚化,进入“回忆时刻”。
技术栈:HTML5 / CSS3 / JavaScript (ES6+)
核心库:Three.js (r160), MediaPipe Hands, GSAP
🛠️ 核心技术原理解析
1. 极致的视觉质感:物理材质与光影 (PBR)
很多 Three.js 初学者的作品看起来像“塑料”或“纸片”,主要原因是材质和灯光没调好。
为了还原真实圣诞球那种车漆般的光泽,我没有使用普通的 MeshLambertMaterial,而是使用了 MeshPhysicalMaterial。
关键点:
-
Clearcoat (清漆层):模拟球体表面打蜡的效果。
-
Metalness (金属度):红球设低一点保留颜色,金球设高一点反射环境。
-
RoomEnvironment:这是最关键的一步!如果没有环境贴图,金属材质就是黑色的。我引入了 RoomEnvironment 给场景一个虚拟的“摄影棚”,让球体有东西可以反射。
codeJavaScript
// 红色烤漆球材质示例
const redMat = new THREE.MeshPhysicalMaterial({
color: 0x880000, // 深红底色
metalness: 0.2, // 降低金属度,避免发黑
roughness: 0.1, // 极度光滑
clearcoat: 1.0, // 100% 清漆层,高光锐利
clearcoatRoughness: 0.05,
reflectivity: 1.0,
});
// 强光小灯泡(配合 Bloom 辉光)
const lightBulbMat = new THREE.MeshStandardMaterial({
color: 0xffffee,
emissive: 0xffaa00,
emissiveIntensity: 15.0, // 强度拉满,亮瞎眼
toneMapped: false // 忽略色调映射,保持纯亮
});
2. 粒子系统的“聚”与“散”
这项目的核心玩法是状态切换。每一个粒子(装饰球、照片)都有两个目标位置:
-
TreePos (树形态):基于圆锥体的随机分布。
-
ScatterPos (星云形态):基于球体的爆炸散射分布。
算法揭秘:
A. 圣诞树形态(自然堆叠)
为了不让树看起来像个完美的圆锥体(太假),我在计算位置时加入了随机扰动,并让内部也有填充,模拟真实的堆叠感。
// 计算圆锥体位置
const percent = i / count;
const y = -height/2 + percent * height;
const radiusAtHeight = (1 - percent) * radius;
// 半径随机化,让一部分球陷进去,更有体积感
const r = radiusAtHeight * (0.4 + 0.6 * Math.sqrt(Math.random()));
const angle = i * 137.5 + Math.random() * 0.5; // 黄金角排列
B. 星云爆炸形态(Big Bang)
为了实现那种“炸开”的视觉冲击力,我使用了球面随机分布算法。
function getScatterPosition(i, total, radius) {
// 球面均匀分布算法
const phi = Math.acos(-1 + (2 * i) / total);
const theta = Math.sqrt(total * Math.PI) * phi;
// 随机扩散半径,形成内密外疏的星云感
const r = radius * (1.5 + Math.random() * 2.5);
return new THREE.Vector3(
r * Math.cos(theta) * Math.sin(phi),
r * Math.sin(theta) * Math.sin(phi),
r * Math.cos(phi)
);
}
3. AI 手势识别 (MediaPipe)
不仅要好看,还要能玩!我接入了 Google 的 MediaPipe Hands。
不需要后端,直接在浏览器前端实时识别手部 21 个关键点。
逻辑判断:
-
握拳:检测指尖与手腕的距离,如果都很近 -> 触发 STATE.CLOSED (变成树)。
-
张开:手指伸直 -> 触发 STATE.OPEN (炸开)。
-
捏合:计算食指指尖与大拇指指尖的距离 (Distance < 0.05) -> 触发射线检测 (Raycaster) -> 放大照片。
为了防止手抖导致画面抽搐,我还给手势坐标加了 Lerp 平滑插值:
// 平滑处理:当前值 += (目标值 - 当前值) * 系数
smoothHandX += (rawHandX - smoothHandX) * 0.15;
smoothHandY += (rawHandY - smoothHandY) * 0.15;
遇到的坑与优化方案
在开发过程中,我遇到了几个比较棘手的问题,这里分享给大家避坑:
-
问题:画面看起来灰蒙蒙的,颜色不正。
-
解决:Three.js 新版需要手动设置色彩空间。
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调 -
问题:金属球一片死黑,只有高光。
-
解决:金属需要反射环境!一定要加 RoomEnvironment 或者 HDR 贴图。光靠打灯是不够的。
-
-
问题:Bloom 辉光让所有东西都发光,像核爆一样。
-
解决:调整 UnrealBloomPass 的 threshold(阈值)。将其设为 0.85,这样只有 emissiveIntensity 很强的小灯泡会发光,普通的红球不会发光。
-
-
问题:照片在树上看起来像纸片,侧面看就没了。
-
解决:给照片加了一个“三明治”结构(正面照片+白色边框+背面照片),并在生成时计算好朝向,让它在树上时“背靠树干”,在散开时“面向摄像机”。
-
-
🚀 源码分享
这个项目完全基于前端技术,无需服务器,直接打开 HTML 就能跑。
使用方法:
-
新建一个 index.html 文件。
-
将完整代码粘贴进去(代码太长,完整版见文末链接或 Gitee 仓库)。
-
注意:由于使用了 ES Module 和摄像头权限,建议使用 VS Code 的 Live Server 插件运行,或者在本地搭建简单的 HTTP 服务(python -m http.server)。直接双击打开可能无法加载模块。
-
允许浏览器使用摄像头权限。
-
点击“上传照片云”按钮,选择几张你们的合照,就可以开始玩了!
-
[🔗 点击这里获取完整源码 (Gitee链接)]https://gitee.com/ding-juncai/3d-gesture-christmas-tree.git
🎅 写在最后
技术不只是冷冰冰的代码,它也可以是表达爱意的最强工具。
想象一下,当你女朋友看着屏幕上一堆粒子随着你的手势起舞,最后汇聚成一张张你们的甜蜜合影时,那种惊喜感绝对比买现成的礼物强多了!
如果你觉得这个项目有趣,欢迎点赞、收藏、关注三连! 祝大家圣诞快乐,代码无 Bug,发际线不后移!🎄🎁
更多推荐

所有评论(0)