塊狀卡通故事影片(「Busted by the Shoeshine Man」風格)
當使用者想要一部直向(720×1280 或 1080×1920,30 fps)的第一人稱故事影片,長度 30–130 秒,透過旁白講述,具有圓潤塊狀 3D 角色、流暢連續的動畫和搶眼的彩色字幕時,請使用此功能。一切皆由程式碼實現:在 headless Chromium 中的 three.js 場景,一個確定性的 frame(t) 函數,並將畫面傳送至 ffmpeg。
使用者的優先重點是角色設計的忠實度與動畫的流暢度。請勿跳過步驟 5 中的角色檢查。
1. 參考影片的樣貌(測量值)
- 720×1280,30 fps,約 122 秒,AAC 立體聲,音量約 -14 LUFS,背景有輕柔持續的墊底音樂襯托旁白。
- 約 47 次硬切(單幀切換,無淡入淡出),平均鏡頭長度約 2.6 秒(範圍 1.2–5 秒)。鏡頭內部始終在移動:緩和的推近、拉遠、吊臂/高角度飄移、低角度推鏡、向上快速搖攝至天花板或窗戶。少於 1% 的幀為靜態。
- 字幕位於下方三分之一處(頂部位於高度約 64-68%),每組 2–3 個小寫單字,粗體圓角幾何無襯線字體(Poppins ExtraBold),白色基底搭配關鍵字彩色(金色 #f5c32c,青色 #48c9f0,珊瑚色 #ff5b4d,粉色 #ff7ac0,綠色 #2ecc8f,橘色 #ffa21f),粗黑色描邊,每組有輕微的縮放彈跳效果。
- 環境:溫暖的奶油色/棕褐色室內(牆壁 #dcbf96,地板 #c7aa7d,石頭 #d9c39b,護牆板 #b99a6f,天空 #c7dcec),搭配重點色栗色 #570d0e,海軍藍 #1c2a49,金色 #e0990d,藍綠色 #1f8f8c,紅色 #a3160f。
- 燈光:柔和、明亮、略帶暖色;平滑的漸層著色和柔和陰影。無硬式 cel 色塊。
2. 角色設計聖經(嚴格遵循)
輪廓與比例
- 每個身體部位均為圓角盒狀。圓角半徑約為該部位最小尺寸的 15-18%。切勿對身體部位使用球體或膠囊體。
- 頭部:一個略寬於高的長方體(0.44 x 0.41 x 0.39 單位),在特寫鏡頭中約為角色身高的 1/4–1/3,因此在廣角鏡頭中角色約為 4 頭身。短脖子塊狀。軀幹 0.46 x 0.56 x 0.26。手臂 0.27 + 0.26(上臂 + 前臂)。腿部 0.30 + 0.27。總身高約 1.7 單位。
- 手部是厚實的連指手套塊:一個手掌塊,加上四個短的兩段式手指和一個獨立拇指。它們可以從張開到握拳彎曲並張開。手絕不能看起來像蜘蛛。
- 每個手腕處有白色袖口帶。黑色鞋子,一個前端圓潤的扁平塊。
- 無可見關節斷裂。 當兩個圓角盒狀在彎曲關節處相接時(臀部、膝蓋、肩膀、手肘、手腕、頸部/軀幹、臀部/軀幹),圓角會產生間隙或缺口,看起來像是斷裂處。使用與衣物同色的布料層隱藏每個關節:一個以關節樞軸為中心的小球體(
ball()),大小為肢體半寬 + 約 0.002,並沿肢體方向拉伸(sc[1, 1.2-1.4, 1]);一個連接兩個臀部的骨盆塊;一個覆蓋頸部/軀幹接縫的領圈塊;一個位於袖口下方的膚色腕部塊。不要將球體過度放大(肢體半寬 + 0.015 會導致膝蓋和手肘凹凸不平)。使用彎曲姿勢的壓力測試驗證(手臂舉起、行走中段、腿部抬起),並放大檢視。
臉部(平面繪製在頭部正面,在畫布紋理上每幀重繪)
- 大眼睛:眼白,綠色或棕色虹膜,黑色瞳孔,白色高光。粗黑色上眼瞼線。眼睛可以環視(lookX/lookY)、眨眼和瞇眼。
- 粗而尖的眉毛,可傾斜(生氣/擔心)和揚起。黑色圓框眼鏡遠大於眼睛(環半徑約為眼睛半徑的 2 倍)。眼鏡必須是一個連貫的框架:兩個環透過一條橫跨鼻樑的弧形橋接相連,每個環都有一條鏡腳線延伸至臉部邊緣。兩個漂浮的環看起來就像壞掉了(用戶在第一張角色表上指出了這個問題)。
- 無鼻子。嘴巴是一條極小的線條/曲線,或者說話時是一個填滿的張開嘴巴,帶有粉紅色舌頭。微笑值範圍 -1..1。
- 可選的柔和腮紅。
頭髮、帽子、配件(堆疊的厚實塊狀)
- 主角:棕色飛機頭,帶有尖翹的髮束、側邊髮片和後腦塊。其他角色:短髮,或長髮(側簾 + 後腦塊)。黑色瀏海是一個薄薄的傾斜塊。
- 警帽:海軍藍帽體塊 + 黑色帽簷 + 金色徽章。平頂帽:棕色塊 + 短帽簷 + 淺色鈕扣。
- 老年男性:白色塊狀鬍子 + 白色鬢角塊。
- 服裝為簡單的色塊:西裝(栗色外套、白襯衫、深色領帶、翻領、金色掛繩 V 形 + 帶藍色條紋的白色 ID 徽章),制服(海軍藍、金色徽章、黑色皮帶 + 金色扣環),圍裙(白襯衫、帶有鈕扣點的棕色圍裙),以及其他角色的灰/綠/棕褐/海軍藍/藍綠色變體。
- 膚色:桃色 #efaa71,棕褐色 #d89a62,棕色 #8a5230,深色 #5a3320,淺色 #f4c49a。頭髮棕色 #59321a,黑色 #14110f。
渲染
- 每個零件透過反向外殼著色器繪製薄黑色墨線 (#1c0f06),螢幕寬度恆定。寬度是相機距離的一個分數:
w = px / ((H/2) / tan(fov/2)),其中px是螢幕像素中所需的線條寬度(720×1280 影片鏡頭約為 0.003,約 1.5–2 像素)。對於 1600×900 的遠距多角色場景圖,使用K.setOutlineWidth(.0009),對於半身像圖塊,使用約.0016;預設值在遠距離相機上過厚。臉部和微小裝飾件跳過描邊。 - Lambert 材質,半球光(約 2.9)+ 一盞柔和陰影太陽光(約 1.5)。柔和的 PCF 陰影。
- 背景人群使用
lod: 1(無手指,較少頭髮塊)。絕不為每個額外角色設計獨特外觀;變化膚色、頭髮、服裝顏色和髮型。
多樣性規則: 每個命名角色必須僅透過輪廓即可識別(帽子、頭髮塊形狀、眼鏡、鬍子、膚色、服裝顏色)。對所有角色使用相同比例。
參考圖表(當使用者要求角色設計或環境時才執行,且在任何影片之前)
在 scene.html 旁邊建立一個 designsheet.html 和一個 envsheet.html(相同工具包,相同 render.cjs,執行 node render.cjs designsheet.html out 0,1,2 --w 1600 --h 900)。frame(t) 透過 Math.round(t) 選擇模式;標籤是透過將 3D 點投影到 new THREE.Vector3(x,y,z).project(camera) 來定位的 DOM 覆蓋層(Poppins);頁面標題和色票也是 DOM。
- 角色檔案
cast.js:一個DEF表(姓名、角色、4 個色票 [皮膚、頭髮、服裝、重點色]),一個BUILD表,包含每個角色的一個建構器(makeChar+ 一些額外的box()配件),makeOne(name),makeCast(),以及pose(name, C),它每幀重置臉部和姿勢,並應用乾淨的角色表姿勢。透過輪廓使每個角色獨一無二:不同的scale/headScale、髮型、帽子、眼鏡、鬍子塊、膚色和一種重點色。 - 圖表 0 排隊:所有角色排成一行(相機約在 [0, 1.02, 15],fov 16.5),每個角色下方有名稱 + 角色 + 色票方塊。圖表 1 主角的旋轉展示,偏航角分別為 0、.75、PI/2、PI(正面、3/4 側面、側面、背面)。圖表 2 表情:六個半身像(中性、開心、生氣、驚訝、擔心、得意),繪製為剪刀圖塊,因為在一個 3D 場景中堆疊六個角色會使列位於地板下方並重疊。圖塊模式:
renderer.autoClear=false; renderer.setScissorTest(true);然後每個圖塊setViewport(x, H-y-h, w, h); setScissor(...); renderer.clear(); camera.aspect = w/h; camera.updateProjectionMatrix(); renderer.render(scene, camera);並在frame結束時重置視埠、剪刀測試和 autoClear。 - 眉毛符號慣例:
face.brow = [height, tilt, arch],tilt > 0 = 內端向下(生氣),tilt < 0 = 內端向上(擔心)。在drawFace中,這表示ang = -s * f.brow[1] * .55。(第一個版本是反過來的,導致一個生氣的警官看起來很悲傷。) - 環境圖表 0:四個 760×318 剪刀圖塊(從入口處的主角廣角、帶有拱形窗的末端牆、關鍵道具角落、一個高角度吊臂鏡頭且
hall.ceiling.visible = false),每個都有標題;將角色安排在場景中以顯示比例,並讓他們面向相機或彼此(檢查偏航:偏航 0 表示面向 +z 朝向位於 +z 方向的相機;角色背對相機是佈景錯誤)。圖表 1:每個道具單獨放在遠處的道具舞台(x = 400 + i*20),這樣它永遠不會出現在大廳鏡頭中,圖塊大小為 486×300。圖表 2:俯視平面圖,使用up = [-1,0,0](長軸水平延伸),相機約在 [-1, 120, -13] 向下看,窄 fov(約 9.3),並從世界點投影標點標籤。始終在頁首包含一個色票條(10 個色票)。 - 大廳的天花板和橫樑存在於一個群組中,外部稱為
H.ceiling(陰影投射關閉,這樣太陽仍能照亮大廳);在吊臂鏡頭和俯視鏡頭中隱藏它。標誌文字必須根據其平面調整大小:一個 1.9 x 0.5 的標誌需要800 56px Poppins,而不是 78px(會被裁剪)。 - Three.js 陷阱:
Object3D.position和.rotation是唯讀屬性;設定組件(m.rotation.x = ...,m.position.set(...))並且永遠不要Object.assign(mesh, { rotation: ... }),這會拋出錯誤並停止__ready。 - 使用 SendUserFile 交付 PNG 檔案和
.js檔案;每次工具包變更後重新渲染並重新發送每個圖表,以免使用者持有過時的圖表。
3. 動畫流暢度規則
- 動畫是時間的純函數:
frame(t)從t設定所有內容。幀之間不攜帶狀態,因此任何幀都可以按任意順序或並行渲染。 - 對每個動作使用
kf([[t0, v0], [t1, v1]], t, easeFn)。對推近和到達使用easeOut,對吊臂/快速搖攝動作使用easeIO,對小物體使用ease。相機或肢體絕不使用線性運動。 - 相機從不靜止。每個鏡頭都有緩和的位置 + 注視點 + fov + 幾度的翻滾,加上一個始終開啟的手持晃動
[sin(.5t)*.03, sin(.37t)*.02]。 - 僅在鏡頭邊界處進行硬切,在一個單字或節拍上。無淡入淡出。平均鏡頭長度 2–3 秒。
- 每個角色始終存在待機圖層:呼吸起伏、搖擺、頭部滾動、每約 3–4 秒眨眼一次(0.13 秒緩和)、微小的眼球移動。
K.idle()執行此操作。 - 嘴巴張開程度來自真實旁白音量(
lipsync.py以 30 fps 寫入 env.json)。絕不使用循環晃動。 - 手勢:預期動作(在指點/重拍前輕微向後拉)、超調(
easeBack)、停頓、穩定。頭部先動,軀幹跟隨。 - 行走循環:雙腿相反,每隻手臂與其同側腿相反方向擺動,膝蓋僅向後彎曲(
Math.max(0, knee)),每周期起伏 2 次,輕微軀幹扭轉和傾斜。相位以循環為單位:一般行走使用約 1 循環/秒。 - 角色絕不能與家具重疊:使用明確的 x/z 座標放置,並檢查靜止幀。
4. 工作流程
- 劇本:第一人稱,在前 3 秒吸引人,簡短有力的句子,逐步升級,轉折,最後一句令人印象深刻。大約 2.5 字/秒。
- 旁白:使用者通常提供 ElevenLabs 檔案。如果沒有,請要求提供或合成一個佔位音軌。儲存為
voice.wav。 - 單字時間戳記:取得單字級別的時間戳(對劇本文字進行強制對齊,例如 pocketsphinx 或 whisper 單字時間戳)。儲存
words.json為[{"w":"my","s":0.20,"e":0.41}, ...],使用劇本的拼寫和標點符號(字幕使用尾隨標點來分割組)。 - 唇形同步包絡線:
python3 lipsync.py voice.wav env.json。 - 鏡頭列表:每個鏡頭一行:開始/結束時間、觀眾看到的內容、相機移動、誰行動。目標是每約 2.6 秒一個切換。變換取景:臉部特寫、中景雙人鏡頭、低角度推鏡、高角度吊臂越過人群、快速上搖、過肩鏡頭、道具插入(手機、徽章)。將鏡頭寫入
scene.html中的SHOTS。 - 從
blockkit.js/props.js建立角色 + 場景(見程式碼)。重用room()、chair()、bench()、crowd();使用上方調色板中的box()建立新道具。 - 先檢查靜止幀(低成本):
node render.cjs scene.html outstills 1,4,7,...;檢查每個命名角色的特寫、一個行走動作、一個手勢、一個廣角鏡頭。在渲染影片前修正問題。 - 渲染影片(在 2 核心上,720×1280 約 1–1.7 秒/幀,因此 40 秒約需 15–25 分鐘;以區塊並行並在背景執行):
export NODE_PATH=$(npm root -g) # playwright is global in the sandbox; otherwise: npm i playwright && npx playwright install chromium
S=30; for i in 0 1; do (node render.cjs scene.html part$i.mp4 --from $((i*S)) --to $(((i+1)*S)) > r$i.log 2>&1 &); done # 2 chunks of 30 s; wait for both
printf "file 'part0.mp4'\nfile 'part1.mp4'\n" > list.txt && ffmpeg -y -f concat -safe 0 -i list.txt -c copy silent.mp4
- 音訊 + 混音(旁白下連續輕柔墊底音樂,約 -14 LUFS):
D=130 # >= video length
ffmpeg -y -f lavfi -i "sine=f=55:d=$D" -f lavfi -i "sine=f=82.4:d=$D" -f lavfi -i "sine=f=110:d=$D" -filter_complex "[0][1][2]amix=inputs=3:normalize=0,lowpass=f=500,tremolo=f=0.12:d=0.35,aformat=channel_layouts=stereo,afade=t=in:d=1[bed]" -map "[bed]" -ar 44100 bed.wav
ffmpeg -y -i silent.mp4 -i voice.wav -i bed.wav -filter_complex "[2:a]volume=0.12[b];[1:a][b]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-14:TP=-1.5:LRA=7,aformat=channel_layouts=stereo[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k final.mp4
- 交付前的品質檢查:(a)聯絡表,每約 2.6 秒一幀,檢查角色/字幕;(b)動作檢查,對縮小的灰階串流進行平均幀差,預期無靜態幀:
python3 - <<'E'
import subprocess, numpy as np
W,H=90,160
a=np.frombuffer(subprocess.run(['ffmpeg','-v','error','-i','final.mp4','-vf',f'scale={W}:{H},format=gray','-f','rawvideo','-'],capture_output=True).stdout,dtype=np.uint8).reshape(-1,H,W).astype(np.float32)
d=np.abs(np.diff(a,axis=0)).mean(axis=(1,2)); print(len(a),'frames; static:',(d<.02).sum(),'; spikes(cuts):',(d>14).sum())
E
(c)響度 ffmpeg -i final.mp4 -af ebur128 -f null -(約 -14 LUFS),(d)字幕與語音匹配。
使用 SendUserFile 交付(mp4 + 封面靜止幀)。
5. 設定
mkdir -p work && cd work && npm init -y >/dev/null && npm i [email protected] @fontsource/poppins
# create the files below in this folder: blockkit.js props.js captions.js render.cjs lipsync.py scene.html
# (or copy the ready-made files from this repo's kit/ folder, which also has st
…
[為翻譯截斷]