[3D技術史 #02] Linear Blend Skinning — メッシュが繋がった革命と「キャンディラッパー」の壁

[3D技術史 #02] Linear Blend Skinning — メッシュが繋がった革命と「キャンディラッパー」の壁

この記事は「スキニング&リギング技術史」4部構成の第2部です。

  1. セグメントアニメーション — 関節がバラバラだった時代
  2. Linear Blend Skinning(本記事) — メッシュが繋がった革命と「潰れ」の壁
  3. DQS・PSD — 潰れを騙す数学的トリック
  4. GPU時代のリアルタイムスキニングと標準フォーマット

剛体モデルの限界を超えて

第1部で見た通り、剛体階層モデルは関節を曲げると隙間が生じるか、パーツ同士が貫通する。

この問題を根本的に解決したのがスキニング(Skinning)— 1つの連続したメッシュを、内部の骨格(ボーン)で変形させる技術。

1990年代後半、ゲーム機の性能向上に伴い、リアルタイムでスキニングを計算できるようになった。

ドリームキャスト(1998年)やPlayStation 2(2000年)の世代から、キャラクターの体は「1枚皮」になっていく。

スキニングの基本原理

頂点ウェイト — 「どのボーンに、どれだけ引っ張られるか」

スキニングの核心は頂点ウェイト(Vertex Weight)。

メッシュの各頂点が「どのボーンからどれだけ影響を受けるか」を数値(0.0〜1.0)で持つ。

例えば肘関節の近くにある頂点は:

頂点A:  上腕ボーン = 0.7,  前腕ボーン = 0.3
頂点B:  上腕ボーン = 0.3,  前腕ボーン = 0.7
頂点C:  上腕ボーン = 0.0,  前腕ボーン = 1.0  (完全に前腕に追従)

上腕ボーンの影響が強い頂点は上腕と一緒に動き、前腕ボーンの影響が強い頂点は前腕と一緒に動く。
中間のウェイトを持つ頂点は両方のボーンの動きをブレンドして追従する。

これが「関節が滑らかに曲がる」仕組み。

第1部の剛体モデルでは「パーツ → ボーン」が1:1対応だった。

スキニングはこれを「頂点 → 複数ボーン」の多:多対応に拡張した。
たったこれだけの発想の転換で、関節の隙間問題は根本的に解消された。

各ウェイトの合計は1.0

重要な制約がある。

1つの頂点に対する全ボーンのウェイト合計は必ず1.0でなければならない。

上腕 0.7 + 前腕 0.3 = 1.0  ✓
上腕 0.5 + 前腕 0.5 = 1.0  ✓
上腕 0.8 + 前腕 0.4 = 1.2  ✗ → 頂点が膨張する

合計が1.0を超えると頂点が元の位置より外側に飛び出し、1.0未満だと内側に沈み込む。

VRM→PMX変換で頂点ウェイトを読み出した時も、この正規化は意識する必要があった。

LBSの数学 — 行列の加重平均

Linear Blend Skinning(線形ブレンドスキニング)

スキニングの最も基本的な方式がLBS(Linear Blend Skinning)、別名マトリクスパレットスキニング。

各ボーンの変換行列を頂点ウェイトで線形補間(加重平均)する。

数式で書くと:

変形後の頂点位置 = Σ (ウェイト_i × ボーン行列_i × バインド逆行列_i × 元の頂点位置)

もう少し具体的に。

肘の近くの頂点Aが上腕ウェイト0.7、前腕ウェイト0.3の場合:

頂点Aの最終位置 = 0.7 × (上腕の現在行列 × 上腕のバインド逆行列 × 頂点A)
               + 0.3 × (前腕の現在行列 × 前腕のバインド逆行列 × 頂点A)

バインド逆行列 — 「初期姿勢を引き算する」

ここで登場するバインド逆行列(Bind Inverse Matrix / boneInverse)は、ボーンの初期姿勢(バインドポーズ、通常はTポーズ)の逆行列。

なぜ必要か。

ボーンの「現在の行列」には、初期姿勢の分の変換も含まれている。
初期姿勢からの差分(回転量)だけを頂点に適用したいので、まずバインド逆行列で初期姿勢を打ち消してから、現在の行列を掛ける。

スキン行列 = 現在のボーンワールド行列 × バインド逆行列

= 「今ボーンがいる場所」×「初期位置の逆」
= 「初期位置からの変化量」

初期姿勢のまま(Tポーズ)なら、スキン行列は単位行列になり、頂点は元の位置に留まる。
ボーンが回転した分だけ、頂点がそちらに引っ張られる。

VRM→PMX変換で書いた手動スキニング

VRM→PMX変換ツールでAスタンス化(腕を40度下げる)を実装した際、まさにこのLBSを手で書いた。

// 各ボーンのスキン行列を事前計算
const skinMats = [];
for (let bi = 0; bi < skeleton.bones.length; bi++) {
  const m = new THREE.Matrix4().multiplyMatrices(
    skeleton.bones[bi].matrixWorld,   // 現在のボーンワールド行列
    skeleton.boneInverses[bi]         // バインド逆行列
  );
  skinMats.push(m);
}

// 頂点ごとにLBSを適用
for (let i = 0; i < vertexCount; i++) {
  const basePos = getVertex(i);       // 元の頂点位置
  basePos.applyMatrix4(bindMatrix);   // バインドワールドへ変換

  const result = new Vector3(0, 0, 0);
  for (let j = 0; j < 4; j++) {       // 最大4ボーン
    const weight = weights[i][j];
    if (weight === 0) continue;
    const boneIdx = joints[i][j];
    // スキン行列 × 元の位置 × ウェイト を加算
    const transformed = basePos.clone().applyMatrix4(skinMats[boneIdx]);
    result.addScaledVector(transformed, weight);
  }
  // result が変形後の頂点位置
}

このコードは教科書的なLBSそのもの。

腕ボーンを40度回転させた状態でスキン行列を作り、全頂点に適用することで、メッシュがAスタンスで焼かれる。

ただし最初はThree.jsの applyBoneTransform を使ったところ、メッシュが渦巻き状に崩壊した。

原因は bindMatrixInverse の二重適用。

applyBoneTransform が内部でバインド逆行列を掛けた結果に、さらにワールド行列を掛けたことで、VRMのように bindMatrix ≠ matrixWorld の場合に変換が二重になった。

LBSの数式を理解していれば「バインド逆行列が2回掛かっている」と気づけるが、ブラックボックスのAPIに任せるとこういう罠にハマる。

結局、上のコードのように手動で1ステップずつ計算する方式に書き直して解決した。

LBSの最大の弱点:キャンディラッパー現象

LBSは関節の隙間問題を解決したが、新たな問題を生んだ。 キャンディラッパー現象(Candy-Wrapper Effect)。

何が起きるか

腕を180度捻ると、上腕のメッシュがキャンディの包み紙を絞ったようにペチャンコに潰れる。

ボリューム(体積)が失われ、腕が細い棒のようになってしまう。

なぜ起きるか

LBSは行列を線形補間しているのが原因。

2つの回転行列を単純に足し合わせると、回転の中間値ではなく、行列のスケール成分が縮んでしまう。

具体的に、ボーンAが0度、ボーンBが180度回転している場合:

ウェイト0.5の頂点の変換 = 0.5 × (0度の回転行列) + 0.5 × (180度の回転行列)

0度の回転行列 = [1, 0; 0, 1]
180度の回転行列 = [-1, 0; 0, -1]

線形補間 = 0.5 × [1,0;0,1] + 0.5 × [-1,0;0,-1] = [0,0;0,0]  ← ゼロ行列!

ゼロ行列を頂点に掛けると、頂点は原点に潰れる。 これがキャンディラッパー現象の数学的な正体。

回転行列を足し算するのは、本来は意味がない操作。

90度と-90度の回転を足しても「0度の回転」にはならず、「回転が消えてスケール0になる」。

LBSはこの「意味のない操作」を行っているために、大きな捻りでボリュームが失われる。

90度の曲げでも潰れる

180度の捻りは極端な例だが、実は90度程度の曲げでも潰れは起きる。

肘を90度曲げた時、内側が凹んで外側が膨らむのではなく、関節部分のメッシュ全体がわずかに痩せる。

この「わずかな痩せ」は目立たないが、モデルの品質が上がるほど(頂点が多いほど)気になるようになる。

MMDの「腕捩りボーン」が存在するのは、まさにこのLBSの弱点を緩和するため。

腕のひねりを1本のボーンではなく3本に分散させることで、各ボーンの回転角を小さくし、キャンディラッパー現象を軽減している。

ウェイトペイント — 職人芸の領域

自動ウェイトと手動調整

ボーンの配置が終わったら、各頂点にウェイトを割り当てる「ウェイトペイント」を行う。Blenderなどの3Dツールには自動ウェイト機能(Automatic Weights)があるが、自動では関節まわりのウェイトが甘くなりがち。

プロのキャラクターモデラーは、自動ウェイトをベースに、関節の変形が美しくなるように手動でウェイトを塗り直す。

肩・股関節・指の付け根など、複数のボーンが密集する部分は特に調整が難しく、職人芸の領域。

ウェイトが破綻すると何が起きるか

VRM→PMX変換で実際に遭遇した問題がある。

VRMの hips(腰)を MMDの センター にマッピングしていたため、腰の頂点ウェイトが「センター」ボーンに乗ってしまい、VMDモーションが「下半身」ボーンを回転させても腰のメッシュが動かなかった。

ウェイトがどのボーンに乗っているかは、正常品PMXをバイナリパースして頂点ウェイトを読み出すことで判明した。

「下半身に19,730頂点、上半身に11,420頂点がウェイトされている」

といった数値が、問題の切り分けに直結した。

ウェイトは地味なデータだが、スキニングの品質を決定的に左右する。

数学的にはLBSの式は単純でも、ウェイトの配分次第で結果は天と地ほど変わる。

スキニングが変えた3Dの風景

表現力の飛躍

スキニングの登場で、3Dキャラクターは「張りぼて」から「生きた体」になった。

  • 関節を曲げても隙間が出ない
  • 表情もスキニング(フェイシャルボーン)で作れる
  • 布や髪も物理ボーン+スキニングでなびかせられる

計算コストの増大

ただし代償もある。

剛体時代はボーン数分(10〜20回)の行列計算で済んだが、スキニングでは全頂点 × 影響ボーン数の計算が毎フレーム必要になる。

剛体時代:   20ボーン × 1行列演算 = 20回
スキニング: 10,000頂点 × 4ボーン = 40,000回の行列×頂点演算

1990年代後半のCPUでこれをリアルタイムに行うには、影響ボーン数を2本に制限する、頂点数を減らすなどの工夫が必要だった。この計算コストの問題は、第4部で扱うGPUスキニングで劇的に解決される。

次回予告

LBSは関節の隙間を解決したが、キャンディラッパー現象という新たな壁を生んだ。

第3部では、この「潰れ」を数学的に回避するDual Quaternion Skinning(DQS / 二重四元数スキニング)と、特定のポーズで補正形状を重ねがけするPose Space Deformation(PSD)を扱う。

LBSの弱点を、計算量を抑えつつどう克服したのか。