形と見た目の次は「動き」

第3章でメッシュの形を、第4章でマテリアルによる見た目を扱いました。本章は3層の最後、「動き」を扱います。 キャラクターが歩く、腕を振る、まばたきする、口を動かして話す。こうした動きのほとんどは、頂点を1つずつ手で動かして作るわけではありません。 メッシュの内側に骨格を入れ、骨を回すと周りの頂点がついてくるように仕組みを作ります。これがリギングです。

Webエンジニアにとっては、DOMツリーとCSSの transform の関係を思い浮かべると入りやすい分野です。 親要素を回転させると子要素も一緒に回る、という継承の考え方が、そのまま3Dキャラクターの骨格になります。 本章ではこの「変換の階層」から始めて、スキニングの計算、表情を作るブレンドシェイプ、時間軸を扱うキーフレーム、glTFでの格納方法、アバター標準のVRM、そしてAIによる自動リギングの動向までを順に見ていきます。

シーングラフと変換の階層

ローカル座標とワールド座標

3Dシーンの物体は、ノードを親子の木構造に並べたシーングラフで管理されます。 各ノードは「親から見た自分の位置・回転・スケール」を持ちます。これをローカル変換と呼びます。 画面に描くときに必要なのは、シーン全体の原点から見た位置、つまりワールド変換です。

ワールド変換は、親のワールド変換に自分のローカル変換を掛けて求めます。glTF 2.0の仕様もこの通りに定めていて、ノードのグローバル変換行列は「親ノードのグローバル変換行列と自分のローカル変換行列の積」、親がいなければローカル変換そのもの、と書かれています。 ローカル変換は平行移動(T)・回転(R)・スケール(S)の3つを行列にして T * R * S の順に掛けます。頂点に対しては、まずスケール、次に回転、最後に平行移動の順で効きます。

// シーングラフのワールド行列を根から葉へ順に計算する
function updateWorld(node: SceneNode, parentWorld: Mat4): void {
  // ローカル行列は T * R * S(スケール → 回転 → 平行移動の順に効く)
  const local = mul(mul(translation(node.t), rotation(node.r)), scaling(node.s));
  node.world = mul(parentWorld, local);
  for (const child of node.children) {
    updateWorld(child, node.world);
  }
}

updateWorld(root, identity());

肩のノードを30度回すと、その子である肘・手首・指のワールド行列がすべて変わります。 子ノードのローカル値は何も変えていないのに、手が動いて見えるのはこのためです。 回転は第4章までと同じくラジアンで、glTFではクォータニオン(4成分の回転表現)で持ちます。

ボーン・スケルトン・ジョイント

キャラクターの骨格は、このシーングラフの一部として表現されます。骨1本をボーン、ボーンの木全体をスケルトン(Blenderではアーマチュア)と呼びます。 glTFは「ボーン」ではなくジョイント(関節)という言葉を使います。中身は普通のノードで、スキンに「このノード群を関節として使う」と登録されているだけです。 ボーンは2つの関節を結ぶ棒、ジョイントはその付け根の回転中心、と捉えると、ツールごとの呼び名の違いに混乱しにくくなります。

graph TB
  H[hips 腰] --> S[spine 背骨]
  S --> C[chest 胸]
  C --> N[neck 首]
  N --> HD[head 頭]
  C --> LS[leftShoulder]
  LS --> LUA[leftUpperArm]
  LUA --> LLA[leftLowerArm]
  LLA --> LH[leftHand]
  H --> LUL[leftUpperLeg]
  LUL --> LLL[leftLowerLeg]
  LLL --> LF[leftFoot]

  style H fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style LH fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style LF fill:#3b82f6,stroke:#1d4ed8,color:#fff
人型スケルトンの階層(左半身のみ。名前はVRMのヒューマノイドボーン名)。根は腰(hips)で、末端の手足は親の変換をすべて引き継ぐ

