MMDのスケールと単位(1ユニット≒8cm)— モデルを縮小してはいけない理由
MMDのモデルは「1ユニット ≒ 8cm」という慣習で作られています。これをメートル前提のエンジンへ持っていくとき、モデルを縮小して合わせたくなりますが、そこが事故の入口です。カメラモーションと輪郭線が付いてこないためです。
1ユニットは約8cm(実モデルの身長で確かめる)
PMXのどこにも単位は書かれていません。実際のモデルの頂点座標から身長を出して、8cm換算が妥当かを確かめました。
| モデル | 頂点のY座標の範囲 | 高さ | 8cm換算 |
|---|---|---|---|
| アリシア | -0.03 〜 18.48 | 18.51ユニット | 1.48m |
| ad_org8 | 0.09 〜 20.62 | 20.53ユニット | 1.64m |
人の身長として妥当な値になります。キャラクターの全高は20ユニット前後と思っておけば、カメラ距離や床のサイズの見当が付きます。頭ボーンのY座標はアリシアが15.36、ad_org8 が18.15でした。
「1ユニット = 8cm」は慣習であって仕様ではありません。 実際にはモデルごとにばらつきます。固定のカメラ距離で撮ると、極端に小さく/大きく写るモデルが必ず出てきます。バウンディングボックスからカメラ距離を決めるようにしてください(このサイトのサムネイル生成はそうしています)。
モデルを縮小するとカメラモーションが壊れる
メートル前提のシーンに合わせるなら、モデルに 0.08 倍を掛けたくなります。これをやると、カメラVMDのアングルが大きく崩れます。
理由は単純で、カメラの位置トラックはモデルの子ではなく、カメラへ直接適用されるからです。モデルだけ縮小してもカメラの数値は元のスケールのままなので、被写体に対して相対的に遠くなります。ボーンモーションはモデルの子なので自動で追従するぶん、「モーションは合っているのにカメラだけおかしい」という分かりにくい形で出ます。
正解は逆向きです。モデルは等倍(MMDネイティブスケール)のままにして、シーン側の距離の定数を12.5倍(= 1 ÷ 0.08)する。 具体的には次のものが対象です。
| 対象 | 12.5倍する | 実際の値(このサイト) |
|---|---|---|
| カメラの初期位置・注視点 | する | y = 11.25 / z = 25 に置いている |
| カメラの near / far | する | 1.25 / 2,500 |
| カメラ操作の最小・最大距離 | する | 6.25 / 375 |
| ライトの位置 | する | 原点から 37.5 |
| 床・グリッド・スカイドームの大きさ | する | 床 750 × 750 |
| シャドウカメラのフラスタム | する | モデルに合わせて毎回詰める(後述) |
| 輪郭線の太さ | しない | PMXの値をそのまま(下記) |
| 物理演算 | しない | スケール適用より前に計算されるため |
輪郭線の太さはスクリーン空間のパラメータ
輪郭線の太さをスケールに合わせて12.5倍すると、輪郭が激太りします。 現行のmmdbox.netで実際にこの不具合を作りました。
three.js の OutlineEffect の頂点シェーダを読むと、輪郭のオフセットは pos + norm * thickness * pos.w としてクリップ空間で計算されています。pos.w はカメラからの距離に比例するので、パースペクティブ除算の後には画面上で一定の太さになります。つまりオブジェクトのスケールにもカメラ距離にも依存しません。
「距離やスケールに関する定数を一括で換算する」作業をするときは、その値が本当にワールド空間の値なのかを確かめてください。 既にスクリーン空間で正規化されている値(輪郭線の太さ、線の幅、アイコンの大きさ)を一緒に掛けると、そこだけ派手に壊れます。シェーダの実装まで見るのが確実です。
影がぼやける原因は解像度で、フィルタではない
スケールの話がいちばん効いてくるのが影です。影の精細さは「シャドウカメラのフラスタムの広さ ÷ シャドウマップの一辺」で決まります。
移植当初、このサイトは mapSize を未設定(three.jsの既定の512×512)のまま、フラスタムを ±62.5ユニット(= ±5 × 12.5)に取っていました。キャラの全高が約20ユニットなので、1ユニットあたり約4テクセルしかなく、自己影が輪郭のない灰色の染みになっていました。
| シャドウマップ | フラスタム | 密度 | |
|---|---|---|---|
| 移植当初 | 512 × 512 | ±62.5ユニット | 4.1テクセル/ユニット |
| 現在 | 2048 × 2048 | モデルに合わせて詰める | 49.7テクセル/ユニット(約12倍) |
- フラスタムはモデルのバウンディングボックスに合わせて毎回詰める。 モデルの追加・削除・立ち位置の変更のたびに計算し直します。
bias/normalBiasはテクセル1つ分の世界サイズから決める。 固定値のままフラスタムを詰めると、影が本体から浮きます(ピーターパン現象)。- フィルタ(PCFソフトシャドウ)の有無はぼけの原因ではありません。 フィルタ無しの
BasicShadowMapはくっきり、PCFSoftShadowMapはなめらか、という違いだけです。
mapSize を上げる代償の実測(GPU無しのソフトウェアラスタライザでの1フレームの描画時間)は次のとおりです。4096は割に合いません。
| シャドウマップ | 512 | 2048 | 4096 | 影OFF |
|---|---|---|---|---|
| 1フレーム | 105.5ms | 112.7ms | 135.1ms | 83ms |
照明の「位置」は向きで、長さに意味がない
照明VMDが持っている「位置」は座標ではなく平行光源の向きベクトルです。長さは明るさにも関係しません。ところが three.js の DirectionalLight は、明るさには関係しないのに、シャドウカメラをその位置に置きます。
そのため、向きだけを使って長さは一定の距離に正規化する必要があります。しないと、照明VMDが短いベクトルを渡してきたときに光源がモデルの内側に入り、影が欠けます。 このサイトは原点から 37.5(= 3 × 12.5)に揃えています。
スマホの画素密度をそのまま使わない
スケールの話の締めとして、描画量の話も1つ。setPixelRatio(window.devicePixelRatio) をそのまま渡すと、画素密度3の端末では描画量が9倍になります。2で丸めるだけで体感がまるで変わります。