说实话,我第一次在公司年会上看到“3D圆球年会抽奖软件”跑起来的时候,心里想的是:这不就是把名单撒在一个球上转吗?但真正坐在操作间里看完第一轮抽奖,我才意识到它和大转盘、PPT抽号码完全不是一类东西。球体转起来,全场几百号人的名字绕着球面翻滚,所有人都会下意识抬头找自己的名字。停止的瞬间,灯光聚焦在一个人身上,现场气氛真的能起来。
我后来利用业余时间自己做了一个 Windows 可执行版,完整走了一遍从需求梳理、技术选型、核心功能实现到现场排障的过程,也用它实际撑过了两场年会。今天把这套经验完整拆开讲一遍,给准备做年会互动工具、或者想拿 WebGL 练手的开发者一个可以照抄的参考。
1. 为什么我要给一个抽奖工具做 3D 圆球
1.1 常见抽奖方案的现场差距
先说背景。大部分公司的年会抽奖,用的无非是这么几套:
- 网页抽奖平台:主持人打开网页,点一下“开始”,屏幕滚动名字,再点一下出结果。优点是方便,缺点是一旦年会场地网络不行,就直接抓瞎。我见过不止一次,大屏已经显示“正在连接服务器”,台下几百号人一起等转圈的尴尬场面。
- PPT 或 Excel 做随机数:优点是零成本,缺点是视觉上极其单薄,更像是在核对名单,而不是在“发奖”。尤其当奖项比较大时,全场没有一个集中视觉的焦点,气氛很容易冷。
- 大转盘:互动感不错,但只适合把奖品做成选项,不适合把几百个参与者的名字全部体现出来。转盘能承载的信息量很有限。
- 微信摇一摇:参与度高,但需要每个人都掏手机、连 WiFi,还会把现场互动变成“手机屏幕互动”,大屏反而成了配角。
这些方案本质上都满足“抽一个人出来”这个结果,但缺一样东西:视觉张力。年会抽奖表面上是一个随机过程,实际上是一场舞台表演。观众要看的是“全场所有人的名字都在一个空间里滚动,最后由系统选出一个焦点”,这种“所有人都在球上、所有人都有机会”的视觉隐喻,是平面列表滚动给不了的。
1.2 从现场需求倒推软件功能
我给自己列了一个需求清单,不追求大而全,只求在现场好用:
| 需求项 | 具体要求 |
|---|---|
| 参与规模 | 300 到 1000 人,名单可滚动导入 |
| 运行环境 | Windows 单机,全程离线可跑 |
| 名单来源 | Excel / CSV,兼容部门、工号、姓名等常见列 |
| 抽奖规则 | 多轮抽奖,已中奖者不再进入后续抽奖池 |
| 大屏效果 | 3D 球体旋转,名字清晰可见,停止时有聚焦动画 |
| 操作方式 | 快捷键启动/停止,不需要主持人碰鼠标 |
| 结果留存 | 每轮中奖名单自动记录,方便后续发奖核对 |
从中能看到一个核心矛盾:**现场电脑性能参差不齐,但视觉要求又不能太低。**所以后面所有技术选型,都要在这个前提下做决定。
1.3 明确不做什么
我也给自己划了底线:不做在线账号体系,不做微信联动,不做云端存储,不做多人在线互动。原因很简单,年会现场最不可控的因素就是网络。任何时候联网都可能成为故障源,离线单机才是低风险方案。后续再想加互动,也是在局域网内部做,而不是依赖公网。
2. 技术选型复盘:三套原型实测后的取舍
2.1 选型标准先定下来
我选技术栈时,主要看五件事:**开发速度、视觉效果上限、部署体积、现场电脑能否撑住、后续维护难度。**年会软件要求开发周期短,可能只有两三个晚上;同时它又要在别人公司的旧电脑上稳定运行,不能要求对方有独立显卡。这两个条件一叠加,就淘汰掉很多看起来很炫的方案。
我当时实际测过三套:
| 方案 | 开发速度 | 视觉上限 | 部署体积 | 低配置电脑表现 | 维护成本 |
|---|---|---|---|---|---|
| 原生桌面 UI 加 3D 控件 | 较慢 | 中 | 小 | 好 | 代码量多 |
| 大型 3D 引擎 | 快 | 极高 | 大 | 配置要求偏高 | 工程重 |
| Chromium 内核加 WebGL | 快 | 高 | 中等 | 可控 | 低成本 |
2.2 为什么最后选了 Chromium 加 WebGL 这套方案
很多人一听到 3D,第一反应是用大型 3D 引擎。我也试过,但建一个场景、调一套 UI、再打包成 Windows 安装包,整个工程重了不少,为了年会抽奖这个场景有点杀鸡用牛刀。它的渲染能力确实强,但体积和内存占用放到现场电脑上并不友好。
我最后用的是Chromium 桌面壳 + WebGL 3D 库。原因很实际:
- WebGL 社区里现成的球体、粒子、灯光、文字标签案例非常多,我可以直接改,不需要从零写矩阵变换。
- 操作界面用 CSS 就能做,比在 3D 引擎里拼 UI 快得多。现场操作员的控制台、中奖名单面板、按钮状态,这些本来就是典型的前端页面。
- 资源全部本地加载,不依赖网络。打出来的包可以做成绿色版,拷到 U 盘就能跑。
- 出问题的时候可以直接开调试工具看性能数据,查起来比黑盒式渲染要直观。
有人会担心 WebGL 在旧电脑上跑不动,我实测下来,只要把纹理尺寸和粒子数量控制好,核显电脑也能维持流畅。这一块后面有专门章节讲优化。
2.3 技术栈清单
具体环境是:Node.js 做桌面壳,WebGL 3D 库做渲染,原生 Canvas 做名字贴图,再用 Web Audio API 处理音效。名单解析用一个开源的表格解析库,能直接读 xlsx 和 csv。打包用 electron-builder 的 portable 模式,产出一个免安装 exe。
这套组合的意义在于:**界面、逻辑、渲染全部用同一种语言体系,改起来没有技术栈切换成本。**年会前一晚突然要增加一个“特别奖”按钮,我也能在十分钟内改完重新打包。
3. 圆球表面、名单翻滚与抽奖公平性的实现细节
3.1 名单导入与脏数据清理
真实名单永远是乱的,这是年会项目最容易被低估的部分。HR 给的 Excel 里可能有合并单元格、表头说明、备注列、重复人员,甚至有人名前后带空格。
我是这么处理的:
- 读取 Excel 后先跳过前几行,自动识别包含“姓名”“工号”“部门”的表头行;
- 将每一行数据格式化成
{ id, name, department }; - 去掉姓名字段首尾空格,过滤空行;
- 根据工号去重,没有工号时用部门加姓名组合判断。
这里有个细节:**抽奖展示时大屏上只显示姓名,但内部保存一定要带唯一 ID。**因为同一单位很容易有同名同姓的人,如果只按姓名去重,会把人吞掉或者让人重复中奖。用 ID 做防重才是最稳的。
3.2 球体建模与名字排布
球体本身用了一个半径 1 的球体网格,半透明材质。难点在于把几百个名字均匀贴在球面上,并且每个名字都能看清。
我最初试过按经纬度等距排布,结果发现两极地区名字挤成一片,赤道附近反而稀疏。后改用斐波那契球体分布,效果好了很多。斐波那契分布的核心逻辑很简单:
const total = names.length; for (let i = 0; i < total; i++) { const y = 1 - (i / (total - 1)) * 2; const radius = Math.sqrt(1 - y * y); const theta = i * 2.39996; // 黄金角近似值 const x = Math.cos(theta) * radius; const z = Math.sin(theta) * radius; sprite[i].position.set(x, y, z); }为什么这个分布均匀?因为它按照黄金角在球面上逐点“盘旋”填满整个球面,每个点之间的相对距离大致相等,不会出现纬线上那种越靠近两极越拥挤的情况。这一点在名字数量多的时候尤其关键,我试过 700 个人的名字贴上去,依然能保证每个人名之间有明显间距。
名字本身不直接用 Three.js 的 TextGeometry,那个加载字体太慢、渲染太费。我用 Canvas 预先画一个 128×64 的透明底图,把名字画上去,再做成 Sprite 贴到球面上。Sprite 的好处是始终朝向相机,不用旋转 3D 文本对象,观众从任何角度都能直接看到名字,不会出现名字被球体挡住的情况。
3.3 旋转动画与停止减速
球体旋转分两段:滚动阶段和停止阶段。
滚动阶段就是让整个名字组绕 Y 轴持续旋转,同时保留一个固定倾角,让球体看起来更立体。为了让观众看清名字,旋转速度不能太快,我控制在每秒 0.6 圈左右,大屏上看是“清晰可辨地滚动”,而不是一团模糊。
停止阶段不能像电闸断电一样瞬间定住,那样视觉上太突兀,也不像一个“抽奖仪式”。我用了减速缓动曲线:
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }停止时记录当前旋转角,在 1.5 秒内让旋转速度从当前值降到零。这个过程观众会感觉球体是“惯性滑落”到一个名字上,而不是被强制掐断。
这里有个视觉设计上的小心机:为了让最终中奖者看起来更“命中注定”,我在点击停止的一瞬间就从剩余名单里选好中奖者,然后计算这个人在球面上的角度,让球体在减速滑行中把该位置带到正前方。观众看到的结果就是:球体慢慢停下来,正中央正好是对准的那个人的名字。整个过程没有任何操作者干预,随机性发生在点击停止的那一刻。
3.4 抽奖公平性与防重逻辑
很多抽奖软件被人吐槽“暗箱操作”,其实问题不在黑幕,而在随机源不够好或者逻辑不透明。我为这个项目刻意保证了两点:
- 使用密码学安全随机源,而不是普通的伪随机数函数;
- 每一轮的抽奖池都从“未中奖名单”里取,已中奖者直接从池子里移除。
核心伪代码如下:
function pickWinner() { const pool = unclaimedList; // 未中奖名单 const array = new Uint32Array(1); crypto.getRandomValues(array); return pool[array[0] % pool.length]; }中奖者确定后,立即从unclaimedList中移除,同时追加到中奖记录里。这个数据更新必须在渲染动画开始前完成,否则容易出现“球停在一个已经中过奖的人身上”的事故。
还有一个容易忽略的点:**连开几个奖项时,很多人想当然地觉得先把名单打乱,然后“从头到尾依次取”就是公平的。**逻辑上同样公平,但大屏效果很差,因为规则可以被观众猜到:第一轮停了第 100 个人,第二轮就是第 200 个人,大家会去数规律,气氛全毁。所以每一轮都重新随机,而且不能让观众看出上一轮的位置和下一轮有任何关联。
3.5 音效、灯光与聚焦
抽奖软件本质是舞台道具,音效占了一半的体验。我用 Web Audio API 做了三段包装:
- 滚动阶段:轻快的循环背景音乐,音量压低,不抢主持人说话;
- 减速阶段:音乐渐弱,叠加一声“叮”的提示;
- 定格阶段:音量拉满一声胜利音响,同时输出“恭喜 xxx”的大字面板。
灯效上,球体周围放了一个光晕粒子环,停止时粒子向中奖者名字位置汇聚,同时把其余名字暂时调暗。这样全场视线会被强制拉到一个点上,效果比单纯文字弹窗好得多。
音效要注意的是预加载:所有音频文件在应用启动时就加载进内存,禁止现场点击时才去读取本地文件。延迟在那时会非常致命。
4. 现场最容易翻车的五个坑与完整排查链路
4.1 旧电脑掉帧:名字一多画面就开始卡
第一次内测时,我把 700 个名字铺到球面上,点击开始轮转,结果画面掉到十几帧。排查链路是这样的:先开性能面板看 GPU 占用,发现纹理上传量很大;再看每张名字贴图尺寸,原来每张 128×64 的透明图在 Canvas 上生成后没做尺寸归一化,字越大纹理越占显存。
结论是降低单位成本:名字贴图统一压缩到 64×32,同时关闭不必要的阴影计算,粒子数量减半,帧率立刻恢复到 40 帧以上。这个经验可以反推一句话:抽奖球上的文字没必要追求高清字体,重要的是清晰和快速。
4.2 接上大屏后字体变成方块
年会现场最常见的显示故障不是卡顿,而是字体丢失。主办方提供的电脑可能没有安装我开发时使用的艺术字体,球面上的人名全部变成方框。
排查后发现,系统自带的默认字体在 LED 大屏上渲染成粗黑体,笔画过粗,名字重叠严重。后来我直接把“思源黑体”的一部分子集打包进应用资源,通过 FontFace 加载,渲染时优先使用。这样不管对方电脑装了什么字体,软件呈现效果都一致。
4.3 双屏模式下鼠标偏移与全屏位置错乱
现场操作间通常会有一个主显示器用于操作者,外接一个大屏用于观众。全屏窗口默认只覆盖主显示器,但我代码里启动时强制覆盖外接屏,问题就来了:物理分辨率不同时,鼠标坐标与实际渲染位置对不上,点击“停止”按钮没反应。
排查原因是我用了系统推荐的“扩展显示器”模式,但没有重新校准设备坐标。解决方案有两个,我同时做了:
- 应用启动时读取显示器列表,用
setWindowBounds精确把窗口设置到外接屏的可用区域; - 现场操作方案改为键盘快捷键,传输所有鼠标按钮,彻底避开坐标问题。
这个坑很隐蔽,我建议大家做类似软件时,一定要在“双屏不同分辨率”的组合下完整测一遍,别只在自己一台电脑上自测。
4.4 操作员手滑:一个奖项抽出了两个中奖者
彩排时发生过一次事故:主持人喊停,操作员连按了两次停止键,动画播放完毕后结果显示的是第二个人的名字,而第一位中奖者已经被记入中奖名单,导致名单错位。
问题根源是停止逻辑没有做防抖,两次点击进入了两次独立流程。修复方案是加一个状态机:滚动中 -> 停止中 -> 已结束。只有“滚动中”状态才响应停止键,“停止中”和“已结束”状态直接忽略输入。此外,正式发布版本里我额外加了一个确认弹窗,但现场为了不破坏气氛,弹窗做成了一个半透明浮条,显示“松手确认:再按一次空格键锁定”,操作员按一次触发减速,再按一次才锁死。
4.5 离线环境下的音效加载失败
有一次彩排时我拔掉网线测试,发现背景音乐循环到第二遍就哑了。查下来才知道我写了一个“网络优先,本地兜底”的音效加载逻辑,本地文件读取路径在打包后发生了变化,兜底路径根本不存在。
修复方法是把所有音频文件打进安装包,启动时先直接读本地,任何网络路径都不走。打包后我会专门跑一遍断网测试,确保拔网线后功能完整可用。
5. 打包、压测与活动当晚的操作清单
5.1 打成绿色版单文件
交付现场的不是源码,而是一个免安装 exe。我用打包工具做成 portable 模式,双击即跑,不需要装任何运行库。资源和配置文件放在 exe 同级的 data 目录下,方便现场临时调整音效和名单。
打包时需要注意三点:第一,路径一律用应用运行目录计算,不能写绝对路径;第二,字体、音频、图片全部内嵌到资源目录,防止被系统清理工具误删;第三,关闭无关的系统权限申请,不要在任何杀毒软件弹窗上出现不明签名警告。
5.2 测试矩阵
我在交付前做了一张压测表,要求自己在三种配置上各跑一遍:
| 测试环境 | 分辨率 | 比例 | 名单容量 | 结果 |
|---|---|---|---|---|
| 办公台式机 | 1920×1080 | 16:9 | 500 | 流畅,字体正常 |
| 老款笔记本核显 | 1366×768 | 16:9 | 700 | 40 帧左右,可接受 |
| 外接 LED 大屏 | 非标分辨率 | 16:10 | 500 | 拉伸明显,需强制缩放 |
非标分辨率这一项尤其重要。很多 LED 大屏的“物理分辨率”不是普通显示器规格,如果按窗口默认比例播放,画面会被拉扁。我后来加了一个“保持宽高比并自动黑边填充”的模式,宁可两边留黑边也不拉伸。
5.3 活动当晚的操盘流程
按照真实年会经验,我给自己定了一张流程清单:
- 提前一小时接好所有线材,设置好外接屏的扩展/复制模式;
- 打开软件,导入当天最终确认版名单,核对总人数;
- 全场彩排抽一轮,确认音效从功放到音箱的输出音量;
- 关闭系统的自动更新、锁屏、屏保,插入电源线;
- 把中奖名单导出文件路径确认好,方便结束后生成待发奖名单。
5.4 应急兜底方案
再稳的软件都有翻车可能,所以我会同时准备一个普通网页版本的抽奖页面,放在 U 盘里。一旦 3D 版本因为某个环境问题跑不起来,操作员可以秒开备用的“姓名滚动抽奖”页面顶上去。这个页面没有球体,但能保证抽奖仪式不被中断。凡事保留一条退路,是我做现场工具最重要的习惯。
6. 这个 3D 抽奖还能长成什么样
6.1 多轮次与奖项等级
第一版只支持单次抽取一人。后来的版本扩展成了配置式抽奖表单:先定义奖项名称、数量、顺序,比如“五等奖抽 20 人,四等奖抽 10 人……特等奖抽 1 人”。每轮结束,中奖名单从球体上逐渐淡出,剩下的名字继续留在球上,观众能直观看到“池子里的人越来越少”。
这个设计非常有意思,它会制造一种天然稀缺感。尤其抽到最后特等奖时,球面上只剩几十个名字,观众会很激动。
6.2 品牌定制和主题皮肤
我给软件加入了球体纹理替换功能:在球面半透明层上叠加公司 logo 或年会主题色。Logo 不会遮挡名字,只要透明度控制在 30% 左右,既能有归属感又不会影响信息可读性。同样配色方案也可以用于中奖大字面板、按钮状态和粒子颜色。
6.3 设备联动与更多玩法
如果你们年会现场有自己的音控系统,3D 抽奖软件还可以输出触发信号给灯光控制台,让抽奖定格瞬间灯光效果同步。或者做成局域网内一个小程序码,让观众在手机端看到“自己是否仍在奖池中”,而不是依赖主持人宣读名单。
这些内容扩展到春节抽奖、校庆活动、门店促销抽奖也同样适用。核心思路不变:先保证离线、稳定、可配置,再谈效果和互动。
最后分享一个我个人的体会。做这类现场工具,最大的风险从来不是 3D 渲染写不出来,而是你没在“真实现场环境”里跑过。我第一次上线前自我感觉很良好,结果到现场才发现外接大屏的分辨率比例不对,名字全部拉长,手忙脚乱调了一刻钟才恢复。从那以后,我所有作品上线前都强制做一轮“垃圾电脑 + 陌生大屏 + 拔网线”的三连压测。你永远不知道现场会发生什么,能做的就是把所有能控制的变量提前锁死。希望这套 3D 圆球年会抽奖软件的完整拆解,能帮你少踩几个晚上熬夜排障的坑。