mcpField
3
3d-toy-character

3d-toy-character

github.com/phantomtonytクリエイティブ・メディア24175 回インストール4 週間前に更新
リポジトリ

3d-toy-character について

3D Toy Characterは、丸みのあるおもちゃ風キャラクターの短い動画を作るためのビジュアルスタイルスキルです。ストーリー構成、動き、画面設計から、Three.js、SVG、Blenderを使う制作方法まで案内します。

主な機能

  1. 01おもちゃ風キャラクターの造形規則
  2. 02ビートと文字の可読性の計画
  3. 03因果関係に基づく動きの指針
  4. 04Three.js、SVG、Blenderの制作ルート
  5. 05カメラ、セット、照明の指針

活用シーン

  1. 01縦型のおもちゃ風キャラクター動画の制作
  2. 02ブランドキャラクターで一つのアイデアを表現
  3. 03キャラクターの動きと動画のビートを計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/phantomtonyt/motion-design-skills --skill 3d-toy-character

インストール後、Claude Code で/3d-toy-characterを入力して読み込まれたか確認します。

詳細

3D トイキャラクター

柔らかく丸みのある、トイのような3Dキャラクターがアイデアを演じるショート動画を作成します。スタジオライトの下に置かれた高級ビニールトイのように、触れたくなる質感で、親しみやすく、少し遊び心のある雰囲気にします。これはビジュアルスタイルのスキルであり、一般的な3Dモデリングのスキルではありません。

オプション

オプションデフォルト備考
format縦型 1080×1920または横型 1920×1080、正方形 1080×1080
duration15 s8~30 s が最適です
toneplayfulcalm(穏やかなイージング、長めの静止)、playful(デフォルト)、hyper(大きなスクワッシュ、テンポの速いビート、カメラ移動多め)
character丸いトイキャラクターまたはユーザーのブランドキャラクター(シルエットと色を合わせます)

ブリーフ(計画の前に回答)

  1. キャラクターが望んでいること、または伝えるべきメッセージは何ですか?
  2. 最初の1秒で引きつける要素(ポーズとタイトル)は何ですか?
  3. 何がうまくいかない、または何に驚き、どんな報いがありますか?
  4. 動画の最後を締めくくるセリフは何ですか?

次に、各ビートの時間、見えるもの、キャラクターの動き、テキスト、音を記したビートリストを作成し、各ビートの長さの合計が目標時間になるようにします。

読み取りシート(ビートリストと一緒に作成)

各ビートについて、視聴者が理解すべきことを1つ(「ロボットは飛べない」「キャッシュミスが発生した」など)と、それが読み取れる状態で表示される時間を記します。一度に表示するのは1つだけです。テキストの最低表示時間:ラベルは0.8 s 以上、文章は完全に画面に表示されてから1語あたり約0.3 s です。時間内に収まらない場合は、表示を速めずに単語を削ります。

動きのルール

  • 動きには因果関係を持たせます。キャラクターは画面上の何かに反応し、すべての動きに原因と結果があります。
  • キャラクターを完全に静止させてはいけません。待っている間も呼吸し、まばたきし、重心を移します。この待機動作も演技の一部であり、単なる埋め合わせではないため、小さく不規則に保ちます。
  • 左右対称の動きは避けます。腕を鏡像のように動かさず、目、アンテナ、体をすべて同じフレームで動かし始めないようにします。各部位を2~4フレームずらします。
  • 行き過ぎやスクワッシュはキャラクターの動きに使います(着地、飛び出し、驚き)。カメラとテキストは滑らかなイーズアウトで動かします。
  • 1つのシーンにつき主な移動方向は1つにし、シーンの切り替えではクロスフェードではなく、キャラクターか小道具を次のシーンへ移動させます。
  • 報いの直前に0.3~0.75 s のポーズを保ち、その効果を際立たせます。

スタイルのルール(常に適用)

これらがこのスタイルを形作ります。色やストーリーを変える場合も維持してください。

  1. 頭の高さは全体の55~70%にします。大きな頭はかわいらしく、トイらしく見えるためです。
  2. すべての形状を丸くし、鋭いエッジは使いません。シルエットがいくつかの柔らかな塊として見えるようにします。
  3. 素材は滑らかなビニールにします。roughness は0.35~0.5、clearcoat は弱め、metalness は0にします。金属や強い光沢は、製品のレンダリングのように見えてしまいます。
  4. キャラクターの下には必ず柔らかな接地影を付けます。これによって3Dらしさが生まれます。
  5. ライティングは大きく柔らかなキーライト、弱いフィルライト、細いリムライトにします。硬い影は使いません。
  6. すべての動きは予備動作、移動、行き過ぎ、収束の順にし、重みを感じさせます。

