低多边形PS1风格短片 是做什么的?
一切皆代码:Claude为每个短片编写一个Blender脚本,在用户机器上渲染,查看帧并修复,然后构建声音和字幕。无需视频生成器。
已在 examples/lift-rocket/(“如果电梯像火箭一样快会怎样?”,34秒)上验证。在编写新短片之前先阅读该示例:它是模板。
要求(先检查,缺失则安装)
- Blender 4.2+(在5.3上测试过)。找到它:
blender在 PATH 上,否则C:\Program Files\Blender Foundation\<ver>\blender.exe//Applications/Blender.app/Contents/MacOS/Blender。 - Python 3.10+ 并安装
numpy pillow soundfile faster-whisper;ffmpeg在 PATH 上。 - 角色:Quaternius "Ultimate Modular Men"(CC0,免费,quaternius.com)。解压到
assets/quaternius-modular-men/或设置LOWPOLY_PACK指向其Individual Characters/Blends文件夹。 - 一个TTS语音(ElevenLabs或用户拥有的任何工具)。可选:
earshotMCP 来检查声音。
流水线(每个短片一个文件夹,复制 examples/lift-rocket/ 作为起点)
| # | 步骤 | 文件 / 命令 | 完成条件 |
|---|---|---|---|
| 1 | 脚本 | script.md | Hook问题 + 后果链,每个更糟更有趣 + 真实数字 + 干巴巴的梗。60-110词,对应30-40秒。每个数字都核对来源并写在事实列表中。 |
| 2 | 配音 | vo/takeA.mp3 | 录制2次,选择单词时间戳清晰的那次。充满活力的创作者声音,不是平静的(平静 = “无聊”反馈)。 |
| 3 | 单词时间戳 | python ../../scripts/words.py vo/takeA.mp3 | takeA_words.json 中无零长度单词。 |
| 4 | 分镜头列表 | script.md 中的表格 | 每句VO一个镜头,每1-3秒一个新画面,每个镜头对应一个单词时间。 |
| 5 | 场景 | build.py(Blender) | blender -b -P build.py -- frames --still 30,120,... 渲染静帧;在完整渲染前查看每个静帧。 |
| 6 | 预览+备注 | 先 python ../../scripts/preview.py . render 然后 serve | 用户没有未处理的备注(见预览循环)。 |
| 7 | 渲染 | blender -b -P build.py -- frames | 360x640 EEVEE PNG,f_0001.png... |
| 8 | 声音 | python build_mix.py | 音效放在画面上,VO在上,低保真背景在下,闪避处理。输出 mix_raw.wav + cues.json。 |
| 9 | 后期 | python post.py | PS1抖动、效果、单词高亮字幕、统计呼出 -> out/ 目录下1080x1920。 |
| 10 | 混流 | 见下方 | short.mp4,然后测量并检查同步。 |
帧规则通用:24 fps,第1帧 = t 0,F = lambda t: int(round(t * 24)) + 1。
混流指令:
ffmpeg -y -framerate 24 -i out/f_%04d.png -i mix_raw.wav -c:v libx264 -crf 16 -pix_fmt yuv420p \
-af "loudnorm=I=-15:TP=-1.5:LRA=11,aresample=48000" -c:a aac -b:a 192k -shortest short.mp4
稍后切换配音而不重新剪辑画面:
python ../../scripts/words.py vo/takeB.mp3 然后 python ../../scripts/align.py vo/takeA_words.json vo/takeB.mp3 --end 34(--end = VO背景音结束位置;输出长度为此结束时间加2秒。如果不加:最后一个单词加1.5秒,这会导致文件比画面短)
-> takeB_aligned.wav + takeB_aligned_words.json。将 build_mix.py(VO环境变量)和 post.py 指向它们。
预览循环(在完整渲染之前)
在浏览器中快速低分辨率预览,用户在时间线上留下备注,Claude修复并仅重新渲染内容发生变化的镜头。镜头 = build.py 中的摄像机标记。
| 操作 | 命令(在短片文件夹内运行) |
|---|---|
| 渲染预览 | python ../../scripts/preview.py . render(首次:所有镜头;之后:仅变化的镜头) |
| 打开备注页面 | python ../../scripts/preview.py . serve -> http://127.0.0.1:8765 |
| 读取备注 | python ../../scripts/preview.py . notes(按镜头分组,带有帧号) |
| 修复后 | 编辑 build.py,再次运行 render,然后 python ../../scripts/preview.py . done 3 5 --reply "你更改的内容" |
| 强制重渲染某镜头 | render --shots c_press,c_slam 或 render --all |
build.py必须接受--blend-only(构建场景,保存,不渲染)。参见示例。- 变化检测:每个预览帧获得一个指纹(活动摄像机、每个可见对象的变换、骨架姿态、面部交换、材质、灯光、网格数据)。仅当某个镜头的一帧发生变化时才重新渲染。未变化的镜头保留旧帧。
preview.json中的设置(可选):scale50(%),step2(每2帧),samples1,audio(默认mix_raw.wav,否则为VO),final= 完整流水线命令。页面的 "批准并渲染最终版" 按钮运行它们;notes在用户点击后打印 APPROVED。- 逐一修复备注,回复每一条,重新渲染,并告诉用户无需刷新:页面 会自动重新加载视频。重复出现的同类备注会成为此文件中的规则。
- 在 lift-rocket 上测量(RTX 3070 Ti 笔记本,820帧):首次预览76秒,一个镜头变化13秒, 无变化12秒。大部分帧渲染时间是固定开销,因此降低分辨率比减少渲染帧数帮助更小。
工具 API(scripts/lowpoly_kit.py,在Blender内部导入)
load_character(scn, "Suit")->(armature, meshes, actions)。角色:Adventurer, Beach, Casual_2, Casual_Hoodie, Farmer, King, Punk, Spacesuit, Suit, Swat, Worker。- 动作(原地,24种):Death, HitRecieve, HitRecieve_2, Idle, Idle_Neutral, Interact, Kick_Left/Right, Punch_Left/Right, Roll, Run, Run_Back/Left/Right, Walk, Wave, Gun_Shoot, Idle_Gun*, Idle_Sword, Sword_Slash, Run_Shoot。
add_faces(scn, arm, head, tag, kinds, spec, drop)在移动骨架之前使用。种类: neutral, blink, side, shock, grimace, squint, dazed, xeyes。drop删除包中的3D眼睛/眉毛。show_faces(faces, [(frame, kind), ...]):硬切换,每个面部保持到下一个。nla(arm, [(action, start, end, blend[, influence]), ...]):后面的条目在更高的轨道上。drive_root(arm, [(frame, x, y, yaw, ease), ...], last):yaw 0 面向 -Y(默认摄像机方向)。new_cam,aim,yaw_to,ps1_render_settings(scn, outdir)。- 场景集:
scripts/set_lift_lobby.py(以BUILD_ONLY=True执行,返回box,uv_world,mat)。 新场景集遵循相同模式:带像素纹理的盒子(interpolation = "Closest")。
视觉效果规则
- 渲染 360x640,
filter_size 0,AgX Punchy,平面着色;后期:15位色彩 + Bayer抖动,x3最近邻放大。 - 面部是扁平 32x28 像素贴花,硬切换;绝不平滑动画脸部。
- 受力时通过关键帧骨架缩放(z向下,xy向上)实现挤压。喜剧节拍:快进(2-3帧),保持。
- 字幕:1-3个词,粗体无衬线字体,白色黑描边,当前词黄色,底部三分之一(y 1300 of 1920)。
- 统计呼出:红色圆角框,顶部三分之一,4帧弹出,每个真实数字一个。
声音规则
- 每个音效都放置在它的峰值落在画面上时(
at(name, frame, db, peak=...))。 - 语音必须突出:VO标准化到 0 dBFS,音效总线 -11 dB,音乐 -7 dB,加上侧链闪避 语音说话时(音效 -5 dB,音乐 -9 dB)。电梯短片的v1版本音效为-3,音乐为0:“我 听不到旁白”。
- 在大的停顿拍子上切断音乐,之后重新引入。寂静成就了笑点。
- 主输出:loudnorm -15 LUFS,真峰值 -1.5 dBTP。
- 检查:使用 earshot 的
timeline检查 mp4 与cues.json(目标:每个提示在100毫秒内),measure检查 LUFS。
已遇过的错误(不要重复)
- 包网格默认渲染隐藏:
hide_render = False(工具包已处理)。 - 门网格已经位于关闭位置:只动画偏移量,否则它们会以0.7米的间隙关闭。
- 天花板低于约2.75米会在电梯大小的车厢内隐藏摄像机视野中的墙面显示器。
- 尚未应出现的角色:将其
location.z = -50直至轮到他的镜头。 - NLA单次动作过渡到保持:设置剪辑的
blend_out = 0,否则姿势会弹出1帧。 - 反向镜头变黑(未照亮的顶棚):添加前灯和一点顶棚发射。尽早渲染反向角度。
- faster-whisper 在 mp3 上可能因 PyAV 不匹配而失败:
words.py先用 ffmpeg 解码。 - 从 -0.04 秒开始的短语在对齐中导致空切片:用
max(0.0, ...)夹紧(已处理)。
成本(电梯短片,RTX 3070 Ti 笔记本)
渲染820帧耗时92秒,后期17秒,$0。角色、音效、音乐:$0(CC0 + 代码合成)。 配音:ElevenLabs 每次约 $0.10,每次短片录2次。
在交付之前
- 查看最终帧的缩略图(每约2秒一张),而不仅仅是静帧。
- 测量响度,检查同步,检查字幕与配音文本是否一致(拼写修正放入 post.py 的
FIX中)。 - 告知用户测量了什么以及哪些未经过人类听觉检查。