摘要
圣诞节快到了,作为程序员,送礼物当然要送点不一样的!本文将带你用 Three.js(3D 渲染)、MediaPipe(AI 手势识别)和 GSAP(动画引擎),从零打造一个酷炫的互动式 3D 圣诞树相册
握拳召唤圣诞树,张手炸裂成金色星云,捏合手指还能查看我们的独家回忆照片。拒绝千篇一律,今年过个赛博朋克风的圣诞节!🎅🚀

🎄 效果演示

先看最终效果(脑补一下画面,建议大家运行源码体验):

  1. 静默状态:屏幕漆黑,只有微弱的星光。

  2. 握拳(✊):无数金色的灯泡、红色的金属球和我们的照片从四面八方飞来,汇聚成一颗高质感的 3D 圣诞树,流光溢彩。

  3. 张开手(🖐):整棵树瞬间“炸裂”,化作漫天金色粒子雨(星云态),照片在太空中漂浮旋转,仿佛置身银河。

  4. 捏合(👌):选中空中的某张照片,它会自动飞到眼前放大,背景虚化,进入“回忆时刻”。

技术栈: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;

 遇到的坑与优化方案

在开发过程中,我遇到了几个比较棘手的问题,这里分享给大家避坑:

  1. 问题:画面看起来灰蒙蒙的,颜色不正。

    • 解决:Three.js 新版需要手动设置色彩空间。

      renderer.outputColorSpace = THREE.SRGBColorSpace;
      renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调
    • 问题:金属球一片死黑,只有高光。

      • 解决:金属需要反射环境!一定要加 RoomEnvironment 或者 HDR 贴图。光靠打灯是不够的。

    • 问题:Bloom 辉光让所有东西都发光,像核爆一样。

      • 解决:调整 UnrealBloomPass 的 threshold(阈值)。将其设为 0.85,这样只有 emissiveIntensity 很强的小灯泡会发光,普通的红球不会发光。

    • 问题:照片在树上看起来像纸片,侧面看就没了。

      • 解决:给照片加了一个“三明治”结构(正面照片+白色边框+背面照片),并在生成时计算好朝向,让它在树上时“背靠树干”,在散开时“面向摄像机”。

🚀 源码分享

这个项目完全基于前端技术,无需服务器,直接打开 HTML 就能跑。

使用方法:

  1. 新建一个 index.html 文件。

  2. 将完整代码粘贴进去(代码太长,完整版见文末链接或 Gitee 仓库)。

  3. 注意:由于使用了 ES Module 和摄像头权限,建议使用 VS Code 的 Live Server 插件运行,或者在本地搭建简单的 HTTP 服务(python -m http.server)。直接双击打开可能无法加载模块。

  4. 允许浏览器使用摄像头权限。

  5. 点击“上传照片云”按钮,选择几张你们的合照,就可以开始玩了!

  6. [🔗 点击这里获取完整源码 (Gitee链接)]https://gitee.com/ding-juncai/3d-gesture-christmas-tree.git

🎅 写在最后

技术不只是冷冰冰的代码,它也可以是表达爱意的最强工具。
想象一下,当你女朋友看着屏幕上一堆粒子随着你的手势起舞,最后汇聚成一张张你们的甜蜜合影时,那种惊喜感绝对比买现成的礼物强多了!

如果你觉得这个项目有趣,欢迎点赞、收藏、关注三连! 祝大家圣诞快乐,代码无 Bug,发际线不后移!🎄🎁


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