デフォルト(自由に変更可能)

色、キャラクターデザイン、背景、カメラアングル、時間、テキストは変更できます。ユーザーがブランドのカラーパレットやキャラクターを指定した場合は、そちらを優先します。優先順位は、ユーザーのブランドまたは仕様、明示的なリクエスト、このファイルのデフォルトの順です。

セットアップ(初回のみ)

node -v                      # needs 22+
ffmpeg -version              # needs any recent build
npx hyperframes init my-video --non-interactive --resolution portrait && cd my-video   # landscape for 16:9

既存の空フォルダー内で npx hyperframes init . --non-interactive --resolution portrait を実行します。次に、生成された index.html を以下のテンプレートに置き換えます。Three.js とフォントはコンポジション内で CDN から読み込まれます(Google Fonts の <link> はレンダリング時に使用できます)。それ以外にインストールするものはありません。Blender が必要なのはオフラインの方法を使う場合のみです。

制作前に決めること

コードを書く前に、次の4項目を決め、1行でユーザーに伝えてください。

  1. アスペクト比: Reels、Shorts、TikTok には 9:16(1080×1920)、YouTube、Web、スライドには 16:9(1920×1080)。デフォルトは 9:16 です。
  2. 制作方法: 表を参照してください。デフォルトは Three.js です。
  3. キャラクター: シルエット、メインカラー、アクセサリーは最大1つ(アンテナ、耳、スカーフなど)。ユーザーにブランドキャラクターがいる場合は、そのシルエットと色に合わせてください。
  4. ビート: 15秒で3~6ビート。1ビートにつきアクションは1つです。
要件SVG + GSAPThree.js(デフォルト)Blender
シンプルなマスコットのリアクションはいはいはい
奥行きのある本格的な回転いいえはいはい
柔らかなライティングと接地影擬似表現はいはい
光沢素材またはクリアコート素材擬似表現はいはい
高速な反復、あらゆる環境でのレンダリングはいはい遅い
映画のような品質、複雑なジオメトリいいえ一部対応はい

ユーザーが求める見た目を保てる、最もシンプルな制作方法を選んでください。キャラクターが回転しないなら、グラデーションを使った SVG でも対応できます。回転する場合や、ライティングが重要な場合は Three.js を使ってください。

ビジュアル表現

プロポーション

  • 頭部:全高の55~70%。頭部が全身を兼ねてもかまいません。
  • 目:大きくシンプルにし、顔の低い位置、上から約55~60%の位置に配置します。それぞれの目に、大きな白いハイライトと小さな白いハイライトを1つずつ入れます。
  • 手足:短く、太く、丸みを持たせます。手はミトン状にし、関節は見せません。
  • 鋭い角は作りません。シルエットがいくつかの丸い塊で見えるよう、すべての角を面取りするか丸くします。
  • 顔のパーツは頭部の中ではなく、表面に配置します。深度方向に sz 倍した半径 r の球形の頭部では、目、口、バイザーを z ≈ r·sz に配置し、パーツ自身の奥行きを加えます。スナップショットを拡大して、パーツが埋もれていないか確認してください。

素材

  • 滑らかなビニールまたはプラスチックを使い、粗さは約0.35~0.5、クリアコートは軽め(約0.3)にします。指示がない限り、テクスチャは使いません。
  • メタリックにはしません。Metalness は常に 0 にします。

ライティング

  • 左上から大きく柔らかなキーライトを1つ、反対側から弱いフィルライト(キーライトの約30%)を1つ、背後から細いリムライト(約60%)を1つ当てます。
  • キャラクターの下に柔らかな接地影を付けます。このディテール1つで立体感が出ます。
  • 点光源による硬い影、暗くドラマチックなライティング、強いブルーム、ホラー調の派手な色のライティングは避けてください。

