ブロックトーンストーリー動画(「靴磨き男にバレた」風)
ユーザーが、縦長(720×1280または1080×1920、30fps)の一人称視点ストーリー動画(30〜130秒)を、ナレーション付きで、丸みを帯びたブロック状の3Dキャラクター、滑らかで連続的な動き、そしてパンチの効いたカラーキャプションとともに作成したい場合に使用します。すべてコードベースです:ヘッドレスChromium上のthree.jsシーン、決定論的な frame(t) 関数、そしてffmpegにパイプされるスクリーンショット。
ユーザーの優先事項はキャラクターデザインの忠実性とアニメーションの滑らかさです。ステップ5のキャラクターチェックを省略しないでください。
1. 参考動画の見た目(計測値)
- 720×1280、30fps、約122秒、AACステレオ、音声の下に静かな連続した背景音がある約-14 LUFS。
- 約47回のハードカット(シングルフレームカット、クロスフェードなし)、平均ショット約2.6秒(範囲1.2〜5秒)。ショット内ではカメラは常に動いています:イーズインのプッシュイン、プルアウト、クレーン/ハイアングルドリフト、ローアングルプッシュ、天井や窓へのホイップティルト。フレームの1%未満が静止画です。
- キャプションは画面下部3分の1(上部から約64〜68%の高さ)、1グループあたり2〜3語の小文字、太字の角丸ジオメトリックサンセリフ体(Poppins ExtraBold)、白地にキーワードが色付け(ゴールド #f5c32c、シアン #48c9f0、コーラル #ff5b4d、ピンク #ff7ac0、グリーン #2ecc8f、オレンジ #ffa21f)、太い黒いストローク、各グループに小さなスケールポップ。
- 環境:暖かみのあるクリーム/ベージュのインテリア(壁 #dcbf96、床 #c7aa7d、石 #d9c39b、腰板 #b99a6f、空 #c7dcec)にアクセントカラーのマルーン #570d0e、ネイビー #1c2a49、ゴールド #e0990d、ティール #1f8f8c、レッド #a3160f。
- 照明:柔らかく、明るく、やや暖かめ。滑らかなグラデーションシェーディングと柔らかいキャストシャドウ。ハードなセル境界はありません。
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単位。
- 手は分厚いミトンブロック:掌ブロックと、4本の短い2セグメントの指、および独立した親指。開いた状態から握り拳まで曲げられ、広げることもできます。手は決して蜘蛛のように見えてはいけません。
- 各手首に白いカフバンド。黒い靴、つま先が丸い平らなブロック。
- 関節の切れ目が見えてはいけません。 2つの角丸ボックスが曲がった関節(股関節、膝、肩、肘、手首、首/胴体、股関節/胴体)で接する場合、ベベルが隙間やノッチを開き、それが切れ目として読まれます。すべての関節を、衣類と同じ色の布の層で隠します:関節のピボットを中心とした小さな球体(
ball())、手足の半幅+約0.002のサイズで、手足に沿って伸ばします(sc[1,1.2-1.4,1])。2つの股関節を接続する骨盤ブロック。首/胴体の継ぎ目にかぶせるカラーブロック。カフの下の肌色の手首ピース。ボールのサイズを大きくしすぎないでください(手足の半幅+0.015だと、ゴツゴツした膝や肘になります)。曲げたポーズ(腕を上げる、歩行中のストライド、脚を上げる)でストレステストを行い、拡大して確認してください。
顔(頭部前面にフラットにペイント、キャンバステクスチャに毎フレーム再描画)
- 大きな丸い目:白い強膜、緑または茶色の虹彩、黒い瞳孔、白いハイライト。太い黒い上まぶたのライン。目は見回す(lookX/lookY)、まばたき、細めることができます。
- 太く先細りの眉で、傾き(怒り/心配)と上げ下げが可能。黒いリングのメガネは目よりもはるかに大きい(リング半径は目の半径の約2倍)。メガネは接続された1つのフレームである必要があります: 2つのリングは鼻の両側にまたがる湾曲したブリッジアークで結合され、各リングには顔の端まで伸びるテンプルラインがあります。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 px)。遠距離のマルチキャラクターシート(1600×900)ではK.setOutlineWidth(.0009)を使用し、バストタイルでは約.0016;デフォルトは遠方カメラでは太すぎます。顔や小さなトリムピースはアウトラインをスキップします。 - Lambertマテリアル、半球照明(約2.9)+ 1つのソフトシャドウサン(約1.5)。ソフトPCFシャドウ。
- 背景の群衆は
lod: 1を使用(指なし、髪ブロック少なめ)。エキストラごとにユニークなデザインを作成しないでください。肌、髪、服装の色と髪型を変更します。
バラエティルール: すべての名前付きキャラクターは、シルエットだけで認識可能でなければなりません(帽子、髪型ブロック、メガネ、口ひげ、肌のトーン、服装の色)。すべてのキャラクターに同じプロポーションを使用します。
リファレンスシート(ユーザーがキャラクターデザインや環境を要求した場合、ビデオを作成する前に行います)
scene.html の隣に designsheet.html と envsheet.html を1つずつ作成します(同じキット、同じ 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つのスウォッチ[肌、髪、服装、アクセント])、キャラクターごとに1つのビルダーを持つBUILDテーブル(makeChar+ いくつかの追加のbox()アクセサリー)、makeOne(name)、makeCast()、および毎フレーム顔とポーズをリセットし、クリーンなモデルシートポーズを適用するpose(name, C)。各キャラクターをシルエットでユニークにします:異なるscale/headScale、髪型、帽子、メガネ、口ひげブロック、肌のトーン、1つのアクセントカラー。 - シート0のラインナップ:すべてのキャラクターを一列に並べる(カメラ約[0,1.02,15]、fov 16.5)、各キャラクターの下に名前+役割+スウォッチ正方形。シート1:主人公のヨー角度0、.75、PI/2、PIでのターンアラウンド(正面、3/4、横、背面)。シート2:表情:6つのバスト(ニュートラル、ハッピー、怒り、驚き、心配、自己満足)をシザータイルとして描画。これは、6人のキャラクターを1つの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 = [高さ, 傾き, アーチ]、傾き > 0 は内側が下がる(怒り)、傾き < 0 は内側が上がる(心配)。drawFaceではang = -s * f.brow[1] * .55です(最初のバージョンでは反転しており、怒った警官が悲しそうに見えました)。 - 環境シート0:4つの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つのグループに存在します(シャドウキャストはオフ、これにより太陽はまだホールを照らします)。クレーンショットとトップダウンショットでは非表示にします。看板のテキストはその平面にサイズを合わせる必要があります:1.9×0.5の看板には 800 56px Poppins が必要で、78px(クリップされる)では不十分です。 - Three.jsの落とし穴:
Object3D.positionと.rotationは読み取り専用プロパティです。コンポーネントを設定し(m.rotation.x = ...,m.position.set(...))、Object.assign(mesh, { rotation: ... })を決して行わないでください。これは例外をスローし、__readyを停止します。 - PNGと
.jsファイルを SendUserFile で配信します。キットを変更した後は、すべてのシートを再レンダリングして再送信し、ユーザーが古いシートを保持しないようにします。
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が30fpsでenv.jsonを書き込みます)。ループするフラップは使用しないでください。 - ジェスチャー:予備動作(指差し/叩きつけの前の小さな引き戻し)、オーバーシュート(
easeBack)、ホールド、セトリング。頭が先行し、胴体がそれに続きます(twistの前にheadYawを使用)。 - 歩行サイクル:脚は逆位相、各腕は同じ側の脚とは逆に振られます。膝は後ろにのみ曲がります(
Math.max(0, knee))。1サイクルあたり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
- ショットリスト:ショットごとに1行:開始/終了時間、視聴者が見るもの、カメラの動き、誰が演技するか。約2.6秒ごとにカットすることを目指します。フレーミングを変える:顔のクローズアップ、ミディアムツーショット、ローアングルプッシュイン、群衆の上のハイアングルクレーン、ホイップティルト、オーバーザショルダー、プロップ(電話、バッジ)のインサート。
scene.htmlのSHOTSにショットを書き込みます。 blockkit.js/props.jsからキャスト+セットを構築します(コードを参照)。room()、chair()、bench()、crowd()を再利用します。上記のパレットのbox()を使用して新しいプロップを構築します。- まず静止画をチェックします(低コスト):
node render.cjs scene.html outstills 1,4,7,...
すべての名前付きキャラクターのクローズアップ、1つの歩行、1つのジェスチャー、1つのワイドを確認します。ビデオをレンダリングする前に修正します。 8. ビデオのレンダリング(720×1280の2コアで約1〜1.7秒/フレーム、つまり40秒で約15〜25分。チャンクを並行してバックグラウンドで実行します):
export NODE_PATH=$(npm root -g) # playwright はサンドボックス内でグローバルです。それ以外の場合は: 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 # 30秒のチャンクを2つ。両方の終了を待ちます
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 # >= ビデオの長さ
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
- 配信前のQC:(a)コンタクトシート、約2.6秒ごとに1フレーム、キャラクター/キャプションを確認。(b)モーションチェック、ダウンスケールされたグレイストリームの平均フレーム差分、0の静止フレームを期待:
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
# このフォルダに以下のファイルを作成します: blockkit.js props.js captions.js render.cjs lipsync.py scene.html
# (または、このリポジトリの kit/ フォルダから既製のファイルをコピーします。そこには標準のシーンとその他も含まれています)