Three.js实现3D圆球抽奖效果的技术解析

1. 项目概述:3D圆球抽奖效果的设计初衷

去年公司年会前一周,行政部的同事突然找到我:"今年老板要求抽奖环节必须搞点新花样,传统的大转盘和九宫格都用腻了,能不能做个科技感强的3D效果?"这个临时需求让我花了三个晚上研究,最终开发出这个支持实时交互的3D圆球抽奖系统。它不仅完美撑起了年会高潮环节,后来还被多家兄弟公司借去使用。

这种3D圆球抽奖的核心优势在于:

  • 视觉冲击力远超2D抽奖形式
  • 支持自定义皮肤适配不同活动主题
  • 中奖名单可实时投影到大屏幕
  • 物理引擎模拟让结果更具随机性
  • 后台可灵活配置奖项和参与人员

2. 技术实现方案选型

2.1 主流3D技术对比

在技术选型阶段,我对比了三种常见方案:

技术方案优点缺点适用场景
Three.js成熟度高,文档完善需要WebGL基础复杂3D可视化
CSS 3D Transform实现简单性能较差简单3D效果
Canvas 2D+透视兼容性好3D效果有限轻量级应用

最终选择Three.js方案,虽然学习曲线较陡,但其完整的物理引擎支持和丰富的材质系统,能完美实现球体旋转、粒子特效等高级功能。

2.2 核心依赖库

项目主要依赖以下技术栈:

// 核心依赖 import * as THREE from 'three'; // 3D渲染引擎 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; // 摄像机控制 import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'; // DOM标签渲染 import { TweenMax } from "gsap"; // 动画补间

特别提示:Three.js的版本管理很重要,建议锁定具体版本号。我们项目中使用的是r128版本,新版本可能存在API变更。

3. 关键实现步骤详解

3.1 3D场景初始化

创建基础场景需要完成以下关键步骤:

// 初始化场景 const scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000); // 设置相机(透视摄像机) const camera = new THREE.PerspectiveCamera( 75, // 视场角 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近截面 1000 // 远截面 ); camera.position.z = 50; // 添加光源(环境光+点光源) const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const pointLight = new THREE.PointLight(0xffffff, 1, 100); pointLight.position.set(10, 10, 10); scene.add(pointLight);

3.2 球体与姓名标签创建

抽奖球体的实现有几个技术要点:

  1. 几何体创建:使用IcosahedronGeometry(二十面体)而非SphereGeometry,前者在旋转动画中更稳定
  2. 材质选择:MeshPhongMaterial配合envMap实现金属反光效果
  3. 姓名标签:通过CSS2DRenderer将DOM元素作为3D对象渲染
// 创建二十面体几何体 const geometry = new THREE.IcosahedronGeometry(15, 3); // 加载环境贴图 const textureLoader = new THREE.TextureLoader(); const envTexture = textureLoader.load('assets/env_map.jpg'); // 创建材质 const material = new THREE.MeshPhongMaterial({ envMap: envTexture, color: 0x2194ce, specular: 0x111111, shininess: 30, transparent: true, opacity: 0.8 }); // 创建球体网格 const sphere = new THREE.Mesh(geometry, material); scene.add(sphere); // 添加姓名标签 participants.forEach((name, i) => { const label = createNameLabel(name); const phi = Math.acos(-1 + (2 * i) / participants.length); const theta = Math.sqrt(participants.length * Math.PI) * phi; label.position.setFromSphericalCoords( 18, // 半径 phi, theta ); sphere.add(label); });

3.3 物理动画系统

为了让抽奖过程更具真实感,我们实现了以下动画效果:

  1. 初始旋转:球体缓慢自转展示所有参与者
  2. 抽奖加速:点击开始后转速逐渐加快
  3. 减速停止:随机时间后开始减速,最终停在获奖者位置
let rotateSpeed = 0; let isRolling = false; function startLottery() { isRolling = true; rotateSpeed = 0.02; // 加速动画 TweenMax.to(this, 3, { rotateSpeed: 0.5, ease: Power1.easeIn }); // 随机时间后开始减速 setTimeout(() => { slowDown(); }, 3000 + Math.random() * 5000); } function slowDown() { TweenMax.to(this, 4, { rotateSpeed: 0, ease: Power1.easeOut, onComplete: showWinner }); } function animate() { if (isRolling) { sphere.rotation.y += rotateSpeed; } requestAnimationFrame(animate); }

4. 实战经验与优化技巧

4.1 性能优化方案

在实际使用中我们遇到了几个性能瓶颈,以下是解决方案:

  1. 内存泄漏:每次抽奖后需要手动清理场景

    function resetScene() { scene.traverse(object => { if (!object.isMesh) return; object.geometry.dispose(); object.material.dispose(); }); }
  2. 移动端适配:通过检测设备类型降低画质

    const isMobile = /Mobi|Android/i.test(navigator.userAgent); const renderer = new THREE.WebGLRenderer({ antialias: !isMobile, powerPreference: isMobile ? "low-power" : "high-performance" });
  3. 预加载策略:使用LoadingManager管理资源加载

    const manager = new THREE.LoadingManager(); manager.onProgress = (url, loaded, total) => { updateProgress(loaded / total); };

4.2 年会现场应急方案

根据多次活动现场经验,必须准备以下应急措施:

  1. 双屏显示:主屏展示3D效果,备用屏准备静态获奖名单
  2. 本地缓存:将参与者名单和奖项设置保存在localStorage
  3. 离线模式:提前打包所有依赖库,避免网络问题
  4. 快捷键:设置空格键开始/停止,Enter键重置

现场血泪教训:永远要测试投影仪的色彩模式!我们第一次演示时因为投影仪色差导致球体变成难看的紫色,最后紧急调整了材质的环境光反射率。

5. 扩展应用场景

这个3D抽奖系统经过简单改造,还可以应用于:

  1. 团队随机分组:将成员名单导入系统,设置分组数量
  2. 任务分配:把待办事项做成彩色球体随机分配
  3. 教学点名:教育机构课堂互动工具
  4. 产品发布会:配合新产品3D模型做展示

最近我们新增了「爆炸效果」模式,抽奖结束后球体会炸开显示所有参与者名字,这个效果需要用到粒子系统:

function createExplosion() { const particles = new THREE.BufferGeometry(); const positions = []; const colors = []; for (let i = 0; i < 1000; i++) { positions.push( Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1 ); colors.push( Math.random(), Math.random(), Math.random() ); } particles.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); particles.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); const particleMaterial = new THREE.PointsMaterial({ size: 0.1, vertexColors: true }); return new THREE.Points(particles, particleMaterial); }

实现这个项目后最大的体会是:3D开发中60%的时间都在调试视觉效果和性能优化。建议在开发初期就建立完善的调试面板,我们最终使用的是dat.GUI来实时调整参数:

const gui = new dat.GUI(); gui.add(sphere.rotation, 'x', 0, Math.PI * 2); gui.add(sphere.rotation, 'y', 0, Math.PI * 2); gui.add(material, 'opacity', 0, 1);