デフォルトの配色(スタイルではなく、初期設定です):

  • 背景:明るく、少し寒色寄りのパステルカラー(例:#e8f0f7)、または柔らかな2色のグラデーション。
  • キャラクター:彩度は高めながら柔らかなメインカラーを1色使い、お腹または顔に明るい色のパッチを付けます。
  • アクセント:小道具や UI 用に、対照的なパステルカラーを1色使います。
  • 影:背景色に寄せた色にし、真っ黒にはしません。

アニメーション表現

動きは柔らかく、やや誇張しつつ、物理的に自然にしてください。すべてのアクションは、予備動作 → メインの動き → 行き過ぎ → 落ち着く、の順で構成します。

アクション時間備考
微細なリアクション(まばたき、視線移動)0.2~0.5秒2~4秒ごとに、不規則にまばたきします
ジェスチャー(手を振る、拍手、指さし)0.5~1.2秒腕が先に動き、2~3フレーム後に体が続きます。頭が大きい場合は、上げた腕を前方に傾け(rotation.x ≈ 0.8)、手が頭の前を通るようにします。小道具から遠い方の腕で手を振ってください
ジャンプ0.5~0.8秒0.08秒かけてつぶれ、上昇、落下、着地時につぶれ、落ち着きます
登場0.8~1.5秒スケールを 0 → 1.1 → 0.95 → 1 と変化させ、最後につぶします。「下から登場」は、最初にキャラクターを隠す床やフレームの端が見える場合にのみ効果があります
首を傾ける(好奇心)0.3~0.4秒6~10°傾けてから少し保ち、元に戻します
待機継続2~3秒周期で、呼吸するようにスケールを±2%変化させます
  • ボリューム感を保ちます。胴体を縦に10%伸ばすときは、幅を約5%狭くします。
  • 接地影も動きに合わせて変化させます。キャラクターが上昇すると小さく薄くなり、着地時に元の濃さに戻ります。
  • すべてを弾ませないでください。行き過ぎの動きは登場時と着地時だけに使い、待機中の動きは穏やかに保ちます。

構図

  • 9:16: キャラクターは中央の3分の1に配置し、顔は y 500〜1100 の範囲に収めます。テキストと顔は x 60〜960、y 270〜1250 の範囲に収めます。下部の約670 pxはプラットフォームのUIに覆われるため、キャプションはその上に配置します。
  • 16:9: キャラクターを片側の3分の1に、テキストまたはUIを残りの3分の2に配置するか、キャラクターを中央に置いてテキストを下に配置します。5%の余白を保ちます。
  • カメラ:目線より少し高く、画角は30〜40°にします。極端な広角は避けます。
  • フレーミング:画角32°の場合、見える高さはカメラからの距離の約0.57倍です。身長約3.3単位のキャラクターなら、カメラの z ≈ 17〜19 で、9:16の画面上では高さ約700 pxになります。小道具も同じフレームに入る場合はカメラのターゲットを横に約0.45単位ずらし、足が y ≈ 1300 より上にくるようにします。

テキスト

  • 太字の丸ゴシック体(例:Google Fonts の Nunito 800)を使用します。9:16ではタイトルを64〜90 pxにし、複数行のキャプションでは行の高さを1.15以上にします。
  • テキストは3:1以上のコントラストが必要です(hyperframes checkで確認されます)。ブランドカラーがブランド背景に対して十分なコントラストを確保できない場合は、図形、塗り、キャラクターにはその色を使い、テキストにはその濃い色合いか、ほぼ黒の色を使います。
  • タイムラインの外で gsap.set(...) を使ってテキストの非表示状態を設定し、その後タイムライン上でアニメーション表示します(時刻0での tl.set は lint 警告の原因になります)。

目を引く画面にする

何もない背景にキャラクターを置いただけでは、動画ではなくテストレンダーに見えます。すべての出力に次の要素を含めます。

  • 何もない空間ではなく、舞台を作ります。 空のグラデーション(透明なキャンバスの背後に CSS で設定)に加え、同じビニール素材で作ったシンプルな舞台要素を1つ(屋上の縁、机、浮遊する島など)と、スケール感を出す小道具をいくつか配置します。
  • 奥行きを3層にします: 遠景(雲、丘、霧)、中景(舞台)、近景(小道具やカメラの前を通るパーティクル)。カメラの移動に合わせて、それぞれ異なる速度で動かします。
  • カメラを動かします。 見せ場では寄り、アクションを追い、効果を見せる場面では引きます。滑らかなキーフレームで動きを設定し、キャラクターが舞台から外れたときは追従させます。
const smooth = (x) => x * x * (3 - 2 * x), mix = (a, b, x) => a + (b - a) * x;
const keys = (t, ks) => { if (t <= ks[0][0]) return ks[0][1];
  for (let i = 1; i < ks.length; i++) if (t <= ks[i][0]) return mix(ks[i - 1][1], ks[i][1], smooth(Math.min(1, (t - ks[i - 1][0]) / (ks[i][0] - ks[i - 1][0]))));
  return ks[ks.length - 1][1]; };
// camera.position.y = keys(t, [[0, 1.8], [1.6, 2.2], [10, 3.6], [11.4, 9], [13.4, 1.8]]);  same for x, z and the look-at target
  • 光るアクセントを1つ加えます。 物語の重要な瞬間(「ひらめき」の場面)に、アクセントカラーの発光する電球、スクリーン、小道具を置き、小さなポイントライトも加えます。
  • すべてのビートにセカンダリーモーションを加えます: 雲の漂い、衝撃時の砂ぼこり、きらめきや星、動きの後のアンテナや耳の揺れなどです。
  • タイトルが映える場面を作ります。 白い角丸カードに太字の丸ゴシック体を載せ、柔らかなドロップシャドウを付けます(混雑した空を背景にしてもコントラストを保てます)。オーバーシュートを付けてポップインさせます。キャラクターがその場所を通る前に、画面外へ移動させます。
  • ループする終わり方にします: 最後のポーズとフレーミングを最初に合わせます。
  • 品質ゲート: スナップショットのどれかがプレースホルダーのように見える場合(平坦な背景、動かないキャラクター、動きが何もないなど)、まだ完成ではありません。

ビルド:Three.js ルート(デフォルト)

HTMLのコンポジションを1つ作ります。HyperFrames は hf-seek イベントを通じて Three.js に描画する時刻を伝えるため、すべてのフレームは時刻から計算され、独自のクロックでは動きません。

<!doctype html><html><head><meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<style>html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:#e8f0f7}
#root{width:100%;height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}</style></head>
<body><div id="root" data-composition-id="main" data-start="0" data-duration="15" data-width="1080" data-height="1920">
<canvas id="three-layer"></canvas>
<div id="caption" style="position:absolute;left:60px;right:120px;top:300px;text-align:center;font:800 76px/1.1 system-ui,sans-serif;color:#1f2a30">Hello, I'm a toy!</div></div>
<script>
  // HyperFrames needs one registered paused timeline; put text/UI tweens on it, 3D is driven by hf-seek below.
  const tl = gsap.timeline({ paused: true });
  tl.fromTo("#caption", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.4, ease: "back.out(2)" }, 1.2);
  tl.to("#caption", { opacity: 0, duration: 0.3 }, 12); tl.set({}, {}, 15);
  window.__timelines["main"] = tl;
