一款受青春独立电影、复古录像带与定格动画启发的 Three.js 网页游戏(垂直切片)。 三个短章节、一个核心特色:MEMORY FRAME RATE —— 角色姿态按 1–60 FPS 逐格采样,而移动、相机、输入、音频和渲染保持流畅。
demo-preview.mp4
npm install
npm run dev # 开发
npm run build # 类型检查 + 生产构建 → dist/
npm run preview # 预览 dist/构建产物是纯静态文件(base: "./"),可以直接部署到任意静态托管 / CDN。
| 按键 | 作用 |
|---|---|
WASD / 方向键 |
移动 |
Space |
跳跃 |
Shift |
第一章:滑板;第三章:冲刺 |
E |
互动 / 拾取 / 调查 |
| 鼠标拖拽 | 转动镜头(第一章) |
F |
打开/收起帧率面板 |
1 2 3 4 |
8 / 12 / 24 / 60 FPS 预设 |
Enter |
跳过演出 |
Esc / P |
暂停(窗口失焦也会自动暂停) |
触屏设备自动切换为触控操作:左半屏任意位置按下出现浮动摇杆(模拟量,推一半慢走);右下角 跳 / 互动 / 滑板(第三章为冲刺) 按钮;右半屏滑动转镜头;顶部 ❚❚ 暂停,过场中出现 跳过 按钮。竖屏时提示横屏,点开始时尝试全屏并锁定横屏(iOS Safari 不支持锁定,会显示横屏提示)。手机默认 LOW 画质(关闭 Bloom 与阴影,像素比上限 1.25),可在暂停菜单切回 HIGH。需要支持 WebGL2 的浏览器(iOS 15+ / 近年 Android Chrome)。
P0
- 完整流程:标题 → CH01 GOLDEN HOUR → CH02 STATIC DREAMS → CH03 THE LAST TRACK → 结尾 / Credits → 重玩、按章节重玩、回到标题
- 主角控制:行走、滑板(带板)、奔跑 / 冲刺、跳跃(土狼时间 + 输入缓冲)、AABB 碰撞、台阶、可跳上的障碍
- AnimationSampler:独立的单调动画时钟,只在采样边界计算骨骼;动作切换只在采样边界生效;脚步声绑定采样到的关键姿态
- 核心交互:拾取磁带、调查、播放磁带、躲避障碍(
Interactable+InteractionManager,同时只能执行一个交互) - 由 Timeline 驱动的章节转场、原创合成音乐、结尾演出、失败后从检查点恢复
P1
- 胶片颗粒、暗角、色彩分级(每章一套)、Bloom、按帧率步进的胶片抖动(gate weave)
- 电影镜头:开场摇镜、拾取特写 + 定格、推镜、照片闪回、列车经过的升降镜头
- 照片墙闪回(回忆中的“她”以 8 FPS 运动)、空椅子(环境音变闷 + 音乐压低)、更多环境音(鸟、远处车辆、时钟、蟋蟀)
- 帧率面板:1–60 滑块、8/12/24/60 预设、作用范围(仅角色 / 角色 + 演出镜头)、实测姿态/秒
P2(未做):换装、更多收集品、自由镜头、照片分享、隐藏结局。
blender/
├── scripts/ build_all · common · textures · characters · environment · props(Blender Python)
├── characters/ environments/ props/ .blend 源文件
├── textures/ 烘焙 / 渲染出的 T_*.png
└── previews/
public/assets/ Blender 导出的 GLB
src/
├── app/ main.ts · GameApp.ts(拥有全部系统,实现 GameContext)
├── core/ GameLoop · GameState(状态机 + 输入权限表)· Input · Physics · AssetManager(GLB 资产库)· dispose
├── player/ CharacterRig(Blender 蒙皮角色实例)· AnimationClips(ACT_* 剪辑索引)
│ AnimationSampler · CharacterAnimator · PlayerController
├── chapters/ Chapter(基类)· GoldenHour · StaticDreams · LastTrack
├── cinematics/ Timeline(事件配置 + 处理器注册表)· CameraDirector
├── audio/ AudioManager(Web Audio 合成音效/环境声)· Music(步进音序器 + 三首原创曲)
├── rendering/ PostProcessing(Bloom → OutputPass → 胶片 Pass)· Sky
├── interaction/ Interactable · InteractionManager
├── world/ Kit(实例化 Blender 资产)· bake(静态模块按材质合批)
├── ui/ GameHud · FpsControl · Screens(标题 / 加载 / 暂停 / 结尾)
└── config/ chapters.ts(章节配置)· settings.ts(持久化设置)
- 渲染帧率:
GameLoop每个requestAnimationFrame都渲染。 - 物理 / 输入:
PlayerController每帧用真实dt更新,与动画采样率无关。 - 动画采样率:每个角色的
AnimationSampler维护自己的动画时钟,只有跨过k / fps边界时才调用mixer.update(自上次采样以来的时间)。两次采样之间不做任何骨骼计算,姿态保持不变。
用 mixer.update(Δ) 而不是 mixer.setTime(),可以保留淡入淡出 / 混合权重的状态;每次采样后 mixer 内部时间与采样时钟完全一致。切章、暂停时调用 reset() 重新对齐,避免一次性补算很长的时间。
章节里没有散落的 setTimeout()。所有演出都是 TimelineEvent[] 配置:camera / light / audio / postprocess / animation / dialogue / transition / call。
- 时间只通过
update(dt)推进 → 暂停游戏即暂停演出 - 每个事件只触发一次;带
duration的事件获得补间进度 skip()补完所有补间并丢弃标记为skippable的音效;stop()(卸载章节)返回false,章节代码据此不再恢复控制权
音乐由 AudioContext 时钟驱动:音序器提前约 120 ms 排程,并把每一拍的精确音频时间放进队列;游戏每帧只取出“音频时钟已经到达”的拍子(第三章的路灯闪烁、镜头震动、色差脉冲)。渲染帧率波动不会造成音画漂移。
所有美术资产都在 Blender 4.2 中制作:角色、骨骼、动作、场景模块、道具、贴图,甚至照片、海报、磁带标签、窗外夜景。游戏代码里不生成任何网格或贴图,只负责摆放、打光、播放 Blender 导出的内容。
npm run assets # = blender -b --factory-startup -P blender/scripts/build_all.py
BLENDER=/path/to/blender npm run assets
blender -b -P blender/scripts/build_all.py -- characters # 只重建某一部分:textures / characters / environment / props一次完整重建约 70 秒,结果完全可复现:
| 步骤 | Blender 中的做法 | 产物 |
|---|---|---|
| 贴图 | 程序化着色器节点(Noise / Voronoi 映射在 4D 环面上,保证无缝平铺)→ Cycles EMIT 烘焙 | blender/textures/T_Asphalt / T_Sidewalk / T_Grass / T_Siding_* / T_Shingles_* / T_Wood_* / T_Wallpaper / T_Hazard |
| 2D 美术 | 平面几何 + 文字物体排版 → 正交相机 Cycles 渲染 | T_Polaroid_0..5 / T_Poster_* / T_Cassette_Label / T_Rug / T_StopSign / T_Window_Night / T_Memory_Sunset |
| 角色 | 见下方「三位主角」 | blender/characters/{protagonist,friend,memory}.blend → public/assets/characters/*.glb |
| 场景 | 40+ 个 ENV_* 模块:12 种房屋、车库(独立的卷帘门子物体)、道路 / 人行道 / 草地平铺块、围栏、3 种树、电线杆与电线、路灯、7 色汽车、路障、垃圾桶、锥桶、木箱、铁道口、列车、卧室、天台、天空球 |
blender/environments/environment_kit.blend → environment_kit.glb |
| 道具 | 25 个 PRP_*:磁带、录音机(磁带轮为子物体)、滑板、信箱、自行车、篮球架、床、书桌、空椅子、台灯、地毯、唱片架、海报、6 张拍立得、串灯… |
blender/props/props_kit.blend → props_kit.glb |
参考风格:青春独立动画(长脸、厚重上眼睑、大虹膜、浓眉、长鼻梁)。
| 角色 | 造型 |
|---|---|
主角 protagonist |
金色波波头 + 侧分刘海、橙色头戴耳机、青绿棒球夹克(罗纹领 / 门襟 / 袖口 / 下摆)、奶白 T 恤、牛仔裤、红色球鞋 |
阿森 friend |
红色罗纹毛线帽、深色长卷发、浓眉、绿色眼睛、耳钉、灰色宽松 T 恤、卡其裤 |
她 memory |
黑色长直发 + 齐刘海、深褐虹膜、深色高领毛衣、百褶短裙、短靴 |
制作流程(leads.py / faces.py / bodies.py / game_leads.py,运行 build_leads.py):
- SDF 雕刻(
sdf.py):头骨、面部体块、颧骨、眉弓、鼻梁/鼻尖/鼻翼/鼻孔、嘴唇与口裂、耳廓、颈部、躯干、四肢、五指手、衣物(布料褶皱置换)、发型(发丝沟槽置换)、毛线帽罗纹、耳机,全部用平滑并集 / 差集 / 交集组合;Surface Nets 成网,再用牛顿步投影回等值面。雕刻姿势为 A-pose(手臂外展 15°),避免袖子与衣身粘连。 - 眼睛:独立眼球 + 程序化虹膜(瞳孔、放射纤维、角膜缘暗环、清漆高光),上下眼睑壳体,上眼睑边缘加厚成睫毛线,外眼角挑线;眉毛按皮肤表面贴合生成。
- 材质(
looks.py):皮肤次表面散射 + 腮红 / 鼻尖 / 耳廓泛红 + 唇色遮罩;布料 sheen + 织纹;头发条纹 + sheen。 - 游戏网格:逐部件减面(每人约 3–4 万三角面),多物体 Smart UV 共享一张 2048 图集;Cycles 烘焙:程序材质颜色、逐部件高模→低模切线空间法线、AO,合成 BaseColor。
- 绑定:解剖朝向骨骼(与游戏内其他角色同名),按 SDF 部位归属(躯干 / 左右臂 / 左右腿)分区计算平滑权重;头部配件刚性跟随头骨,长发在颈部以下过渡到胸骨,裙子按高度在胯与大腿之间过渡。
- 表情 Shape Key:
Blink / Wide / BrowUp / BrowSad / MouthOpen / Smile,游戏内自动眨眼并由剧情 Timeline 驱动表情。 - 姿势库:16 个
ACT_*动作(行走、奔跑、跳跃、滑板、坐、举磁带、挥手、回头、摔倒……以及新增的LookUp仰望、Listen听歌、Point指向、Shrug耸肩、Cheer欢呼),已标记为 Blender 资产,可在 Asset Browser 的姿势库里直接套用。 - 展示渲染:
render_leads.py -- portraits | lineup | poses | night→blender/previews/leads_*.png。
命名与组织遵循策划案 4.3:CHR_ / RIG_ / ACT_ / ENV_ / PRP_ / M_ / T_,Collection 为 CHARACTER / ENVIRONMENT / PROPS / LIGHTING / CAMERAS。导出时应用变换、统一米制与朝向(游戏空间 Y 朝上、角色面向 +Z),贴图打包进 .blend 和 GLB。
动作数据只存在于 Blender。游戏端的 AnimationSampler 以 1–60 FPS 对这些 Action 采样:12 FPS 时每秒恰好 12 个姿态,与渲染帧率无关。
预览图见 blender/previews/(贴图、动作姿态、场景模块、道具)。
- 需要 WebGL2;桌面用键盘 + 鼠标,手机 / 平板用触控(见上文)。
- 画质菜单(暂停界面)可切到 LOW:关闭 Bloom 和阴影、像素比 1。
- 天空渐变和胶片效果是渲染层的着色器(作用在 Blender 导出的天空球和画面上),不属于美术资产。