人型の根は腰に置くのが一般的です。歩くときに全身を移動させるのは腰のボーンで、ほかのボーンは基本的に回転だけを持ちます。 後で触れるVRMの仕様でも、hipsについて「通常このボーンだけが移動し、他のボーンは回転のみ」と注記されています。

スキニング — 頂点を骨に追従させる

リニアブレンドスキニング

骨格を動かしただけではメッシュは動きません。各頂点が「どのボーンにどれだけ追従するか」を決め、骨の動きを頂点に伝える計算が必要です。これをスキニングと呼びます。 最も広く使われているのがリニアブレンドスキニング(LBS、Linear Blend Skinning)です。glTF仕様の用語集は、LBSを「頂点ごとの変換行列を、変換行列の線形な重み付き和として計算する方法」と定義しています。

肘の近くにある頂点を例にします。この頂点を上腕ボーンに60%、前腕ボーンに40%追従させると決めておきます。 肘を曲げたら、上腕ボーンの変換で動かした位置と前腕ボーンの変換で動かした位置を、0.6対0.4で混ぜた場所に頂点を置きます。 式で書くと、変形後の頂点 v' は「Σ wᵢ × Mᵢ × v」です。wᵢ は重み(ウェイト)、Mᵢ はボーン i のスキン行列で、ウェイトの合計は1にします。

逆バインド行列の意味

ここで重要になるのが逆バインド行列(inverse bind matrix)です。 メッシュの頂点座標は、モデルを作った時点の姿勢(バインドポーズ、多くはTポーズ)で、モデル空間に記録されています。 ボーンが「バインド時からどれだけ動いたか」だけを頂点に反映したいので、次の2段階を踏みます。

  1. 逆バインド行列を掛ける: 頂点をモデル空間から「バインド時のボーンから見た座標」に変換する。逆バインド行列は、バインド時のボーンのワールド行列の逆行列
  2. 現在のワールド行列を掛ける: ボーンから見た座標を、いま動いているボーンのワールド行列でモデル空間に戻す

ボーンが動いていなければ、2つの行列は互いに打ち消し合って単位行列になり、頂点はそのまま残ります。 ボーンが回っていれば、その回転の分だけ頂点が動きます。つまりスキン行列 = 現在のワールド行列 × 逆バインド行列です。 glTFでは skin.inverseBindMatrices がこの行列の配列を指し、順番は skin.joints と一致させる決まりです。省略した場合は各行列が単位行列とみなされます。

// リニアブレンドスキニング(LBS)の擬似コード

// 1. フレームごとに、ジョイントごとのスキン行列を作る(CPU側)
function computeSkinMatrices(joints: SceneNode[], inverseBind: Mat4[]): Mat4[] {
  // 現在のワールド行列 × 逆バインド行列 = 「バインド時からの差分」
  return joints.map((joint, j) => mul(joint.world, inverseBind[j]));
}

// 2. 頂点ごとに、影響するジョイントの変換を重み付き平均する(頂点シェーダ相当)
function skinVertex(
  position: Vec3,       // バインドポーズでの頂点位置
  jointIndex: number[], // JOINTS_0(skin.joints 配列の添字が4つ)
  weight: number[],     // WEIGHTS_0(合計がほぼ1になる4つの重み)
  skin: Mat4[],
): Vec3 {
  let result = vec3(0, 0, 0);
  for (let k = 0; k < 4; k++) {
    if (weight[k] === 0) continue;
    const moved = transformPoint(skin[jointIndex[k]], position);
    result = add(result, scale(moved, weight[k]));
  }
  return result;
}

実際のエンジンでは、1のスキン行列だけをCPUで計算してGPUに渡し、2は頂点シェーダで全頂点を並列に処理します。 頂点数が数万あっても、CPUの仕事はボーン数ぶんの行列計算だけで済みます。Three.jsの SkinnedMesh もこの構成です。

ウェイトペイント