</script>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
const W = 1080, H = 1920;                                   // 16:9: 1920, 1080 and move the camera back less
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("three-layer"), antialias: true, alpha: true });
renderer.setSize(W, H, false); renderer.setPixelRatio(1);
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100); camera.position.set(0, 2.4, 16); camera.lookAt(0, 1.3, 0);

const vinyl = (hex, rough = 0.42) => new THREE.MeshPhysicalMaterial({ color: hex, roughness: rough, clearcoat: 0.3, clearcoatRoughness: 0.4 });
const ball = (r, mat, x, y, z, sx = 1, sy = 1, sz = 1) => { const m = new THREE.Mesh(new THREE.SphereGeometry(r, 48, 32), mat); m.position.set(x, y, z); m.scale.set(sx, sy, sz); m.castShadow = true; return m; };
const C = { body: 0x35b5a7, belly: 0xc9f2ea, cheek: 0xffb4a2, eye: 0x1f2a30 };   // defaults; replace with brand colours

const hero = new THREE.Group(); scene.add(hero);
const bodyG = new THREE.Group(); hero.add(bodyG);
bodyG.add(ball(1.25, vinyl(C.body), 0, 1.2, 0, 1, 0.92, 0.95));              // head = body
bodyG.add(ball(0.55, vinyl(C.belly, 0.5), 0, 0.62, 0.86, 1, 0.55, 0.35));     // belly patch
const eyes = [-0.4, 0.4].map((x) => {                                        // big, low eyes with two highlights
  const g = new THREE.Group(); g.position.set(x, 1.12, 1.08);
  g.add(ball(0.2, vinyl(C.eye, 0.2), 0, 0, 0, 1, 1.15, 0.55));
  g.add(ball(0.06, new THREE.MeshBasicMaterial({ color: 0xffffff }), -0.07, 0.09, 0.1));
  g.add(ball(0.03, new THREE.MeshBasicMaterial({ color: 0xffffff }), 0.07, -0.08, 0.1));
  bodyG.add(g); return g;
});
[-0.78, 0.78].forEach((x) => bodyG.add(ball(0.17, vinyl(C.cheek, 0.6), x, 0.9, 0.93, 1, 0.55, 0.4)));  // cheeks
const mouth = new THREE.Mesh(new THREE.TorusGeometry(0.1, 0.03, 12, 24, Math.PI), vinyl(C.eye));
mouth.position.set(0, 0.92, 1.17); mouth.rotation.z = Math.PI; bodyG.add(mouth);
const arm = (side) => { const p = new THREE.Group(); p.position.set(side * 1.15, 0.95, 0.15);   // pivot at the shoulder
  const m = new THREE.Mesh(new THREE.CapsuleGeometry(0.2, 0.32, 8, 16), vinyl(C.body)); m.position.y = -0.3; m.castShadow = true;
  p.add(m); p.rotation.z = side * 0.45; bodyG.add(p); return p; };
