Low-poly PS1 短片
一切都是程式碼:Claude 為每部短片編寫一個 Blender 腳本,在用戶的機器上渲染,檢視幀、修正問題,然後構建聲音和字幕。不使用影片產生器。
已在 examples/lift-rocket/(「如果電梯像火箭一樣快?」,34 秒)上驗證。在撰寫新短片前請先閱讀該範例:它是模板。
要求(先檢查,缺少則安裝)
- Blender 4.2+(已在 5.3 上測試)。在 PATH 中尋找:
blender,否則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 | 鉤子問題 + 連鎖後果,每個更糟更有趣 + 真實數字 + 乾冷笑點。30-40 秒時長約 60-110 字。每個數字都須查證來源並寫入事實列表。 |
| 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 | SFX 放置於畫面幀上,VO 在上方,低保真背景音樂在下,側鏈壓縮。輸出 mix_raw.wav + cues.json。 |
| 9 | 後製 | python post.py | PS1 抖動、效果、單字高亮字幕、統計數據標註 -> out/ 目錄下 1080x1920 格式。 |
| 10 | 封裝 | 見下方 | short.mp4,然後測量並檢查同步。 |
幀規則:24 fps,幀 1 = 時間 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 背景音樂停止處;輸出長度為 end + 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(每隔一幀)、samples1、audio(預設mix_raw.wav,否則為 VO)、final= 完整流程的指令。頁面上的 「核准並渲染最終版」按鈕會執行這些指令;當使用者點擊該按鈕時,notes會列印 APPROVED。- 逐一修正筆記,對每個筆記回覆,重新渲染,並告訴使用者無需重新整理:頁面 會自動重新載入影片。重複的同類筆記會成為此檔案中的規則。
- 在 lift-rocket(RTX 3070 Ti 筆記型電腦,820 幀)上測量:首次預覽 76 秒,一個鏡頭變更 13 秒, 無變更 12 秒。大部分幀的渲染時間是固定開銷,因此降低解析度的幫助 不如減少渲染幀數。
Kit 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/1920)。
- 統計數字標註:紅色圓角框,上方三分之一,4 幀內彈入,每個真實數字一個。
聲音規則
- 每個 SFX 的放置位置應使其峰值落在畫面幀上(
at(name, frame, db, peak=...))。 - 語音必須清晰:VO 歸一化至 0 dBFS,SFX 匯流排 -11 dB,音樂 -7 dB,此外在語音播放時 進行側鏈壓縮(SFX -5 dB,音樂 -9 dB)。電梯短片的 v1 版本 SFX 為 -3,音樂為 0: 「我聽不到旁白」。
- 在大的停頓節拍處音樂驟停,然後再恢復。沉默能強化笑點。
- 主輸出:loudnorm -15 LUFS,真實峰值 -1.5 dBTP。
- 檢查:對 mp4 使用 earshot
timeline與cues.json比對(目標:每個提示點在 100 ms 內),使用measure檢查 LUFS。
已遇到的錯誤(請勿重複)
- 角色包網格預設為渲染隱藏:
hide_render = False(Kit 已處理)。 - 門網格已位於關閉位置:僅動畫化偏移量,否則它們會關閉時留下 0.7 公尺的間隙。
- 低於約 2.75 公尺的天花板會隱藏電梯車廂內牆上的顯示器,使其無法被攝影機看到。
- 暫時不該被看到的角色:在其鏡頭開始前,設定位置關鍵影格
location.z = -50。 - NLA 一次性動作過渡到保持狀態:將剪輯的
blend_out = 0,否則姿勢會彈回一幀。 - 反向鏡頭變黑(天花板未照明):添加前置燈光和一點天花板自發光。提早渲染 反向鏡頭。
- faster-whisper 在 mp3 上可能因 PyAV 不匹配而中斷:
words.py先使用 ffmpeg 解碼。 - 從 -0.04 秒開始的短語會在校準中產生空片段:使用
max(0.0, ...)進行鉗制(已修正)。
成本(lift 短片,RTX 3070 Ti 筆記型電腦)
渲染 820 幀需 92 秒,後製 17 秒,成本 0 美元。角色、SFX、音樂:0 美元(CC0 + 程式碼合成)。 語音:ElevenLabs 每次約 0.10 美元,每部短片 2 次。
在提交之前
- 檢視最終幀的縮略圖(大約每 2 秒一張),而不僅僅是靜態幀。
- 測量響度,檢查同步,檢查字幕與 VO 文字是否一致(拼寫修正請在
post.py的FIX中進行)。 - 告訴使用者哪些是經過測量的,哪些未經人工聆聽檢查。