頂点ごとのウェイトを決める作業をウェイト付けと呼びます。Blenderでは、ボーンごとに同名の「頂点グループ」を作り、そこにウェイトを持たせます。ウェイトペイントモードでは、メッシュの表面を筆で塗るように影響度を編集でき、赤が1.0、青が0.0のように色で表示されます。 BlenderのArmatureモディファイアには、頂点グループを使う方法のほかに、ボーンの周囲に一定半径の影響範囲(エンベロープ)を持たせて近くの頂点を動かす方法もあります。

ウェイトはキャラクターの見栄えを大きく左右します。肩や股関節のように複数のボーンが集まる場所は、ウェイトの境目がそのまま変形の不自然さとして見えてしまいます。 第3章で触れた「エッジフローの整ったクアッド中心のトポロジー」が変形に強いと言われるのは、関節まわりに十分な頂点があり、ウェイトをなめらかに変化させられるからです。

LBSの弱点 — キャンディラッパー現象とデュアルクォータニオン

LBSは軽くて単純ですが、行列を線形に混ぜる点に弱点があります。回転行列どうしを足して平均した結果は、一般に回転行列になりません。 たとえば前腕を軸まわりに180度ひねると、0度側と180度側の変換を半々に混ぜた中間の頂点が軸に向かって縮み、包み紙の両端をねじったキャンディのように細くつぶれます。 これがキャンディラッパー現象です。肘を深く曲げたときに関節の内側が痩せる現象も同じ原因で起きます。

対策として知られているのがデュアルクォータニオンスキニング(DQS)です。回転と平行移動を「デュアルクォータニオン」という形でまとめて表し、行列ではなくこちらを混ぜることで、混ぜた結果も剛体変換(回転+平行移動)に保ちます。 BlenderではArmatureモディファイアの「Preserve Volume(体積を維持)」がこれに当たります。マニュアルは、オフにすると関節の回転で周りのジオメトリが縮み、180度付近ではほぼゼロまで縮むこと、オンにすると縮まなくなる代わりに180度に達したところで不連続な「隙間」が出ることを説明しています。

項目リニアブレンドスキニング(LBS)デュアルクォータニオンスキニング(DQS)
混ぜるもの4x4の変換行列デュアルクォータニオン(回転+平行移動)
ひねり・深い曲げ体積が痩せる(キャンディラッパー)体積を保ちやすい
苦手なこと大きな回転180度付近の不連続、スケールの扱い
計算コスト最小LBSよりやや重い
glTF 2.0での扱い仕様が前提とする方式仕様には含まれない(実装側の選択)

ブレンドシェイプ — 骨では作れない動き

表情の変化、口の開閉、まばたき、頬のふくらみといった動きは、骨を回すだけでは作りにくいものです。 こうした変形にはブレンドシェイプを使います。同じトポロジー(同じ頂点数・同じつながり)のまま頂点位置だけを変えた形を何種類も作っておき、重みを付けて基本形に足し込みます。 呼び名はツールごとに違い、Blenderはシェイプキー、glTFはモーフターゲット、MayaやUnity系はブレンドシェイプと呼びます。

glTFの仕様では、モーフターゲットは基本形からの差分(デルタ)として格納され、変形後の位置は「元のPOSITION + weights[0] × targets[0].POSITION + weights[1] × targets[1].POSITION + …」で計算されます。 ターゲットは mesh.primitives[].targets に入れ、POSITION・NORMAL・TANGENTの3属性に対応することが推奨されています。

たとえば「口を開けた形」の重みを0.0から1.0へ動かせば口が開き、「笑顔」0.5と「まばたき」1.0を同時に効かせることもできます。 音声に合わせて母音ごとの口の形の重みを切り替えれば、口パク(リップシンク)になります。 骨格は体の大きな動き、ブレンドシェイプは顔の細かな動き、という分担が一般的です。

キーフレームと補間

キーとキーの間を埋める3つの方式

骨やブレンドシェイプをどう動かすかを時間軸に沿って記録したものがアニメーションです。 毎フレームの値をすべて保存するのではなく、特定の時刻の値(キーフレーム)だけを記録し、間は補間で埋めます。 CSSの @keyframes で0%と100%だけを書き、間をブラウザに任せるのと同じ発想です。