const armL = arm(-1), armR = arm(1);
[-0.45, 0.45].forEach((x) => hero.add(ball(0.3, vinyl(C.body), x, 0.12, 0.25, 1, 0.55, 1.2)));   // stubby feet

const floor = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.ShadowMaterial({ color: 0x5a6b80, opacity: 0.14 }));   // tinted, never black
floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
// contact shadow that reacts to height (the key-light shadow alone doesn't shrink when airborne)
const blobTex = (() => { const c = document.createElement("canvas"); c.width = c.height = 128; const g = c.getContext("2d");
  const r = g.createRadialGradient(64, 64, 0, 64, 64, 64); r.addColorStop(0, "rgba(60,72,90,0.55)"); r.addColorStop(1, "rgba(60,72,90,0)");
  g.fillStyle = r; g.fillRect(0, 0, 128, 128); return new THREE.CanvasTexture(c); })();
const blob = new THREE.Mesh(new THREE.PlaneGeometry(3.2, 1.4), new THREE.MeshBasicMaterial({ map: blobTex, transparent: true, depthWrite: false }));
blob.rotation.x = -Math.PI / 2; blob.position.y = 0.01; scene.add(blob);
const key = new THREE.DirectionalLight(0xffffff, 2.3); key.position.set(-3, 9, 5); key.castShadow = true;   // large soft key, upper left
key.shadow.mapSize.set(2048, 2048); Object.assign(key.shadow.camera, { left: -6, right: 6, top: 6, bottom: -6 }); scene.add(key);
const fill = new THREE.DirectionalLight(0xdfe9ff, 0.7); fill.position.set(5, 2, 4); scene.add(fill);
const rim = new THREE.DirectionalLight(0xffffff, 1.4); rim.position.set(0, 4, -6); scene.add(rim);
scene.add(new THREE.HemisphereLight(0xffffff, 0xcfd8e3, 0.6));

// deterministic helpers: every value comes from t (no Math.random, no Date.now)
const clamp = (x) => Math.min(1, Math.max(0, x));
const seg = (t, a, b) => clamp((t - a) / (b - a));
const backOut = (x, s = 1.7) => 1 + (s + 1) * (x - 1) ** 3 + s * (x - 1) ** 2;
const pulse = (t, a, b) => Math.sin(Math.PI * seg(t, a, b));

function renderAt(t) {
  const enter = backOut(seg(t, 0.2, 0.9), 2.2);                               // scale pop with overshoot
  const hop = pulse(t, 4.0, 4.6), land = pulse(t, 4.6, 4.8), antic = pulse(t, 3.85, 4.0);
  const y = 1.1 * hop;
  hero.position.y = y;
  const squash = 0.1 * (land + antic + pulse(t, 0.85, 1.05));
  hero.scale.set(enter * (1 + 0.8 * squash - 0.05 * hop), enter * (1 - squash + 0.08 * hop), enter);   // keep volume
  blob.scale.setScalar(enter / (1 + 0.55 * y)); blob.material.opacity = 0.35 + 0.65 / (1 + 0.55 * y);   // shadow reacts
  bodyG.rotation.z = 0.12 * pulse(t, 2.0, 2.8);                              // curious tilt
  const blink = (a) => Math.max(0, seg(t, a, a + 0.07) - seg(t, a + 0.07, a + 0.14));
  const b = Math.max(blink(3.1), blink(6.0), blink(11.4));                    // irregular blinks
  eyes.forEach((e) => (e.scale.y = 1 - 0.9 * b));
  const raised = seg(t, 8.0, 8.2) - seg(t, 9.6, 9.8);
  armR.rotation.z = 0.45 + 1.6 * raised + 0.35 * Math.sin(Math.PI * 4 * seg(t, 8.2, 9.6));   // wave
  armR.rotation.x = 0.8 * raised;                                             // hand passes in front of the head
  armL.rotation.z = -0.45 - 0.1 * hop;                                        // follow-through
  bodyG.scale.y = 1 + 0.02 * Math.sin(t * 2.6);                               // breathing
  renderer.render(scene, camera);
}
window.addEventListener("hf-seek", (e) => renderAt(e.detail.time));    // HyperFrames sends the frame time on every seek
renderAt(window.__hfThreeTime || 0);                                      // and stores the current time here for the first draw
</script></body></html>

テキストとUIは HTML または SVG としてキャンバスの上に重ね、登録済みの tl 上でアニメーションさせます(Three.js 内ですべての動きを完結させる場合でも、HyperFrames ではこのタイムラインが必要です)。

このルートのルール:

  • ルート要素には data-duration が必須です。Three.js のシーンには、それ自体の長さがありません。
  • requestAnimationFrame、setAnimationLoop、Date.now、Math.random を使ってアニメーションさせてはいけません。すべての値を t から計算します。
  • 初回のレンダリング前にモデルやテクスチャを読み込んでおきます。クリップ付きの GLTF キャラクターは、mixer.setTime(t) を使ってシークします。

ビルド:その他の方法

  • SVG + GSAP(奥行きなし): キャラクターを SVG の楕円で描き、放射状グラデーションの塗り(左上を明るく、右下を暗く)を使います。頭には白い楕円形の鏡面ハイライトを加え、接地影にはぼかした楕円を使います。上記と同じタイミングを GSAP のイージング(登場には back.out(1.7)、ホップの上昇には power2.out、下降には power2.in)で適用します。window.__timelines["main"] に一時停止状態のタイムラインを 1 つ登録します。
  • Blender(オフラインで高品質に制作): サブディビジョンサーフェスとベベルを使ってモデリングし、粗さ 0.4、クリアコート 0.3 の Principled BSDF を使います。3 点のエリアライトとシャドウキャッチャーの床を設置し、目標のアスペクト比で PNG シーケンスまたは MP4 をレンダリングします。テキスト、字幕、サウンドを追加するため、HyperFrames に <video> クリップとして取り込みます。

ユーザーに公開するパラメーター

aspect · duration · characterColor · accentColor · background · accessory · beats(時間 + アクションのリスト) · cameraAngle · captionText。ファイルの先頭で定数として定義し、修正が 1 行で済むようにします。

避けること

  • リアルな人体、小さな頭、細い手足、細かい鼻や指。
  • 金属、クロム、ガラスのような素材。おもちゃではなく、製品レンダーのように見えてしまいます。
  • 真っ黒な影や輪郭線。
  • 跳ね続ける動き、リニアなイージング、すべてのパーツが一斉に動くこと。
  • 顔の上にテキストを置くこと、または 9:16 フレームの下から 670 px の範囲に重要な要素を置くこと。
  • 画面上のテキストやタイトルにスタジオ名やフランチャイズ名を使うこと。見た目の特徴を説明してください。

失敗例