glTF 2.0の補間方式は3種類です。既定値はLINEARで、回転に対しては球面線形補間(slerp)を使うことが推奨されています。 クォータニオンの各成分を単純に線形補間すると回転の速さが一定にならないため、球面上をまっすぐ進むslerpを使います。

interpolation動き出力値の数向いている用途
LINEAR(既定)キー間を直線で補間。回転はslerp入力(時刻)と同数モーションキャプチャを焼いたデータなど、キーが密なもの
STEP次のキーまで前の値を保持入力と同数パラパラ漫画風の動き、表示・非表示の切り替え
CUBICSPLINE接線付きの3次スプラインでなめらかに補間入力の3倍(入り接線・値・出接線)手付けアニメーションをキー少なめで滑らかに保存

FKとIK

アニメーターがポーズを付ける方法には2通りあります。FK(フォワードキネマティクス)は、肩→肘→手首と根元から順に関節を回して手の位置を決める方法です。シーングラフの計算そのままなので、腕を振るような弧を描く動きを付けやすくなります。IK(インバースキネマティクス)は逆に、「手をこの位置に置きたい」「足をこの地面に付けたい」と末端の目標を指定し、途中の関節角度をソルバーに計算させます。 歩行で足が地面を滑らないようにする、ドアノブに手を添える、といった場面ではIKが欠かせません。

ただし、glTF 2.0のアニメーションで動かせるのはノードの変換(TRS)とモーフターゲットの重みだけです。IKの拘束条件そのものはファイルに入りません。 DCCツールでIKを使って付けた動きは、書き出し時に各ボーンの回転キーフレームへ焼き込む(ベイクする)ことになります。 ゲームエンジンのように実行時にIKを解く仕組みは、エンジン側の機能です。

glTFでの格納 — skin と animation

skin: JOINTS_0 と WEIGHTS_0

ここまでの仕組みがglTFでどう表されるかを確認します。スキン付きメッシュの各頂点は、通常のPOSITIONやNORMALに加えて次の2属性を持ちます。 ノードがskinを持つ場合、そのメッシュのすべてのプリミティブがJOINTS_0とWEIGHTS_0を持つことが必須です。

  • JOINTS_0: その頂点に影響するジョイントの番号4つ(skin.joints 配列の添字)
  • WEIGHTS_0: それぞれの影響度4つ。負の値は禁止で、float型なら合計を可能な限り1.0に近づけ、正規化された符号なしbyte/shortなら正規化前の合計を255/65535ちょうどにする

よく誤解されますが、glTFの影響数は「最大4」ではなく「1セットあたり4」です。 5本以上のジョイントの影響を受ける頂点があれば、JOINTS_1/WEIGHTS_1という次のセットに追加の4つを格納できます。 ただし実行環境が2セット目以降をどこまで扱うかは別問題なので、配信用のモデルでは1頂点4影響に収めておくのが無難です。 仕様がウェイトの合計に厳しいのは、合計が1からずれると、その誤差がジョイントの位置に掛け算されて頂点が大きくずれてしまうからだと説明されています。

animation: channel と sampler

アニメーションは animations 配列に入り、1つのアニメーション(「歩く」「走る」など)はchannelsとsamplersの組で記述されます。 samplerは「時刻の配列(input、秒単位)」「値の配列(output)」「補間方式(interpolation)」を持つデータ本体で、channelは「どのsamplerの値を、どのノードの何に流し込むか」を指す配線です。 channelの target.path に指定できるのは translation・rotation・scale・weights の4つです。

{
  "animations": [
    {
      "name": "Wave",
      "channels": [
        { "sampler": 0, "target": { "node": 5, "path": "rotation" } },
        { "sampler": 1, "target": { "node": 9, "path": "weights" } }
      ],
      "samplers": [
        { "input": 10, "output": 11, "interpolation": "LINEAR" },
        { "input": 10, "output": 12, "interpolation": "STEP" }
      ]
    }
  ],
  "skins": [
    { "inverseBindMatrices": 3, "joints": [1, 2, 4, 5, 6], "skeleton": 1 }
  ]
}

この例では、5番ノード(たとえば右前腕のジョイント)の回転と、9番ノードのメッシュが持つモーフ重みを、同じ時刻配列(accessor 10)に沿って動かしています。 スキンアニメーションに専用の仕組みがあるわけではなく、「ジョイントであるノードの回転を動かす」だけで、スキニングの計算がメッシュに反映されます。 なお仕様が定めるのはキーフレームの保存方法だけで、自動再生するか、ループするかといった再生時の振る舞いはアプリ側に任されています。

graph LR
  A[animation Wave] --> CH[channels]
  A --> SM[samplers]
  CH -->|sampler番号| SM
  CH --> T["target: node + path"]
  SM --> IN["input: 時刻 秒"]
  SM --> OUT["output: 値"]
  SM --> IP[interpolation]
  T --> N[ジョイントのノード]
  N --> SK[skin.joints]
  SK --> M[スキン付きメッシュ]

  style A fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style M fill:#3b82f6,stroke:#1d4ed8,color:#fff
glTFのアニメーションの構造。samplerがデータ、channelが配線。ジョイントのノードを動かすと、skin経由でメッシュが変形する

モーションキャプチャとリターゲット

モーションキャプチャ

人の動きを手でキーフレームとして付けるのは大変な作業です。そこで実際の人の動きを計測して取り込むモーションキャプチャが広く使われています。 方式は大きく3つに分かれます。体に付けた反射マーカーを多数のカメラで追う光学式、体に付けた慣性センサー(IMU)で姿勢を推定する慣性式、そして普通の動画からAIで骨格の姿勢を推定するマーカーレス式です。 どの方式でも、出力は「各ボーンの回転を毎フレーム記録したキーの密なデータ」になるため、glTFに書き出すときはLINEAR補間の密なキーとして保存されるのが一般的です。

リターゲット

撮った動きや買ったモーションを別のキャラクターに流用する処理をリターゲットと呼びます。 これが簡単ではない理由は3つあります。1つめはボーンの名前と構成がモデルごとに違うこと。2つめは手足の長さや比率が違うこと。3つめは基準姿勢やボーンの軸の向きが違うことです。 たとえばFBXのボーンは-X方向、Blenderのボーンは+Y方向を軸にするため、ツール間を行き来すると向きの補正が要ります。

名前と構成の違いを吸収するために、多くの環境は「人型ならこの部位はこの名前のボーン」という共通の対応表を持ち、モーションをその標準骨格経由で受け渡します。 この考え方をファイル形式のレベルで標準化したのが、次に紹介するVRMです。

VRM — 人型アバターの標準

VRMは、glTF 2.0をベースにした人型アバター用のファイル形式です。VRMコンソーシアム(2019年4月設立)が管理し、2022年9月22日にVRM 1.0が正式リリースされました。 リリース時点でUniVRM、バーチャルキャスト、VRoid Hubなどが対応しています。 ピクシブのキャラクター制作ソフトVRoid Studioや、2018年12月に始まった投稿・配布サービスVRoid Hubを通じて、VTuberやメタバースのアバターで広く使われています。

VRMの特徴は、汎用形式のglTFに「アバターとして動かすための約束事」を拡張として載せた点にあります。

  • ヒューマノイドボーン: 55本の人型ボーン名(指30本を含む)を定め、そのうちhips・spine・head・左右の上腕/前腕/手・左右の太もも/すね/足の15本を必須とする。どのアプリでも同じモーションで動かせる
  • VRM Tポーズ: 基準姿勢を、+Z方向を向いて左右対称に直立し、上腕だけを真横に上げた姿勢と定める。リターゲットで基準姿勢がずれる問題を仕様で防ぐ
  • 表情(Expression): happy・angry・sad・relaxed・surprisedの感情、aa・ih・ou・ee・ohの口形(リップシンク用)、blinkなどのまばたきをプリセット名で定義。中身はブレンドシェイプなどの組み合わせ
  • 揺れもの(SpringBone): 髪やスカートを物理的に揺らすボーンの設定
  • 利用許諾メタデータ: アバターごとの利用条件(誰が使ってよいか、商用利用の可否など)をファイル内に持つ
  • MToon: アニメ調のトゥーン表現用マテリアル(PBRも使える)