見た目原因修正
平面的なベクターアート接地影がない、キーライトが弱い、マテリアルが平坦床に柔らかな影を加え、キーライトを強め、クリアコートとリムライトを追加します
リアルな 3D フィギュア頭が小さすぎる、顔が細かすぎる、手足が長い頭を身長の 60% 以上にし、顔は目と口だけに簡略化し、手足を短くします
プラスチック製品のレンダーメタルネスが 0 より大きい、エッジが鋭い、光沢が強いメタルネスを 0、粗さを約 0.4 にし、すべてを丸くします
ロボットのような動きリニア補間、予備動作がない、タイミングが均一予備動作、イージングされたカーブ、オーバーシュートを加え、腕の動きを 2~3 フレームずらします
無重力に見える着地時にスクワッシュがない、影が反応しない接地時にスクワッシュさせ、空中では影を小さくしてフェードさせ、最後に動きを落ち着かせます
色が濁って見える背景が暗い、影が灰色パステルカラーの背景にし、背景色に合わせて影に色を付け、彩度の高い主要色を 1 色使います
レンダーがちらつくアニメーションが壁時計の時刻を参照しているrenderAt 内で t だけを使って計算します

最終レンダーの前に検証する

npx hyperframes check                                   # lint, layout, contrast, timing
npx hyperframes snapshot --at <one time per beat at the peak of each action, plus one mid-transition>
npx hyperframes render -o renders/<name>.mp4
ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration -of csv=p=0 renders/<name>.mp4

すべてのスナップショットと snapshots/contact-sheet.jpg(全フレームを 1 枚にまとめた画像)を確認し、次の点を確かめます。

  • すべてのビートでシルエットが読み取りやすく、顔が見える
  • 接地影があり、ホップに反応している
  • ライティングに一貫性がある(キーライトは左上、輪郭にリムライトが見える)
  • すべての動きに予備動作と動きの収束があり、浮いて見えない
  • 選択したアスペクト比のセーフエリア内にテキストが収まっている
  • 出力サイズがアスペクト比(1080×1920 または 1920×1080)に一致し、長さも適切である

最終レンダーの前に自己評価する

スナップショットシートを、フック(最初の 1 秒)、スマートフォンの幅での読みやすさ、動きの質、構図、正確さ、「ミュートテスト」(音声なしで、視聴者が内容を一文で要約できるか)の各項目で 1~10 点で評価します。7 点以下の項目だけを修正し、再度スナップショットを作成して、すべてが 8 点以上になるまで繰り返します。可能であれば、自分の作品を評価しないよう、別の読み取り専用の工程で採点してください。

しないこと代わりにすること
情報量の多い 2 つのレイアウトをクロスフェードする古い内容を退出させてから新しい内容を表示するか、1 つのオブジェクトを持ち越します
複数のものを同時に動かす動きをずらし、その瞬間の焦点を 1 つにします
間を浮遊や揺れで埋める静止させるか、キャラクターに動作させます
「勢い」を出すためにパーティクル、虹色のグラデーション、3D フリップを加えるより強いイベント(登場、衝突、カメラ移動)を加えます
時間に収めるためにテキストを急いで表示する言葉を削ります

納品物

  • ポスター:サムネイルに表示されるよう、最も完成度の高い静止画をフレーム 0 に焼き込みます。ffmpeg -ss <t> -i renders/<name>.mp4 -frames:v 1 poster.jpg を実行し、続けて ffmpeg -i renders/<name>.mp4 -i poster.jpg -filter_complex "[0:v][1:v]overlay=enable='eq(n,0)'" -c:a copy renders/<name>-final.mp4 を実行します。
  • 投稿できる状態の、アイデアを技法ではなくアイデアの内容で示す1~2文のキャプション。
  • 各ビートで何が起こるかと、修正時に変更するパラメーターをユーザーに伝えます。

参考にするもの

  • @scheemunai:Three.js で制作された、スクワッシュ&ストレッチの演技を取り入れた90年代カートゥーン風の短編:https://x.com/scheemunai/status/2102788223835463902
  • @JaydenDavisNC:Blender で制作された、リグとスタジオ照明を使ったキャラクター短編:https://x.com/JaydenDavisNC/status/2107086714820870617
  • @angrypenguinPNG:1つのプロンプトから制作された、クレイアニメ風の卓上シーン(Blender、スキルはオープンソース化されています):https://x.com/angrypenguinPNG/status/2103205636662341661
  • @Stefan_3D_AI:ジャンプと噛みつきのサイクルを持つクリーチャーのリグ(予備動作とオーバーシュート):https://x.com/Stefan_3D_AI/status/2102641562824135022
  • HyperFrames のモーションに関する指針と、顔出しなしの解説動画制作スキル、JohnHeibel/ClaudeAnimationBase のアニメーションガイド、howseen-ai/claude-motion-design の批評ループ、latent-spaces/brag から着想を得ています(そのままコピーしたものではありません)。

コメント