ブラウザで扱うならThree.js向けのthree-vrm、UnityならUniVRM、BlenderにはVRM用アドオンがあります。 VRMは、日本発の3D形式標準として数少ない成功例と言えます。本章で扱った骨格・ウェイト・ブレンドシェイプ・Tポーズといった要素を、アバター用途に絞って規約化した形式だと見ると理解しやすいでしょう。

AIによる自動リギングの動向

リギングとウェイト付けは、熟練者でも時間のかかる工程です。Blenderにはボーンからの距離をもとに自動でウェイトを付ける機能が昔からありますが、骨格を置く作業や、関節まわりの手直しは人が行ってきました。 この工程に機械学習が使われ始めています。

  • UniRig / RigAnything(SIGGRAPH 2025): 任意の3Dモデルから骨格とスキニングウェイトを予測する研究。以降の研究の土台になっている
  • Skin Tokens / TokenRig(arXiv、2026年2月): UniRigの著者陣による研究。スキニングを離散トークンの系列予測として解き、既存手法よりスキニング精度が98〜133%向上したと主張している
  • ViP-Rig(arXiv、2026年7月): 視覚的なプロンプトで骨格とスキニングを制御する研究

製品側では、3D生成サービスが「生成→自動リギング→アニメーション」を一続きで提供する流れが進んでいます。MeshyはMeshy 5以降でリギングと500以上のアニメーションを用意し、Tripo Studioも自動リギングを備えています。Hunyuan3D Studio(招待制)も同様の機能を持ちます。 ただし、AIが生成したメッシュは三角形が乱雑に並ぶことが多く、関節まわりのトポロジーが変形に向いていない場合があります。 自動でウェイトを付けても肩や股関節が破綻しやすいのはこのためで、各社がクアッドのネイティブ生成やリトポロジーに力を入れているのは、リギングのしやすさとも直結しています。 生成AIの全体像は第10章で詳しく扱います。

まとめと次章へ

仕組み何を動かすかglTFでの表現
シーングラフノードのTRS。親の変換を子が継承nodes と children、T * R * S
スキニング(LBS)頂点を複数ジョイントで重み付け平均skin(joints・inverseBindMatrices)、JOINTS_n / WEIGHTS_n
ブレンドシェイプ頂点位置の差分を重みで足すprimitives の targets、mesh.weights
キーフレーム時刻ごとの値と補間方式animations の channels / samplers
IK・拘束実行時の計算(ファイルには入らない)回転キーにベイクして格納

動きの仕組みは、結局のところ「変換行列の木をどう更新し、その結果をどう頂点に配るか」に集約されます。 骨の変換は親から子へ掛け算で伝わり、逆バインド行列でバインド時からの差分を取り出し、ウェイトで頂点ごとに混ぜます。 顔の細かな動きはブレンドシェイプで補い、時間方向はキーフレームと補間で表します。VRMはそれをアバター用途に規約化したものです。

これで3Dモデルの「形・見た目・動き」がそろいました。次章では、こうして変形したメッシュとマテリアルを、実際に画面のピクセルへ変える工程を扱います。 MVP変換からZバッファまでのラスタライズと、光の経路を追うレイトレーシングの仕組みを見ていきます。

主な参考資料

理解度チェック

問題 0 / 50%
Q1

スキニングで逆バインド行列を使う理由として最も適切なものはどれですか?

キーボード: 1〜4 で選択、Enter で回答