Unity + MMD4Mecanim + oculus quest(非oculus link)環境で発生した問題あれこれ
※下書き中
Windows上では問題なかったのに、quest上で動かすと発生した問題がメインです。
Windows上でも発生するがquest上だと(立体視したり、近くで細かく見ると)違和感が強く出る描写上の問題や、そもそもwindows/quest関係なく発生する問題も一部含まれます。
制作物の方針
quest上でモデルが表示されない
Unity上で使うと違和感が出るモデルがある
スクリプトでStandardシェーダに切り替え
歯が顎から飛び出す 口が歪む 口が開いた状態になる
目玉の制御方法について (白目になる、瞼を貫通するなど)
関節が曲がりすぎると破綻する
揺れものの一部が黒くなる
足がスカートを貫通する
AddComponentで付与したMMD4MecanimAnimMorphHelperが動作しない
quest上で表情(モーフ)が変化しない(MMD4MecanimAnimMorphHelperが動かない)
一部のモーフが動作しない
中途半端なweightのモーフが破綻する
モデルのサイズや頭身を変える
モデルのサイズを変えるとラグドール時の挙動がバグる
quest上でBulletPhysicsによる物理演算が動かない
モデルの頭や手足のオブジェクトを探して変数に保管
Dボーンのあるモデルがうまく動かない
モデルの一部を非表示にしたい
特殊な姿勢から自然に次のモーションを開始したい
Unity変換後にモーションが正常に動かない
Playable APIでIAnimationJobによる左右反転とAnimationMixerPlayableを併用
Sceneビューでは問題ないのにGameビューでは影にノイズが見える
#制作物の方針
基本的には以下のような方針で作っているので、似たような方針の方の参考になればと。
・モデルはHumanoid化して使い、モーションも汎化する。
多数のモデルに多数のモーションを共有させると容量が肥大化します。
(50のモデルに100のモーションを登録する場合、モデルごとにモーションを専用化すると、50x100=5000のモーションデータが必要になる)
これはディスクにもメモリにも、アセットバンドル化の処理時間的にも厳しいので、Humanoid化して、すべてのモデルで汎用モーションを使い回します。
汎用モーションは再現性が落ちますが、モデル間で共有できる上に、モーション1つあたりの容量も1/5程度なので、多数のモデルやモーションを使う場合は必須な気がします。
・アニメーションはPlayable APIで動的にグラフを生成させる
大量のモーションを静的なグラフで管理するのは困難なので、任意のタイミングで任意のモーションに切り替えられるようにします。
・物理演算はリアルタイムで
焼き込みの物理演算の方が再現性は高いですが、大量のモデル&大量のモーションの組み合わせすべてを焼き込むと膨大なデータ量になってしまいます。
なので物理演算はデバイス上でリアルタイムに計算します。
・IAnimationJobを使う(MMD関係ないですが)
左右対象などの似たようなモーションを大量にメモリに読み込むのは無駄ですので、同じモーションをIAnimationJobで改変して使い回せるようにします。
・重複するモデルデータ(fbx)を用意しないようにする。
モデルのサイズ、シェーダ、等身、部位消去などの差異は、単一のfbxからスクリプトで変更できるようにします。
・なるべくモデルデータ・モーションデータそのものを改変しなくて良い方法を探す。
モデルの取り込み後にスクリプトで修正可能な問題であれば、その手法を採用します。
MMD以外での利用OKでも、改変はNGなモデルも多いので、可能な限りデータをいじりたくないです。
・モデルごと、モーションごとの個別にカスタマイズが必要な問題解決は可能な限り避ける。
複数のモデルやモーションで同様の問題が発生し、共通の処理で解決できるなら、可能な限り共通化します。
(モデルのボーン構造や個別の名称に左右されない解決法があればそれを採用するなど。新しいデータで同様の問題が発生した時に楽したい。)
#quest上でモデルが表示されない
■現象
Windows上では問題なく表示されるのに、quest上では何も表示されない。
■原因
MMD4Mecanimデフォルトのシェーダがandroidに対応してない?
※最近試すとMMD4Mecanimのデフォルトシェーダでも表示されました。
Unityを2019から2020.2に更新したタイミングか、その後に新規プロジェクトを作って中身を移行したタイミングで治った可能性あり。
ただ、Standardシェーダの方がVRとの親和性が高いような気もするので残しておきます。
以下、シェーダごとの見た目のサンプルです。
MMD4Mecanim変換後デフォルトシェーダ(Directional Light強め)
MMD4Mecanimシェーダ(-Edge系を使用しない)(Directional Light強め)
Standardシェーダ(Directional Light強め)
Standardシェーダ(environment light強め)
サンプルのように、MMD4MecanimシェーダとStandardシェーダを比較すると、同じDirectional Light強めの環境でも、影の見え方が大きく異なります。
具体的に言うと、MMD4Mecanimシェーダでは、Standardシェーダと比べて光の影響を受けづらく、影は良い感じに薄く表示してくれます。
そのため、MMD4MecanimシェーダのモデルをStandardシェーダで作られたステージの中に配置すると、明るい場所では相対的に暗く、暗い場所では相対的に発光しているように見えます。
■対応
この問題が発生した時はStandardシェーダに置き換えて解決していました。
以下、Standardシェーダに置き換える際の注意点や工夫です。
・そのままStandardに置き換えても透過モーフを判別しないので、顔色がバグったようになる場合があります。
透過マテリアルは、standard化した後にRendering ModeをFadeにしておきます。
基本的にはPMX editorの「材質」で非透過度が0になっているものが透過モーフです。透過モーフは、一覧の左にある灰色の正方形の色が薄いのでそれを見て探します。
「effect」「face_emo」「eye_sh」「touka」「morph」といった名前のものは大体が透過が必要です。
「青ざめ」「頬ぞめ」「照れ」表情専用のマテリアルは見落としやすいので注意。色々な表情に変えてみて問題ないことを確認します。
他にも、メガネのレンズや薄いヴェールなんかも透過だったりします。
スカートのレースなど、部分的に向こう側が見える切り抜き細工などはFadeではなくCutoutでも良いかもしれません
透明で無いテクスチャでも、このようなテクスチャ化けが発生する場合は、Cutoutに変えることで解決する場合があります。
以下は、上から順に、MMD4Mecanimシェーダ、Standardシェーダ(Opaque)、Standardシェーダ(Cutout)
※Standardシェーダの2つは、シェーダを置き換えただけで、色などは未調整の状態です。
※Windowsではcutoutできれいに表示されても、quest上では、cutoutが効かない(Opaqueと同じように表示される)こともあるので、ちゃんと実機でチェックが必要です。
・非透明材質なのに透明にしたい材質
MMDモデルの「材質」の非透過度が1なのに、見た目上透過している場合もあります。(前髪で多く見ます)
仕組が理解できていませんが、このよう材質でもStandardシェーダのRendering ModeをFadeにすれば同様の表現が可能です。
ただし、この場合は他のFadeオブジェクトとの描写順が怪しくなります。
たとえば下図のように、半透明な前髪を半透明なメニュー越しに見た時、ある角度だと前髪がメニューの色の影響をうけずに表示され、別の角度だと色の影響はうけながらも、メニュー上のUIを隠したりします。
原因はFade状態のStandardシェーダーでZWriteがうまく動作しないこと。(有効化しても描写が怪しいので、最初から_ZWriteは無効化しておいた方が無難)距離に応じてQueueを変えるような処理を別途作れば一応は回避可能だが、それでも複数の材質がクロスしている時、材質Aの前半分は材質Bより前、材質Aの後半分は材質Bより後のような描写するなら、きちんとZWrite計算してあげるシェーダーが必要。
独自のシェーダを作るか、モデル自体を改造しないと根本的には解決できない気がします。
なお、問題が発生するのは半透明な2つのオブジェクトが重なる場合だけですので、間に非透明なオブジェクト(OpaqueやCutOut)が挟まっていれば問題は生じません。
UIの背景をImageで描写している場合、Imageはたとえ非透過0%設定でもFadeと同様に処理されるために同様の問題が生じますが、UIの少し後ろにPlaneでも置いて、Rendering Mode=OpaqueのMaterialを貼り付けておけば、距離に応じて正しい順序で描写してくれます。
(つまり、UIの向こうにある半透明なモデルよりも、非透明なPlaneが優先して描写され、非透明なPlaneよりも少し手前にあるimageが優先して描写される様子。これを応用して解決できないものか、、、)
・Standardシェーダは裏面からの描写に対応していません。
薄い上着、袖、マント、スカートなどが、裏面から見ると透けて見えます。
Unityデフォルトでも両面描写のシェーダはありますが、Standardシェーダと併用すると違和感が出ます(特にDirectional Lightが強い環境で)
そこで、StandardシェーダにCull Offを追加した両面シェーダを用意し、「両面描写が必須なマテリアルにだけ」適用します。(基本的にはMMDモデルの「両面描写」設定が有効な材質だけでOK)
以下サイトではCull Off以外に色々やっていますが、衣装用ならCull Offだけの方が良いです。
参考:https://note.com/nanase_jp/n/n8de032347abd
Cull Offを追加したStandardシェーダは、光に対して以下のような挙動になります。
・表面に当たった光で明るさが決まる。(裏面の明るさも、「表面に当たった光」で決まる)
・裏面に当たった光に影響しない。(裏面からしか光があたっていないと、表も裏も暗い)
理想的とはいえませんが、厳密に両面の明るさを管理すると無茶苦茶重くなる。(多分、やり方が間違っている。シェーダの勉強しないと。。)
衣服のテクスチャで裏面から光が当たって違和感出るのはマントや大型リボンくらいなので妥協しました。
改変シェーダを最初から適用してアセットバンドル化するなら問題ないですが、後からスクリプトで変更する場合は、Edit → Project Settings → Graphics →Always Included Shadersに登録しておかないとNullReferenceExceptionになるので注意が必要です。
(あるいは、そのシェーダを使っているマテリアルをHierarchyかResourcesに1つでもおいておけばOK。その方が管理が楽かも。厳密にはCompiling shader variants時の処理を考えて工夫しないと見た目が微妙に変わりそうですが、そのあたりはまだ理解できていない。)
・MMD4Mecanimが生成したマテリアルファイルそのものを直接いじる場合、Lockをかけないとシェーダ設定が勝手に戻されますので、pmxファイルのMaterialを開き、下の方にあるMaterials上で、すべてのmaterialの「Locked」をチェックしておきます。
勝手に戻されるのはMMD4Mecanimが直接生成したファイルだけですので、後述のようにスクリプトで動的にシェーダを切り替える場合や、MMD4Mecanimが生成したマテリアルを複製して加工し、プレハブ化したモデルの参照先マテリアルをそちらに切り替えている場合は対応不要です。
・色が黒っぽくなったり、白っぽくなる場合は、Albedoの色を変えて調整します。
全AlbedoをColor.whiteに置き換えた後、hadaやfaceだけ肌色に調整すれば大抵は良い感じになります
肌の色が真っ白だとWindows上では違和感なくてもquest上だと真っ白な肌の違和感が際立つ気がするので注意が必要です。
・Standard化する時にAlbedoのbitmapが外れて単色化してしまう場合があります。
Texture ShapeがCubeだと、StandardのAlbedoに使えませんが、2Dに戻せば使えます。
・それでも描写順が変。模様や眼球が消えてしまう場合、、、
renderQueueを細かく設定することで制御しているモデルの場合、Standardシェーダで描写順を再現するには、モデル自体の改変が必要かも。
renderQueueの細かな設定はシェーダを変えるとだいたい失われてしまい、元のシェーダに戻しても治りませんので、シェーダを変更する際には注意が必要です。
他にも、SphereCubeで質感を表現しているような場合は、Standardシェーダで良い感じに再現するのは困難かと思います。
#違和感が出るモデル
MMDとして使えば問題無いのに、Unity上では、あるいはquest上では違和感が出るもの。
■現象1 モーフを変えると顔に黒い影ができる
口の頂点しか登録されていないモーフのweightを上げただけなのに、顔全体が陰る。
顔と関係ないモーフ(手足のアクセサリ消しなど)でも同様に顔が陰る。
複数のモーフのweightを同時に上げるとどんどん黒くなっていく。
原因不明。
モーフ変更で発生する微妙な凸凹で影ができるのか?と思ったが、頂点を一切いじらないダミーモーフのweightを上げても謎の影がきできてしまう。
Directional LightやSpot Lightの光量を落として、代わりに環境光多めにすれば影響の軽減は可能。
※Rendering→Lighting→Environmentで、Environment Lighting→SourceをColorにして、Ambient Colorを明るめの色に、Environment ReflectionsのSourceもColorにする
あるいは、顔や肌だけでもToon調のシェーダー(Unity標準なら「Toon/Basic」など)を使う。
■現象2 顔に写った影が動かない
テクスチャ画像に影が書き込まれているかも?(アニメ絵的なモデルでありがち。)
髪の毛が揺れても髪の影が微動だにしないため、VR上で見ると違和感が出る。
テクスチャ画像をいじって影を消すしか無い。
(そういったテクスチャは顔だけでなく全身のテクスチャに同様の工夫がされているので完全な修正は大変ですが。)
テクスチャにある場合でも、影が別の素材として登録されている場合は、それに対応するMaterialをCutoutするか透明マテリアルに置き換えることで削除可能。
■現象3 単純に周りのUnity世界とMMDの描写が合わない。
MMD4Mecanimのシェーダは、かなり独特なので、Unity標準のシェーダで作られた世界に配置すると違和感が出る場合があります。
Standardシェーダに置き換えることで違和感が消えるかも。注意点はこちらで説明。
#スクリプトでシェーダ切り替え
先の通り、シェーダをMMD4Mecanim標準からStandardシェーダに置き換えることで良い感じに表示できる場合があります。
あらかじめStandardシェーダ化したMaterialを別途用意しておいて張り替えても良いが、アセットバンドル化した時のマニュフェストに大量のmatファイルが見えるのが嫌なので、スクリプトからパラメータを変えることで対応します。
切り替えるシェーダはEdit → Project Settings → Graphics →Always Included Shadersに登録しておかないとNullReferenceExceptionになる場合があるので注意。
(たとえアセットバンドル内のモデルにデフォルトで使われているシェーダでも、一度別のシェーダに切り替えた後に戻そうとすると駄目。ただし、最初からScene内に配置していたり、Resourcesに配置しているプレハブで使われているシェーダであれば、Always Included Shadersに登録していなくてもShader.Findで検索できるっぽい。)
private Shader Shader_std;
private Shader Shader_kai;
void Start()
{
Shader_std = Shader.Find("Standard");
Shader_kai = Shader.Find("Standard_kai"); //Standardを改造して裏面描写対応にしたもの
}
//指定のMaterialを裏面描写対応のシェーダに切り替え
private void ToBeStandardKaiShader(GameObject Target, int U_Char_Num , int[] nums)
{
SkinnedMeshRenderer smr = Target.transform.Find("U_Char/U_Char_" + U_Char_Num ).GetComponent<SkinnedMeshRenderer>();
Material[] mats = smr.materials;
foreach (int num in nums)
mats[num].shader = Shader_kai;
smr.materials = mats;
}
//Fade化による透過マテリアル設定(RenderingMode=Fade)
private void ToBeFade(GameObject Target, int U_Char_Num, int[] nums)
{
SkinnedMeshRenderer smr = Target.transform.Find("U_Char/U_Char_" + U_Char_Num).GetComponent<SkinnedMeshRenderer>();
Material[] mats = smr.materials;
foreach (int num in nums)
{
//RenderingMode以外にも色々設定しているが、設定漏れがあると透過しなかったり、逆に裏に隠れたオブジェクトまで透過させたりするので汎用的に設定するなら必須
mats[num].SetFloat("_Mode", 2);
mats[num].SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mats[num].SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mats[num].SetInt("_ZWrite", 0);
mats[num].DisableKeyword("_ALPHATEST_ON");
mats[num].EnableKeyword("_ALPHABLEND_ON");
mats[num].DisableKeyword("_ALPHAPREMULTIPLY_ON");
mats[num].renderQueue = 3000;
}
smr.materials = mats;
}
//Cutout化。Fadeと比べて利用頻度は低い。
private void ToBeCutout(GameObject Target, int U_Char_Num, int[] nums)
{
SkinnedMeshRenderer smr = Target.transform.Find("U_Char").GetComponent<SkinnedMeshRenderer>();
if (U_Char_Num != -1)
smr = Target.transform.Find("U_Char/U_Char_" + U_Char_Num).GetComponent<SkinnedMeshRenderer>();
Material[] mats = smr.materials;
foreach (int num in nums)
{
mats[num].SetFloat("_Mode", 1);
mats[num].SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
mats[num].SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
mats[num].SetInt("_ZWrite", 1);
mats[num].EnableKeyword("_ALPHATEST_ON");
mats[num].DisableKeyword("_ALPHABLEND_ON");
mats[num].DisableKeyword("_ALPHAPREMULTIPLY_ON");
mats[num].renderQueue = 2450;
}
smr.materials = mats;
}
//マテリアルのColor(Albedo)設定。
private void SetColor(GameObject Target, int U_Char_Num, int[] nums, Color? _color = null) //デフォルトにnew Colorは使えないのでNull合体演算子でデフォルトを定義
{
Color color = (_color ?? new Color(1.0f, 0.94f, 0.94f, 1.0f)); //引数省略時は薄肌色
SkinnedMeshRenderer smr = Target.transform.Find("U_Char/U_Char_" + U_Char_Num).GetComponent<SkinnedMeshRenderer>();
Material[] mats = smr.materials;
foreach (int num in nums)
mats[num].color = color;
smr.materials = mats;
}
//マテリアルのMetaric設定。引数省略だとかなりの金属感(Metaric=0.5,Smoothness=0.5)。服をちょっとテカらせるくらいなら0.2/0.2くらいを指定する
private void SetMetaric(GameObject Target, int U_Char_Num, int[] nums, float Metaric = 0.5f, float Smoothness = 0.5f)
{
SkinnedMeshRenderer smr = Target.transform.Find("U_Char/U_Char_" + U_Char_Num).GetComponent<SkinnedMeshRenderer>();
Material[] mats = smr.materials;
foreach (int num in nums)
{
mats[num].SetFloat("_Metallic", Metaric);
mats[num].SetFloat("_Glossiness", Smoothness);
}
smr.materials = mats;
}
public void ToBeStandardShader(GameObject Target) //引数のMMDモデルオブジェクトをStandardシェーダに切り替え
{
//まずは全materialのシェーダをStandardに置換
foreach (Transform U_Char in Target.transform.Find("U_Char"))
{
Material[] mats = U_Char.GetComponent<SkinnedMeshRenderer>().materials;
for (int i = 0; i < mats.Length; i++)
{
mats[i].shader = Shader_std;
}
U_Char.GetComponent<SkinnedMeshRenderer>().materials = mats;
}
if (Target.name.Contains("モデル名")) //モデルごとの固有設定
{
//U_Char0のElement1,3,4,6を裏面描写対応のStandard_kaiに変更(リボン、マント・・・など必要なもののみ)
ToBeStandardKaiShader(Target, 0, new int[] { 1,3,4,6});
//U_Char1のElement1をFadeに変更(表情用透過マテリアル)
ToBeFade(Target, 1, new int[] { 1 });
SetColor(Target, 0, new int[] { 10 }); //体を肌色に(U_Char0のElement10)
SetColor(Target, 1, new int[] { 0, 1 }); //顔,首を肌色に(U_Char1のElement0,1)
SetMetaric(Target, 0, new int[] { 4, 5, 6, 11 }); //アクセサリに金属光沢を付与(U_Char0のElement4,5,6,11)
}
}
_ZWrite(距離に応じた描写順位づけ)は、なぜかFade状態ではうまく動かなかったので0にしているが、環境によっては動作するかもしれない。正常に動作するなら、1にしても良い。(正常に動けば、複数のFadeなオブジェクトが重なった時に、距離に応じて適切に描写してくれる。)
デフォルトのシェーダも記憶しておいて相互に変換したい場合は、事前に文字列として辞書にでも保管しておく。
シェーダーを変更するとrenderQueueの情報も失われるので、シェーダー名と一緒に格納しておく。
保管したいのは「U_CharのSkinnedMeshRenderer、material番号(int)」に対応する「シェーダ名(string)、renderQueue番号(int)」の紐付けなので、本来ならstringとintを格納するクラスを作ってlist in dictで格納するべきだが、大したデータ量でも無いのでシンプルなdictで記録しておく。
public Dictionary<string, string[]> dict_defShader = new Dictionary<string, string[]>();
foreach (Transform U_Char in transform.Find("U_Char"))
{
Material[] mats = U_Char.GetComponent<SkinnedMeshRenderer>().materials;
for (int i = 0; i < mats.Length; i++)
{
//U_Char番号と、renderQueue番号はStringにして無理やり格納
dict_defShader.Add(U_Char.name + "," + i.ToString(), new string[] { mats[i].shader.name , (mats[i].renderQueue).ToString() });
}
}
if (transform.Find("U_Char/U_Char_0") == null) //モーフ登録が無いモデルでは、U_CharオブジェクトそのものにRendererが登録される。
{
Material[] mats = transform.Find("U_Char").GetComponent<SkinnedMeshRenderer>().materials;
for (int i = 0; i < mats.Length; i++)
{
dict_defShader.Add("U_Char," + i.ToString(), new string[] { mats[i].shader.name, (mats[i].renderQueue).ToString() });
}
}
元のシェーダに戻したい時は以下のように、一度SkinnedMeshRenderer.materialsをMaterial[]変数に放り込んだ後、個別に変更してからまとめてSkinnedMeshRenderer.materialsに戻す。
SkinnedMeshRenderer smr = transform.Find("U_Char").GetComponent<SkinnedMeshRenderer>(); ////モーフ登録が無いモデル
if (transform.Find("U_Char/U_Char_0") != null)
smr = transform.Find("U_Char/U_Char_0").GetComponent<SkinnedMeshRenderer>(); //デフォルトは0から。
Material[] mats = smr.materials;
foreach (string key in dict_defShader.Keys)
{
//keyは"U_Char名,mat番号"の文字列で、順番に登録されている。
string U_Char = key.Split(',')[0];
if (smr.name != U_Char)
{
smr.materials = mats; //現在のmatsをsmrに反映
smr = transform.Find("U_Char/" + U_Char).GetComponent<SkinnedMeshRenderer>(); //次のU_Charに切り替え。
mats = smr.materials; //matsも次のU_Char用に切り替え。
}
Shader shader = Shader.Find(dict_defShader[key][0]);
//別のシェーダーに置き換えたい場合は、ここにifでも入れてshaderを置換
mats[int.Parse(key.Split(',')[1])].shader = shader;
mats[int.Parse(key.Split(',')[1])].renderQueue = int.Parse(dict_defShader[key][1]);
}
smr.materials = mats; //最後のU_Charは次に切り替えることなくforeachを抜けるので、ここで反映。
なお、別項 で記載しているように、透明マテリアルを使用することでモデルの一部を透明化している場合、その部位の設定が変更されることで透明化しておいた部位が再表示されてしまう場合があります。
これを回避するために、各関数のforeach (int num in nums)内でif (!mats[num].name.Contains("InvisibleMaterial")){}を入れて、マテリアル名が透明マテリアルの場合には変更対象外にしておきます。
#口が開いた状態になるなど
■現象
大きく口を開けた時などに歯が顎から飛び出したり、口が歪んだりする。
■原因
HumanoidのJawに何かが登録されていることが原因かも。
ほとんどのMMDモデルはJawに相当するJointを用意していません。(表情はモーフで管理するので)
また、Jawが0の状態では口が半空き状態になるため、それを想定していないモーションを使うと常に口を開けた状態になってしまいます。
Headの一番下のJawの登録を解除します。
Jawに限らず、Humanoid化した際にはボーンの位置を元に自動的にマッピングが作成されますが、以下はかなりの頻度で正常に登録されないので注意が必要です。
・Hips:HipMasterが登録されているべきだが、なぜかRootが登録されていることが多い
・Toes:Footまでは自動的にマッピングされるのに、なぜかToesだけはNoneになっていることが非常に多い
・Neck:必須のボーンではないが、Noneになっていることが多い。手動でも登録しておくべき
・Eyes:そもそも用意されていないモデルが多い。用意されている場合でも目全体を動かすような誤ったボーンに自動マッピングされる場合が多い
・Jaw:存在しないモデルが多いし、存在している場合でもNoneにしておいた方が都合が良いことが多い。
#目玉の制御方法について
■現象1:目玉が動きすぎて白目になる
MMDでは、まぶたや眉毛の動き、眼球の大小、ハイライト等の制御にはモーフを使用しますが、目玉(眼球)そのものを上下左右に動かすのはモーションで制御している場合がほとんどです。
(「コッチミンナ」モーフは、実際には上下左右に動かして調整しているわけではないので例外です。)
Unityでいうと、Humanoid Avator設定のHeadにあるLeft Eye / Right Eyeの箇所に対応するjointが登録されることで制御しているのですが、モデル/モーションを汎用的に使おうとすると、かなり厄介です。
たとえば、大きな目のモデル向けに調整したモーションで「眼球を右に向ける」動作を行っていた場合、
これを目の小さなモデルに適用すると、眼球が右に動きすぎて、白目になってしまったりします。
目の大きさが同じ程度でも、目とまぶたのテクスチャの奥行き距離が近い/遠いモデル間では同じような問題が生じます。
下図の3つのモデル、すべてLeft Eye / Right EyeのlocalRotation.y=-7で同じなのですが、目の角度がずいぶん違って見えます。
(中央のモデルは眼球の初期位置が中央よりのモデルなので、右目が特に顕著に見えます。)
モーションによっては目のlocalRotation.yが+-14くらいまで変化するものもあるので、真ん中のモデルだと完全に白目になってしまいます。
IAnimationJobあたりを使って、目のサイズに合わせて、モーション再生時のLeft Eye / Right Eyeの動きに、モデル固有の倍率をかける方法であれば、モーションの内容に応じた調整も可能なので最適解と思われますが、調整が大変そう。
なので、以下のようなスクリプトで毎フレームLateUpdateで補正してごまかしました。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MMD_ReduceRotate : MonoBehaviour
{
//効果の割合。0だと効果無し。1に近づくほど、Quaternion(0.0f, 0.0f, 0.0f, 1.0f)に近い方向に補正される。
public float value = 0.0f;
private Quaternion zeroq = new Quaternion(0f, 0f, 0f, 1f);
//多用する処理ではないが、毎フレームのtransform呼び出しは重いらしいので、一応変数に格納
private Transform ttf; //this.transform
void Start()
{
ttf = this.transform;
}
void LateUpdate()
{
ttf.localRotation = Quaternion.Lerp(ttf.localRotation, zeroq , value);
}
}
目玉が大きく動きすぎるモデルは、
transform.Find("282.!Root/7.joint_HipMaster/(略)/22.joint_LeftEye").gameObject.AddComponent<MMD_ReduceRotate>().value = 0.5f
しておけば動きが半減します。
一定の割合で軽減するのではなく、一定以上の角度にならないように制限したいならこっち。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MMD_RotateLimit : MonoBehaviour
{
//曲がる上限
public float value = 90.0f;
private Quaternion zeroq = new Quaternion(0f, 0f, 0f, 1f);
//多用する処理ではないが、毎フレームのtransform呼び出しは重いらしいので、一応変数に格納
private Transform ttf; //this.transform
void Start()
{
ttf = this.transform;
}
void LateUpdate()
{
float Angle = Quaternion.Angle(zeroq, ttf.localRotation);
if (Angle > value)
ttf.localRotation = Quaternion.Lerp(ttf.localRotation, zeroq, ((Angle - value) / Angle));
}
}
■現象2:まぶたから目がはみ出す
まぶたと目のテクスチャ距離が近いモデルでは、Left Eye / Right Eyeの動きと同時に「まばたき」モーフを使うと、目がまぶたより前に出てしまう場合があります。
下図はLeft Eyeをモーションで動かないようにNoneにして、あるモーションで「まばたき」がweight=57になったときのものです。
Right Eyeを登録している右目は目のテクスチャがまぶたより前に出てしまっています。
問題回避のためにLeft EyeをNoneに変更した左目は、まぶたの描写はできていますが、目がまぶたについてきていません。
上記のケースは、目の奥行き(localPosition.z)が浅すぎために目の上下回転(localRotation.x)時にまぶたからはみ出すことが原因でした。(MMDの目は球体ではないので、過度に回転すると手前に飛び出るのですね。)
この場合は目のlocalPosition.zを-0.0062くらいまで押し込むことで、localRotation.xが8程度まで下がっても違和感が無いようになります。
下図は左目だけlocalPosition.zを-0.0062に変更して、localRotation.x=8でまばたきweight=50にした図です。
ただ、上記のケースではlocalPosition.zを-0.0062まで押し込むと、次は目を左右に動かした時、白目に隠れて瞳孔が欠けてしまう問題が発生しました。上図を見ても、左目の下の方が白目に隠れているのが分かります。
こちらのケースも、そもそもの原因はモデル製作者が想定した以上に目のlocalRotationが回転したことに起因しているので、先のMMD_ReduceRotateスクリプトで目玉が大きく動かないように調整するだけでかなり改善される。
それでも足りない場合はlocalPosition.zをすこしだけ奥に補正する。
なお、Avatorに登録されているjointのposition/rotationはモーションにより毎フレーム補正されてしまうので、localPositionを補正する場合もLateUpdateで毎フレーム補正する必要があります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MMD_FixPosition : MonoBehaviour
{
//mode:何を固定するか?
//0:localPosition.x 1:localPosition.y 2:localPosition.z
public int mode = 0;
//この値に固定する
public float value = 0.0f;
//多用する処理ではないが、毎フレームのtransform呼び出しは重いらしいので、一応変数に格納
private Transform ttf; //this.transform
void Start()
{
ttf = this.transform;
}
// Update is called once per frame
void LateUpdate()
{
if (mode == 0) { ttf.localPosition = new Vector3(value, ttf.localPosition.y, ttf.localPosition.z); }
else if (mode == 1) { ttf.localPosition = new Vector3(ttf.localPosition.x, value, ttf.localPosition.z); }
else if (mode == 2) { ttf.localPosition = new Vector3(ttf.localPosition.x, ttf.localPosition.y, value); }
}
}
こちらも、両目に対して以下のように付与すれば、目のlocalPosition.zを固定できます。
MMD_FixPosition mmdfp = transform.Find("245.!Root/9.joint_HipMaster/(略)/19.joint_LeftEye").gameObject.AddComponent<MMD_FixPosition>();
mmdfp.mode = 2;
mmdfp.value = -0.0045f; //デフォルト-0.004018848
#関節が曲がりすぎると破綻する
■現象
どこまで関節を曲げることを想定しているか?はモデルによって異なります。
モーションを汎用的使うには、モデルごと、関節ごとに個別の上限を設定する必要があります。
■対応
膝関節など回転方向がある程度決まっている関節であれば、「目玉の制御方法について」で作成した、localRotationが一定以上の角度にならないように制限するスクリプトで対応可能です。
ただし、「腕(Arm)を真上に伸ばすと破綻する」ような場合は、曲げる方向を考慮した制限が必要です。
(単純に腕を90度以上曲がらないようにしてしまうと、真上だけでなく、正面方向に曲げるのも制限されてしまう。)
クォータニオン計算で、「特定方向に一定以上曲がった場合、その特定方向の回転だけを軽減」する処理が必要なのですが、うまく制御できずに苦戦中。
#揺れものの一部が黒くなる。不自然な線が見える
■現象
下のように、揺れている衣服の一部が、別の衣服と重なったり、厚みのある衣服の一部が黒くなります。
あるいは下のように、口の中や目の周りなどに不自然な線(edge)が表示されます。
遠目に見る分にはあまり違和感が無いです、VRなどで近くで見ると違和感が強く出ます。
■原因
Edge系シェーダーでは、裏面が黒色で表示されてしまうため。
厚みのある衣装は、実際には2枚の生地を重ねてそれぞれ表側だけを描写しているような状態になっている。
しかし、物理演算で衣装が揺れることで、一時的に手前の生地より奥側の生地が表示されてしまい、奥側の生地の裏面である黒色で描写されてしまう。
■対応
MMD4Mecanimのシェーダを使っている場合は、Edge系以外のものに変えたら改善するかも。
Edge系で無いシェーダーは表面だけ描写するので、衣装が揺れても必ず表面が表示される。
たとえばもとがMMDLit-EdgeならMMD-Litに、MMDLit-Transparent-Edgeなら、MMDLit-Transparentに変えれば良い。
基本的に、シェーダ名から-Edgeを除けば良いので、こちらで作っているdict_defShaderのように、あらかじめ使用しているシェーダーのリストを作っておけば、.Contains("-Edge")で検索して一括置換できます。
ただし、edge無しのシェーダーにすることで描写が変になることもあるので、色々な角度から見てチェックが必要。
bool noedge = true; //ここをtrueにすればedge無しシェーダに、falseにすればedge有りシェーダに切り替わる。
SkinnedMeshRenderer smr = transform.Find("U_Char").GetComponent<SkinnedMeshRenderer>(); ////モーフ登録が無いモデル
if (transform.Find("U_Char/U_Char_0") != null)
smr = transform.Find("U_Char/U_Char_0").GetComponent<SkinnedMeshRenderer>(); //デフォルトは0から。
Material[] mats = smr.materials;
foreach (string key in dict_defShader.Keys)
{
//keyは"U_Char名,mat番号"の文字列で、順番に登録されている。
string U_Char = key.Split(',')[0];
if (smr.name != U_Char)
{
smr.materials = mats; //現在のmatsをsmrに反映
smr = transform.Find("U_Char/" + U_Char).GetComponent<SkinnedMeshRenderer>(); //次のU_Charに切り替え。
mats = smr.materials; //matsも次のU_Char用に切り替え。
}
Shader shader = Shader.Find(dict_defShader[key][0]);
//ここでedge無しのシェーダーに変換
if ((noedge == true) && (dict_defShader[key][0].Contains("-Edge")))
{
shader = Shader.Find(dict_defShader[key][0].Replace("-Edge", ""));
}
mats[int.Parse(key.Split(',')[1])].shader = shader;
mats[int.Parse(key.Split(',')[1])].renderQueue = int.Parse(dict_defShader[key][1]);
}
smr.materials = mats; //最後のU_Charは次に切り替えることなくforeachを抜けるので、ここで反映。
あるいはMMD4MecanimのシェーダをやめてStandardシェーダに変える対応でも、edgeは消えます。
#足がスカートを貫通する
■現象
Dynamic Boneでスカートを揺らしているが、足がスカートを貫通して見えてしまう。
■原因
原因は色々あるが、スカートを構成するjoint数が少ない場合や、丈が長い場合はとても厄介。
たとえば、6本のboneでスカートを構成している場合、各jointの間には60度の隙間があることになる。
モデルが膝を上げると、スカートのjointが足のcolliderから滑り落ちてしまう。
スカートの判定(Radius)を大きくすれば軽減できるが、大きすぎると常に捲れたような状態になってしまうので調整が難しい
■対応
破綻しやすいのは、膝を上げた時の正面方向のスカードなので、そこに絞って対策する。
LeftHip/RightHipの子供にふとももに張り付くような巨大なdynamic bone colliderを作成して、スカート正面方向のjointだけに判定されるように登録する。
下図では、わかりやすくするために、対象のcolliderを青色に着色している。
乱用すると重くなるので、突き抜けが目立つ箇所に絞って実装する必要がある。
また、dynamic bone colliderを巨大にしすぎると、radiusと実際の当たり判定にズレが出て挙動が怪しくなる。(球の当たり判定を計算する際の丸め誤差が大きくなる?モデルのlocalScaleを変えた時などに特に顕著にずれる。)
colliderのradiusは、モデルの身長が直径になる程度を上限にしておき、あとはHeightを伸ばして壁になるように調整するのが無難。
この方法は、1つなぎに描写されている長髪が不自然に首を貫通してしまうような問題の軽減にも有効。
#addcomponentで付与したmmd4mecanimanimmorphhelperが動作しない
■現象
予めモデルにMMD4MecanimAnimMorphHelperを付与しておけば動くのに、後からAddComponent()しても動かない
■原因
謎。そもそもinspector上で設定することが前提の設計なのかも?
■対応
MMD4MecanimAnimMorphHelper.csの、void _PlayAnim( Anim anim )の最初に以下を追記
MMD4MecanimAnim.InitializeAnimModel(this);
これを入れないと、animListに登録した.animデータから「_playingAnim.morphMotionList」が生成されず、これがnullのまま処理が進んでしまう様子。
本来はAwake()およびStart()から呼び出される_Initialize()内の処理であって_PlayAnim()の都度に実行する必要は無い気がする。多分、もっと良い解決策がある気がするが、そもそもの中身が詳細不明(DLL内)なので妥協。
#quest上で表情が変化しない
■現象
MMD4MecanimAnimMorphHelperでモーションと同時にモーフデータも再生したが、quest上では再生されない。(Windows上では問題ない)
■原因
なぜかquest上ではMMD4MecanimAnimMorphHelperが動かない。
よく分からないのでMMD4MecanimAnimMorphHelperの修正でごまかす。
ちなみにMMD4MecanimMorphHelper(Animじゃない方)も同様にquest上で動かない。
■対応
MMD4MecanimAnimMorphHelper.csを修正
再配布は不可なコードなので、どこを修正したかだけをメモ。
各モーフが、どのU_Char_*(のSkinnedMeshRenderer)の、何番目に格納されているか?という情報を保管するためのクラスとリストを定義
public class MorphInfo
{
public SkinnedMeshRenderer skm;
public int index;
public MorphInfo(SkinnedMeshRenderer skm, int index)
{
this.skm = skm;
this.index = index;
}
}
private Dictionary<string, MorphInfo> morphs = new Dictionary<string, MorphInfo>();
void Start()内で_Initialize();の後に追記。
全てのU_CharのSkinnedMeshRendererの、全BlendShapeの情報を格納する。
BlendShapeNameをキーにして、それがどのSkinnedMeshRendererコンポーネントの何番目にあるか?を検索できる辞書になる。
foreach (Transform U_Char in transform.Find("U_Char"))
{
SkinnedMeshRenderer skm = U_Char.gameObject.GetComponent<SkinnedMeshRenderer>();
for (int i = 0; i < skm.sharedMesh.blendShapeCount; i++)
{
morphs.Add(skm.sharedMesh.GetBlendShapeName(i), new MorphInfo(skm, i));
}
}
//morph情報が格納されていることの確認デバッグ用
foreach (KeyValuePair<string, MorphInfo> test in morphs)
Debug.Log(this.name +" " +test.Key + " " + test.Value.skm + " "+test.Value.index);
void IAnimModel._SetAnimMorphWeight( IMorph morph, float weight )の中で、morph.weight = した後に追記(モーフ変更メイン処理)
if (morphs.ContainsKey(morph.name))
morphs[morph.name].skm.SetBlendShapeWeight(morphs[morph.name].index, morph.weight * 100);
void _StopAnim()の中で、_inactiveModelMorphSet.Add( morph );した後に追記(別animに切り替えた場合や、animEnabled=falseになった際の全モーフreset処理)
現在weightが0になっていないモーフをすべて0に戻します。
これをしないと、前のanimの最後で特殊なモーフが有効になっていて、次のanimでそれを使っていない場合、前のモーフを残したまま次のanimが再生されて表情が壊れたりします。
if (morphs.ContainsKey(morph.name))
morphs[morph.name].skm.SetBlendShapeWeight(morphs[morph.name].index, 0);
#一部のモーフが動作しない
先の処理を行っても、一部のモーフだけ動作しない場合
■原因
そのモーフがMMD4 Mecanim ModelのMorphタブには存在するのに、U_Char_*にBlendShapesとして登録されていない場合、グループモーフの可能性がある。
グループモーフとは、PmxEditor上でモーフを見るとGとなっているもの。「笑い」や「まばたき」のような両目のモーフが「ウィンク(左)」「ウィンク右」をあわせることで表現されているなど。
グループモーフもMMD4Mecanimで取り込むことは可能だが、U_Char_*に登録されないので、先のMMD4MecanimAnimMorphHelper改修では対応できない。
そもそも「笑い:100」「ウィンク:0」の譜面があると「ウィンク」の方が優先されて、実質的に「笑い」が無効化されるような問題も生じたりする。
そういった譜面が無いようにモーフを調整すれば良い話ではあるが、Unity上で汎用的に使うのにグループモーフに頼るのはあまり望ましくない気がする。
■対応
Unity上のスクリプトだけで対応するのは煩雑すぎるので、モデルのモーフデータを改修するのが無難。
PMXエディッタのモーフ編集で、グループ元となるモーフ「ウィンク」「ウィンク右」を両方選択して、ctrl+c(コピー)。
メモ帳に貼り付けて、「ウィンク」と「ウィンク右」を「笑い」に置換。途中に不要なヘッダ行が混ざるので消しておく。
これで、名前が「笑い」で、「ウィンク」と「ウィンク右」の両方のデータを結合したものになるので、これをコピーしてPMXエディッタのモーフ編集画面にctrl+Vする。
「笑い」モーフが、メモ帳にコピーしておいたデータに置き換えられる。
#中途半端なweightのモーフが破綻する
一部のモーフで、weightを50のような中途半端な値にすると表情が変になるする。
■原因
モデルがそのようなweightを想定していない。
たとえば「なごみ」モーフは、モデルによって「本来の目を細める」手法と、「本来の目を消して、代わりに専用のテクスチャを表示する」手法の2種類の実装がある。
前者の実装であれば、モーフの重みを上げると少しずつ目を細めるような動きになるので問題ないが、そのモーフデータを後者の手法で実装しているモデルに採用すると、本来の目が消える途中の中途半端な状態が見えてしまう。
■対応
モデルかモーションのいずれかを修正すれば良い話ではあるが、色々なモデル・モーションを手軽に使いたいのでスクリプトで解決する。
以下のようなコンポーネントを用意して
public class FixTheMorph : MonoBehaviour
{
public int morphnum = 0; //固定するモーフの番号(Skinned Mesh Rendererのに登録されているBlendShapesのindex番号)
public int mode = -1;
//mode=-1 : 常に0固定
//mode=0 : 常に1固定
//mode=1~99 : 指定値未満なら0、指定地以上なら100に固定
private SkinnedMeshRenderer skm;
void Start()
{
skm = this.GetComponent<SkinnedMeshRenderer>();
}
// 通常のモーフ操作より優先させるためLateUpdateで操作
void LateUpdate()
{
if (mode == -1 ) { skm.SetBlendShapeWeight(morphnum, 0); } //0固定
else if (mode == 0) { skm.SetBlendShapeWeight(morphnum, 100); } //100固定
else
{ //現在値がmodeのint値未満なら0、以上なら100に固定
if (skm.GetBlendShapeWeight(morphnum) < mode) { skm.SetBlendShapeWeight(morphnum, 0); }
else { skm.SetBlendShapeWeight(morphnum, 100); }
}
}
}
以下のように、モデルのU_Char*に貼り付けることで、問題となるモーフを固定する。
//U_Char_1のBlendShapesのindex34のモーフは、weight90未満なら0、90以上なら100に固定
FixTheMorph FTM = transform.Find("U_Char/U_Char_1").gameObject.AddComponent<FixTheMorph>();
FTM.morphnum = 34;
FTM.mode = 90;
モーフのweightを固定するだけの仕組みなので、modeを-1や0に変化させることで、モーフのweight操作で脱着可能なオプションをON/OFFするような機能にも利用可能。
#quest上で物理演算が動かない
■原因
多分、Android用のBulletPhysicsはリアルタイム物理演算に対応していない?
(MMD4Mecanimのpluginsの中にMMD4MecanimBulletPhysics/Android/libsとあるが、これを使ったリアルタイム物理演算についていまいち情報が見つからなかった。解析はNGだと思うので諦める。)
こちらについても、いつの間にかquest上でもリアルタイムで物理演算するようになっていた。
MMD4Mecanimのバージョンは変えていないので、Unityバージョン変更か、新しいUnityバージョンでプロジェクトを作り直したことが原因だろうか。
ただ、BulletPhysicsを使うとモデルのサイズを変更した時などにバグるので、Unity上で汎用的に使うならBullethysics以外の方法を使うのはアリかもしれない。
■対応
Unity標準のPhysXやPhysicsに置き換えるか、Dynamic Bone(有名な有料アセット)あたりを使って回避しました。
Dynamic Boneの使い方は詳しく紹介しているサイトが多くあるので割愛。
各所で補助ツールが公開されているので、併用すれば楽に調整できます。
参考:https://gist.github.com/gamebox777/0c993078f0e34d608a7d6ec165268009
Dynamic BoneのDynamic Bone Colliderを付ける際、Bullet Physicsの「Generate Colliders」で付与されるcollider(Coll.~)オブジェクトを参考に付けると楽です。
「Coll.~」の名前を「DB.~」に変えてからDynamic Bone Colliderを付与。Capsule ColliderのRadius/Height/Directionを、そのままDynamic Bone Colliderの同名のパラメータに変えれば良い。もとのCapsule Colliderコンポーネントは削除。
Dynamic Bone ColliderではBox Colliderを再現できないので、そこは自力で調整する必要あり。
最後にBullet Physics用の不要なcolliderを消すために「Remove Colliders」するのを忘れずに。
名前が「Coll.~」のものを消す仕組みらしく、「DB.~」にリネームしたものが消されることはありません。
colliderの数は負荷に影響するので、揺れものとの判定を考えて必要なものだけを付けた方が良さそう。たとえば短髪の髪の毛に対して、足の当たり判定を付けるのは無駄なので。
#モデルのサイズや頭身を変える
UnityではlocalScaleを変えることでオブジェクトのサイズを自由に変えることができますが、MMDモデル全体のlocalScaleをVector3(1.0f, 0.9f, 1.0f)、首から上をVector3(1.1f, 1.1f, 1.1f)などにすることで、モデル本体データを改変することなく、ちびキャラ風(太め&等身低い)に表示することも可能です。
微妙な差ですが、上(デフォルト比率)と下(太め&等身低い)で印象が変わります。
首のtransformを汎用的に取得する方法は「モデルの頭や手足のオブジェクトを探して変数に保管」で作った辞書を利用します。
本体の縦横比の方は、あくまでアスペクト比を変えて見た目の印象を操作しているだけなので、Vector3(1.0f, 0.6f, 1.0f)みたいな極端な値を入れては駄目です。手を上げると太く短いのに、手を横に伸ばすと細長く変形するようなことになります。
dynamic boneでつけたコライダーも一緒に縮小されるので、適切な部位に付与していれば、サイズ変更後もちゃんと動くはずです。
注意点としてBullet Physicsを切っておく必要があります。
Bullet Physicsの挙動はサイズ1.0fで固定されており、モデルのサイズ(localScale)を変えると揺れものの挙動が不安定になります。
モデルのサイズを変えたい場合はBullet Physicsは諦めて、DynamicBoneなどに切り替えます。
#モデルのサイズを変えるとラグドール時の挙動がバグる
■現象
モデルのサイズ(localScale)を変えた状態でラグドール化すると、関節の動きが変になる。
たとえばモデルのScaleが0.5倍になったのに、ラグドール時に胴体(Torso)と腕(Arm)との距離が変わっておらず、相対的に伸びて見えたりする。
■原因
Unityのjointが記録しているアンカー情報は親オブジェクトのサイズが変わっても自動的に更新してくれない。
(localScaleしか見ておらず、lossyScaleの変化を反映してくれない。)
■対応
jointを格納する変数を用意して
private List<Joint> joints = new List<Joint>();
Start()の時にでも全jointを格納しておく。
autoConfigureConnectedAnchorが有効だと修正できないので、ついでに無効化しておく。
foreach (var t in GetComponentsInChildren<Joint>())
{
joints.Add(t);
t.autoConfigureConnectedAnchor = false;
}
モデル自身、またはその親オブジェクトのScaleが変わったタイミングで、すべてのjointのAnchor情報を入れ直す。
値を変えてないので意味がない処理のように見えるが、これらの変数が更新されると、jointのAnchor座標が現在のscaleに応じた相対位置に再計算されるっぽい。
foreach (Joint joint in joints)
{
joint.connectedAnchor = joint.connectedAnchor;
joint.anchor = joint.anchor;
}
#モデルの頭や手足のオブジェクトを探して変数に保管
■現象
モーションデータではなく、モデルの動きをスクリプトで細かく制御する場合、モデルの各部位のオブジェクトを変数に保管しておきたい。
たとえばFinal IKなどのアセットで制御する場合や、特定の部位装飾品を固定するような場合に便利。
Animator制御とragdoll制御を切り替える場合、各部位に付与されたRigidbodyやColliderをON/OFFする必要があるので、そういった場合にも必要になる。
しかし各部位のオブジェクト名や構造はモデルによって異なる上、モデルを修正すると変わる(数字が連番で振り直される)。
■対応1 Humanoidを構成する部位の保管
モデルを構成するオブジェクトのオブジェクト名とTransformの辞書を再帰的に作成する関数
private void GetChildDict(Transform target , ref Dictionary<string,Transform> dict)
{
if (!dict.ContainsKey(target.name)) { dict.Add(target.name, target); }
foreach(Transform child in target)
{
GetChildDict(child,ref dict);
}
}
Animatorに登録されているavatorのhumanDescription.humanリストから、Humanoidの各部位に対応するオブジェクト名を取得、
そのオブジェクト名を↑で生成したリストから探して辞書化する。
辞書のキーはUnityのHumanoid上の名称(Hips/Spine等)ではなく、MMDモデルで汎用的に使われている名前(HipMaster/Torso等)にする。そっちの方がオブジェクト名と一致してわかりやすいので。
Dictionary<string,Transform> partsdict = new Dictionary<string,Transform>();
GetChildDict(this.transform, ref partsdict);
foreach (HumanBone bone in GetComponent<Animator>().avatar.humanDescription.human)
{
if ( bone.humanName == "Hips" ) {mmdparts.Add("HipMaster",partsdict[bone.boneName]);}
else if (bone.humanName == "Spine") { mmdparts.Add("Torso", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftUpperLeg") { mmdparts.Add("LeftHip", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftLowerLeg") { mmdparts.Add("LeftKnee", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftFoot") { mmdparts.Add("LeftFoot", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftToes") { mmdparts.Add("LeftToes", partsdict[bone.boneName]); }
else if (bone.humanName == "RightUpperLeg") { mmdparts.Add("RightHip", partsdict[bone.boneName]); }
else if (bone.humanName == "RightLowerLeg") { mmdparts.Add("RightKnee", partsdict[bone.boneName]); }
else if (bone.humanName == "RightFoot") { mmdparts.Add("RightFoot", partsdict[bone.boneName]); }
else if (bone.humanName == "RightToes") { mmdparts.Add("RightToes", partsdict[bone.boneName]); }
else if (bone.humanName == "Neck") { mmdparts.Add("Neck", partsdict[bone.boneName]); }
else if (bone.humanName == "Head") { mmdparts.Add("Head", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftShoulder") { mmdparts.Add("LeftShoulder", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftUpperArm") { mmdparts.Add("LeftArm", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftLowerArm") { mmdparts.Add("LeftElbow", partsdict[bone.boneName]); }
else if (bone.humanName == "LeftHand") { mmdparts.Add("LeftWrist", partsdict[bone.boneName]); }
else if (bone.humanName == "RightShoulder") { mmdparts.Add("RightShoulder", partsdict[bone.boneName]); }
else if (bone.humanName == "RightUpperArm") { mmdparts.Add("RightArm", partsdict[bone.boneName]); }
else if (bone.humanName == "RightLowerArm") { mmdparts.Add("RightElbow", partsdict[bone.boneName]); }
else if (bone.humanName == "RightHand") { mmdparts.Add("RightWrist", partsdict[bone.boneName]); }
}
foreach (KeyValuePair<string,Transform> test in mmdparts)
{
Debug.Log(test.Key + " " + test.Value);
}
生成されたmmdparts辞書からKey:HipMasterを参照すればHipMasterのTransformが得られるようになる。
全transformの辞書作成、再帰的に回すよりもGetComponentsInChildren()してforeachで回した方が早かったりするかも?
もっとスマートな方法がありそうだが、avatar.humanDescriptionのプロパティであるhumanやskeletonを調べても、部位のtransformを直接取得できそうな方法が見つからず、全部位と名前一致チェックするゴリ押しになってしまった。
■対応2 Humanoidの構成に使用されていない部位の保管
Humanoidに登録されていない部位(アクセサリなど)を探す場合。
単純に全オブジェクトから再帰的に名前を探すと重複する場合もあるので、1階層ごとに名前を指定して潜っていくようにする。
自身の子オブジェクトに含まれる名前をcontainsで探す関数を用意。
private Transform GetChildByContain(Transform obj, string targetstr)
{
foreach (Transform childobj in obj)
{
if (childobj.name.Contains(targetstr))
return childobj;
}
return null; //指定の文字列のオブジェクトが見つからなければnullを返す
}
これを使って汎用的な名前をヒントに潜っていく。
たとえば"301.!Root/202.joint_HipMaster/203.joint_LeftHip/204.joint_LeftKnee/xxx.XXXXX"の場合、文字列「Root」を含む子供を探して、その中に「HipMaster」を含む子供を、その中に「LeftHip」を含む、、のように潜っていけば、モデルの改修で数字部分が変わっても見つけることができるはず。
Transform TargetObj = transform;
Transform TargetObj_tmp = transform;
TargetObj = GetChildByContain(TargetObj, "Root");
TargetObj = GetChildByContain(TargetObj, "HipMaster");
TargetObj = GetChildByContain(TargetObj, "LeftHip");
TargetObj = GetChildByContain(TargetObj, "LeftKnee");
TargetObj = GetChildByContain(TargetObj, "XXXXX"); //最終的にLeftKnee配下の「XXXXX」のtransformが取得できる。
多用するなら、引数に任意の数の文字列を与えて一発で探すするようにした方が美しいかな?
#dボーンのあるモデルがうまく動かない
■現象
Dボーンのあるモデルを変換すると、足先が伸びたり、ふとももが変な角度にねじ曲がったりする。
Humanoid設定のMuscles & Settings画面を確認すると、本来はこんなふうになるところ、
■原因
HumanoidはUnityの仕組みなので、MMDのDボーンに対応していない。
■対応
Humanoidの登録ボーンをDボーンの方にするだけで解決する場合もあるが、解決しない場合は構造を変えて工夫。
Dボーンを通常ボーンの子供になるようにjointの親子関係を変えれば違和感はほぼ無くなる。
localPostionはVector3.zeroにしておけば大体は問題ないが、実際の動きを見て調整する。
Transform Hip_L_Master = this.transform.Find("301.!Root/202.joint_HipMaster/203.joint_LeftHip");
Transform Hip_L_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/215.!joint_LeftHipD");
Transform Knee_L_Master = this.transform.Find("301.!Root/202.joint_HipMaster/203.joint_LeftHip/204.joint_LeftKnee");
Transform Knee_L_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/215.!joint_LeftHipD/216.!joint_LeftKneeD");
Transform Foot_L_Master = this.transform.Find("301.!Root/202.joint_HipMaster/203.joint_LeftHip/204.joint_LeftKnee/206.joint_LeftFoot");
Transform Foot_L_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/215.!joint_LeftHipD/216.!joint_LeftKneeD/217.!joint_LeftFootD");
Transform Toes_L_Master = this.transform.Find("301.!Root/202.joint_HipMaster/203.joint_LeftHip/204.joint_LeftKnee/206.joint_LeftFoot/207.!joint_LeftToe");
Transform Toes_L_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/215.!joint_LeftHipD/216.!joint_LeftKneeD/217.!joint_LeftFootD/218.joint_hidariashikubiEX");
Transform Hip_R_Master = this.transform.Find("301.!Root/202.joint_HipMaster/209.joint_RightHip");
Transform Hip_R_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/219.!joint_RightHipD");
Transform Knee_R_Master = this.transform.Find("301.!Root/202.joint_HipMaster/209.joint_RightHip/210.joint_RightKnee");
Transform Knee_R_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/219.!joint_RightHipD/220.!joint_RightKneeD");
Transform Foot_R_Master = this.transform.Find("301.!Root/202.joint_HipMaster/209.joint_RightHip/210.joint_RightKnee/212.joint_RightFoot");
Transform Foot_R_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/219.!joint_RightHipD/220.!joint_RightKneeD/221.!joint_RightFootD");
Transform Toes_R_Master = this.transform.Find("301.!Root/202.joint_HipMaster/209.joint_RightHip/210.joint_RightKnee/212.joint_RightFoot/213.!joint_RightToe");
Transform Toes_R_Sub = this.transform.Find("301.!Root/202.joint_HipMaster/219.!joint_RightHipD/220.!joint_RightKneeD/221.!joint_RightFootD/222.joint_migiashikubiEX");
Hip_L_Sub.SetParent(Hip_L_Master, true);
Hip_L_Sub.localPosition = Vector3.zero;
Knee_L_Sub.SetParent(Knee_L_Master, true);
Knee_L_Sub.localPosition = Vector3.zero;
Foot_L_Sub.SetParent(Foot_L_Master, true);
Foot_L_Sub.localPosition = Vector3.zero;
Toes_L_Sub.SetParent(Toes_L_Master, true);
Hip_R_Sub.SetParent(Hip_R_Master, true);
Hip_R_Sub.localPosition = Vector3.zero;
Knee_R_Sub.SetParent(Knee_R_Master, true);
Knee_R_Sub.localPosition = Vector3.zero;
Foot_R_Sub.SetParent(Foot_R_Master, true);
Foot_R_Sub.localPosition = Vector3.zero;
Toes_R_Sub.SetParent(Toes_R_Master, true);
複数のDボーンが使われている場合、すべて同じように修正する。腕のDボーンも同様に修正する。
※こういったボーンは、joint名の末尾に、D、D2、DS、EX、sub、C、Sといった文字がついていることが多いので、それを参考に探す。
※腕にIKを仕込んでいたりする場合、jointではなくIKの方をjointにあわせる必要があるので注意。Unity humanoidで腕IKは無理。
Dボーンの名前は統一性がなく、モデル作者によってバラバラなので個別に確認が必要。
どれが問題のjointか分からない時は、とりあえずHierarchy上に放り込んで、jointの親子関係いじりながら関節の角度を変更して試していれば、数分で確認できると思う。
#モデルの一部を非表示にしたい
■やりたいこと
装飾具がついているモデルの一部を消したい。(帽子、メガネ、艦娘の武装など)
最初から消したものを用意しておくのではなく、任意のタイミングで消したい。
■対応1 モーフで消せる場合
モーフで消せる場合は、それで対応します。
該当のモーフが含まれるU_Char_*を探して、それにSetBlendShapeWeight(モーフ番号, 100)すればOKです。
こちらに貼っているコンポーネントのようにLateUpdateで固定しておけば、MorphHelperによる変更を無視して強制できます。
ただし部位を消すモーフには2種類あり、
・頂点操作で部位を縮小することで消すタイプ
後者の場合、MMD4Mecanimシェーダでは、Diffuseに設定されているcolorの透過値を上げることで透明化するような実装になる。
U_Char上のSkinnedMeshRendererに登録されないため通常モーフと同じようには扱えず、そもそもStandardシェーダでは全く動かないので、モデルデータに手を加えるか、別の手法を使う必要がある。
■対応2 消したい部位が特定の材質全体の場合
特定の材質(material)を透明化すれば良いだけ。
本当にその材質すべてを消して良いのか、PmxEditorで確認しておきます。(髪飾りだけを消したつもりが、服の装飾も同じ材質で設定されていて同時に消えてしまうといったことがあり得ます。)
PmxEditorの「絞」から「頂点/材質」→「材質」「パーツ毎」を使って、対象の材質のみをチェック、消したいパーツ以外が画面に表示されていないことを確認すればOK。
手順:
最初に透明なマテリアルを用意(Create→Materialで新規作成し、Rendering ModeをCutoutに、Albedoの色でA値を0にすればOK)
透明マテリアルをinvisibleMat変数に入れておいて、それを部位に適用する関数を用意
private void Invisible(SkinnedMeshRenderer smr, int num)
{
Material[] mats = smr.materials;
mats[num] = invisibleMat;
smr.materials = mats;
}
あとは消したい部位がU_Charの何番目に入っているかを確認し、そこを指定する。
Invisible(transform.Find("U_Char/U_Char_0").GetComponent<SkinnedMeshRenderer>(), 1);
該当のマテリアルにDynamic Boneの設定なども入っている場合は、違和感出ないようにDestroyします。
Destroyは非可逆なので、後で戻す可能性があるならSetActive(false)にしつつ、transform.localScale = Vector3.zeroします。
(非アクティブにしても付与されているdynamic bone colliderの影響は消えないようなので、localScaleをゼロにすることで可能な限り影響しないようにします。)
private void DestroyIfExist(GameObject Target,string path)
{
if (Target.transform.Find(path) != null)
GameObject.Destroy(Target.transform.Find(path).gameObject);
//else
// Debug.Log(path + " not found");
}
DestroyIfExist(this, "138.!Root/117.joint_HipMaster/3.joint_Torso/9.joint_kazari");
■対応3 同じマテリアルで作成された部位のうち、一部だけを消す。
モデルデータを改変せずに実現するのは困難と思うので諦める。
問題の材質をFadeにして、消したい部位を優先度の高い透明Fade材質で覆い、且つ背景の描写優先度をより高くすることで光学迷彩みたいな隠し方もできますが、シーン全体のmaterialに工夫が必要になるので色々と弊害が出る。(Fadeで半透明化したいものは色々あるのに、それらの描写にも影響が、、、)
#特殊な姿勢から自然に次のモーションを開始したい
■やりたいこと
あるアニメーションを中断して別のアニメーションに繋げる時、AnimationMixerPlayableを使うことでスムーズに切り替えができます。
しかし、この手法は、単に関節等の角度を前のモーションの値から次のモーションの値に切り替えるだけの仕組みなので、次のような問題が生じます。
・転んでいる状態、寝ている状態、逆立ち状態などから繋げると不自然になる
大抵のアニメーションは、直立に近い状態で開始しますが、開始前の状態が転んでいる状態、寝ている状態などの場合、切り替えが不自然になります。(寝ている状態から地面に手をつけることもなく起き上がったりするなど)
・前のモーションがanimationclipで無い場合に使えない
AnimationMixerPlayableは、複数のanimationclipを混ぜながら切り替える仕組みなので、直前のポーズがanimationclipで無い場合は使えません。
たとえばラグドール状態だったり、animationclip以外の手段で関節を制御していた場合などです。
このような場合に、自然に次のモーションを開始する方法を探しました。
■やったこと
無料アセットRagdoll and Transition to Mecanimを参考にしました。
内容はラグドールで転んでいる状態から自然に次のモーションに繋げるものですが、色々と応用が効く手法が使われています。
アセット内で使われている手法を簡単に説明すると
①直立状態に移行するためのモーションデータを複数用意しておく
このモーションでは即座に直立に移行するのではなく、前半をすこしゆっくりと動くように作っておきます。
アセット内には、「うつ伏せ状態からの起き上がり」「仰向け状態からの起き上がり」の2種類のモーションがありました。
②転んでいるモデルに対して「起き上がり」処理を開始したタイミングで、そのモデルの全transformのlocalPosition/localRotationの値を保管しておく。
③用意しておいた①のモーションのうち、現在の姿勢に最も近いものを選定する。
アセット内では、LeftHip,RightHip,HipMasterの3つのグローバルpositionの関係から現在のモデルの姿勢(というか傾き)を計算し、仰向けorうつ伏せを判定していました。
④選定されたモーションを開始する。
⑤モーション再生の前半部分の間、LateUpdate内で、現在の(モーションによる)姿勢と、②で保管した姿勢との間で少しずつ切り替える。
アセット内ではlocalPosition/localRotationのそれぞれに対して.Slerpを使用することでスムーズに切り替えていました。
実際のコードの主な部分は以下です。
ラグドールからアニメーションに切り替える場合は、ラグドール用のRigidbodyおよびcolliderを格納するための変数を用意
private List<Rigidbody> rigidbodies = new List<Rigidbody>();
private List<CapsuleCollider> capcolliders = new List<CapsuleCollider>();
private List<BoxCollider> boxcolliders = new List<BoxCollider>();
private List<SphereCollider> spherecolliders = new List<SphereCollider>();
Start()内で上記変数にRigidbodyおよびcolliderを登録します。
モデル生成直後はragdoll化させずに動かす場合は、これらを無効化しておきます。
GetComponentsInChildrenは、再帰的にすべての子、孫オブジェクトに付与されたcolliderを拾ってきますので、ragdoll以外の目的でcolliderを付与している場合は、それらを除外するための一工夫が必要です。
foreach (var t in GetComponentsInChildren<Rigidbody>())
{
rigidbodies.Add(t);
t.isKinematic = true;
}
foreach (var t in GetComponentsInChildren<CapsuleCollider>())
{
capcolliders.Add(t);
t.enabled = false;
}
foreach (var t in GetComponentsInChildren<BoxCollider>())
{
boxcolliders.Add(t);
t.enabled = false;
}
foreach (var t in GetComponentsInChildren<SphereCollider>())
{
spherecolliders.Add(t);
t.enabled = false;
}
transformを保管しておくためのクラス、およびそれを格納するListを用意します。
起き上がる前のlocalPosition/localRotationの格納(Stored**)だけでなく、Transform自身や、再生中の値(Priv**)も一緒に格納できるようなクラスになっています。
class TransformComponent
{
public Transform Transform;
public Quaternion PrivRotation;
public Quaternion StoredRotation;
public Vector3 PrivPosition;
public Vector3 StoredPosition;
public TransformComponent(Transform t)
{
Transform = t;
}
}
private List<TransformComponent> _transforms = new List<TransformComponent>();
Start()内で上記_transformsの内容を生成します。
アセットではGetComponentsInChildren()で、モデルを構成する全オブジェクトのTransformを拾ってきていましたが、MMDは衣装や揺れものなどの付属物が多いので、モーションに関わる部位のみを登録します。
「モデルの頭や手足のオブジェクトを探して変数に保管」で作成したmmdpartsに、MMDで一般的に使われている、全関節(指以外)のtransformが格納されているので、これを使用します。
foreach (KeyValuePair<string, Transform> part in mmdparts)
_transforms.Add(new TransformComponent(part.Value));
LateUpdate内での処理を開始するためのフラグとなる変数を定義しておきます。
private float motionBlendTimer = 9999f;
ここまでが準備です。
ragdoll化する場合は、rigidbodyとcolliderを有効化し、Animatorを無効化すればOKです。
foreach (var t in rigidbodies)
t.isKinematic = false;
foreach (var t in capcolliders)
t.enabled = true;
foreach (var t in boxcolliders)
t.enabled = true;
foreach (var t in spherecollide
this.GetComponent<Animator>().enabled = false;
同時にモーフの再生も止めて手動制御する場合の処理(ragdoll化中は目を閉じさせるなど)
GetComponent<MMD4MecanimAnimMorphHelper>().StopAnim();
GetComponent<MMD4MecanimAnimMorphHelper>().animEnabled = false;```
だけだとモーフ再生はすぐには止まらないので、何フレームかの間、LateUpdateでモーフを0に固定する仕組みが別途必要です。
ragdoll状態を解除して起き上がる場合は、逆にrigidbodyとcolliderを無効化し、Animatorを有効化します。
ただし起き上がりモーションを非同期でファイルから読み込む仕組みの場合は、Animatorを有効化するのは実際にモーションを開始するタイミングにしたほうが良いです。(そうしないとAnimator無効化前のモーションが一瞬再生されてしまう。)
foreach (var t in rigidbodies)
t.isKinematic = true;
foreach (var t in capcolliders)
t.enabled = false;
foreach (var t in boxcolliders)
t.enabled = false;
foreach (var t in spherecolliders)
t.enabled = false;
//this.GetComponent<Animator>().enabled = true; //起き上がりモーションは非同期でロードするのでここでは有効化しない
それに続いて、LeftHip、RightHip、HipMasterのいち関係から現在の姿勢を推測し、用意した「直立状態に移行するモーション」のうち、現在の状態からの移行に最適なものを選出、再生します。
以下のBundleMotionStart()とあるものは、指定のモーション名をアセットバンドルから読み込んで再生する仕組みですが、本件から外れるので割愛。Animatorはこの中で有効化します。
先のアセット内にある2つのモーションは「仰向け」「うつ伏せ」から復帰する2種だけですが、色々な姿勢からの復帰モーションを用意して最適なものを選定できれば、柔軟にカスタマイズできます。
Vector3 left = mmdparts["LeftHip"].position;
Vector3 right = mmdparts["RightHip"].position;
Vector3 hipsPos = mmdparts["HipMaster"].position;
left -= hipsPos;
left.y = 0f;
right -= hipsPos;
right.y = 0f;
var result = Quaternion.FromToRotation(left, Vector3.right) * right;
//Debug.Log("判定結果:" + (result.z < 0f)); //仰向けに近いならtrue、うつ伏せに近いならfalseになる。
if (result.z < 0f)
BundleMotionStart("standing_up_from_back",init:true); //仰向けからの起き上がりモーション開始
else
BundleMotionStart("standing_up_from_belly", init:true); //うつ伏せからの起き上がりモーション開始
ここで注意ですが、ラグドールのように体の各部位が個別に動いている間、モデル本体の座標は固定されたままになります。
(HipMaster以下のオブジェクトがrigidbodyにより自由に動いている状態。)
モーションデータによるAnimation制御を本体座標を基準にしている場合、ラグドールから復帰したタイミングで、モデルの位置がラグドール化した時点の座標まで引き戻されることになるため、座標の補正が必要です。
どのように補正するかは、モデル本体座標やモーションデータのRoot Transform Positionをどのように扱っているか次第です。
続いて、現在の各関節部位のlocalPosition/localRotationを用意しておいたリストに格納します。
と同時にmotionBlendTimer変数を0.0fにしてLateUpdate内での処理を開始します。
foreach (TransformComponent trComp in _transforms)
{
trComp.StoredRotation = trComp.Transform.localRotation;
trComp.PrivRotation = trComp.Transform.localRotation;
trComp.StoredPosition = trComp.Transform.localPosition;
trComp.PrivPosition = trComp.Transform.localPosition;
}
motionBlendTimer = 0.0f; //起き上がりモーションの開始
LateUpdate内に以下の処理を入れて、motionBlendTimer変数が0.0fから1.0fになるまでの1秒間かけて、起き上がり用のモーションに切り替えていきます。
移行時間の調整次第では、1つのモーションをいろいろな状態からの復帰につかえます。
たとえば仰向け状態からの復帰モーションの1.0f秒時点が、上半身を起こした状態に調整しておけば、完全に倒れてはいない状態から移行した場合でも良い感じに動いてくれます。
(たとえば壁に背中をつけて座っている姿勢など。移行時間が短いと一度仰向けに倒れてから起き上がる動作になりますが、1.0fかけて移行するように調整しておけば、起き上がりのモーションと開始時のポーズとの間で良い感じにブレンドされて、自然に立ち上がります。)
if (motionBlendTimer < 1.0f)
{
motionBlendTimer += Time.deltaTime;
if (motionBlendTimer > 1.0f) { motionBlendTimer = 1.0f; }
foreach (TransformComponent trComp in _transforms)
{
//rotation is interpolated for all body parts
if (trComp.PrivRotation != trComp.Transform.localRotation)
{
//trComp.PrivRotation = Quaternion.Slerp(trComp.Transform.localRotation, trComp.StoredRotation, (motionBlendTimer));
trComp.PrivRotation = Quaternion.Slerp(trComp.StoredRotation, trComp.Transform.localRotation, (motionBlendTimer));
trComp.Transform.localRotation = trComp.PrivRotation;
}
//position is interpolated for all body parts
if (trComp.PrivPosition != trComp.Transform.localPosition)
{
//trComp.PrivPosition = Vector3.Slerp(trComp.Transform.localPosition, trComp.StoredPosition, (motionBlendTimer) );
trComp.PrivPosition = Vector3.Slerp(trComp.StoredPosition, trComp.Transform.localPosition, (motionBlendTimer) );
trComp.Transform.localPosition = trComp.PrivPosition;
}
}
}
なお、先のアセット内で使われている起き上がりモーション2種ですが、Standing_up_from_backの方はデフォルトだと起き上がり途中で足と尻の両方が地面から浮きます。
途中で浮かないようにRoot Transform Position (Y)のOffsetを調整すると、次はモーションの終盤で足が地面にめり込みます。
つま先位置が地面にくるようなスクリプト調整は可能ですが、おそらく一般的な手足長のモデルすべてで同じ状況なのでモーション自体を改善したほうが良いですね。
#変換後にモーションが正常に動かない
■現象
MMD上では問題ないのに、Unityに取り込んだ後にモーションが崩れる(足首が捻れたり、変な方向に伸びていたり、膝が裏返ったりなど)
■原因1
IK無効設定がUnity上に取り込まれないことが原因かも?
MMDでは、足首の向きを細かく調整するためにIKを使用します。
モーションによって、このIKを細かく調整しているものや、逆にIKを無効にして、関節の角度だけで調整しているものもあります。
具体的には「モデル操作」のIK設定でOFF/ONを登録し、「表示・IK・外観」のところにIKの無効/有効設定が入っているので、ここを見ます。
「足を高く蹴り上げる」などの極端なモーションの箇所だけIKを無効化しているものも見かけます。
(そういった極端な動きは、特定のモデル用に細かくIKを調整すると、別のモデルでは余計に足首が捩れたりするので。)
Unityにモーションを取り込む差異、この「IK無効化」設定が正常にせず、「IKが有効化された状態の動き」を元に変換されることがあるようです。
IK無効化についてはいまいち良くわかりませんが、
・最初のframeで無効化しても駄目
1st PLACEのIA公式が出しているもの(higher等)などは、最初の0frameでIKを無効化し、モーション全体を通して無効のままです。
このまま変換すると壊れますが、少し後(1frame目など)に、もう1箇所IK無効設定を入れておくと、正常に変換されたりします。
・部分的に無効化しているものは駄目?
基本的にはIK有効で、一部だけIKを無効化しているものはうまく変換できなかったりします。
ただ、昔のUnity/MMD4Mecanimバージョンで変換したものは今も正常に動作していたりするので、何か条件があるのかも?
■原因1の対応
とりあえず、全体を通してIK無効にするものは、0frame目だけでなく1frame目にもIK無効設定を入れることで対処します。
部分的に無効化する必要のあるものは、いっそIK無効化設定を解除した状態でも正常に見えるように、IKを調整してモーションを修正します。
(特定モデル用に調整するので、別のモデルで再生すると壊れますが無視。)
MMD4MecanimでUnity用に取り込む際、編集に使用したMMDモデルを使って変換すればおそらくOKです。
MMDでは、特定のモデルでないと正常に動作しないのに、Unity上では、他のモデルでもきちんと再生されることが多いです。
■原因1の対応に関する考察:MMDとUnityでIKの仕様が異なる?
MMDとUnityではIKの仕様が異なるらしく、MMD上では特定のモデルだけでしかきれいに動かないように調整されていても、Unity上では他のモデルでも大丈夫だったりします。
UnityのIKの仕様をいまいち理解できてないですが、おそらくMMDとは異なり、IKは、モデルの各部位の長さに応じて座標が相対的に処理されているように見えます。
MMDでは、足の長いモデルで調整したモーションを足の短いモデルで再生すると足が宙に浮いたり、逆に足の短いモデルで調整したモーションを足の長いモデルで再生すると膝が曲がりすぎていたりします。
これは、足の長さはモデルによって異なるのに「センター」や「足IK」等の座標を同じ値で再生するために発生する現象ですが、Unityに取り込むとそのような問題が発生せず、どのモデルでも足が同じ角度で曲がり、地面についてくれます。
ただ、その仕様の違いのためUnityでは同じモーションであってもモデルによってモーション中の移動量が変わってしまいます。
(足の短いモデルと、足の長いモデルでは、同じ歩くモーションでも移動距離が異なります。)
複数人でのダンスモーションを再生する際は、足の長さを取得して開始時の座標を調整する処理を入れたり、移動量を調整する細工を入れないと、モデル間の位置関係を再現できません。
■原因2
IKの計算のズレ
原因1にある通り、MMDとUnityではIKの処理方法が異なっているように見えます。
そのため、MMD上でギリギリに調整して作っていると、MMD上では軽微な捻れが、Unity上では極端に捻れてしまうことがあります。
(IK位置や関節角度を少しずらしただけで見た目上の関節角度が一気に変わるような状態で、パラメータを細かくいじって見た目を整えていると、Unityに持っていった後に捻れが生じやすい気がします。)
■原因2の対応
あえて、関節が少しでも捩れていると違和感が目立つモデルでモーションを修正してからUnityに持ち込みます。
また、MMD上でパラメータを少し動かして、周囲の関節に極端な変化が無いことを確認しておきます。
MMD上できっちり安定したモーションになっていれば、Unityにもっていっても大丈夫なはずですので。
■原因3
足以外のIKがUnity上で動作しない。
モデルやモーションによっては、独自に手のIKを使って調整しているものがあります。
(両手を合わせたり、2つのモデル間で物を受け渡すような動作をモデル間で汎用的に使うためには必須なので)
■原因3の対応
Unityの標準Humanoidに手のIK機能は無いので、そのようなモーションをきれいに使うなら、手のIKに相当する機能を独自で実装するか、そういうアセットを使う必要があると思われます。
多少のズレが気にならないモーションなら、手の関節を調整・変換で試行錯誤して、なるべく色々なモデルで動作させても違和感が出ないように調整して妥協します。
#左右反転とmixerを併用
■やりたいこと
前のモーションから次のモーションに自然に移行するためにAnimationMixerPlayableを使いたい。
必要に応じてIAnimationJobで左右反転したモーションも使いたい。(ON/OFFを切り替えたい)
■困ったこと
・そもそもAnimationMixerPlayableとIAnimationJobを併用している例が見当たらない。
・現状、IAnimationJobだけでモーションを完全に左右反転するには、BodyLocalRotationではなくbodyRotationを、BodyLocalPositionではなくbodyPositionを反転させる必要がある様子。(つまり、グローバル座標で鏡の世界に行ってもらう必要がある。)
これに対応するため、モーションの左右反転と同時にグローバル座標を反転させることで、その場で左右反転モーションに切り替えたように見せる必要がある。
■やったこと
・左右反転用のIAnimationJobは、以下にあるMirrorPoseJobを使用
https://forum.unity.com/threads/playables-api-mirroring-clips-and-directorupdatemode-manual.504533/
・現在再生中のAnimationClipはcurrentPlayableに、直前まで再生していたAnimationClipはprePlayableに保管。
・nowMirrorとisMirrorの2つのbool 変数を用意。
モーション切り替えのタイミングでisMirrorがtrueになっている場合は、左右反転させる。
nowMirrorは現在反転しているかどうかを保管。
使うもの
using UnityEngine.Playables;
using UnityEngine.Animations;
using UnityEngine.Timeline;
使う変数
PlayableGraph graph;
AnimationMixerPlayable mixer;
private AnimationClipPlayable prePlayable, currentPlayable;
private AnimationScriptPlayable preScriptPlayable, currentScriptPlayable;
public bool isMirror = false;
public bool nowMirror = false;
初期化時に以下を実施
//普通にgraphとmixerを定義。
graph = PlayableGraph.Create();
var output = AnimationPlayableOutput.Create(graph, "output", GetComponent<Animator>());
mixer = AnimationMixerPlayable.Create(graph, 2, true);
//IAnimationJobのScriptを定義。モーション切り替え時には2つのモーションを同時処理するので2つ。
currentScriptPlayable = AnimationScriptPlayable.Create(graph, new MirrorPoseJob(), 1);
preScriptPlayable = AnimationScriptPlayable.Create(graph, new MirrorPoseJob(), 1);
//出力はmixerで固定。mixerへの入力にScriptPlayableを通すかどうかを切り替える。
output.SetSourcePlayable(mixer);
mixer.SetInputWeight(0, 1);
graph.Play();
新しいモーション(AnimationClip newAnimation)に切り替える際、コルーチンでmixerの割合を変えながら、前のモーションから次のモーションに少しずつ切り替える。
参考:https://gist.github.com/tsubaki/24565b08cc8c0ec3b1614b7bf6edaa5b
そのタイミングでisMirrorがnowMirrorと異なる場合は、mixerの入力を切り替えつつグローバル座標を反転。
isMirrorがtrue:mixerの2つの入力両方ともScriptPlayable経由で新旧それぞれのAnimationClipに接続
isMirrorがfalse:mixerの2つの入力を新旧それぞれのAnimationClipに直接接続
prePlayable = currentPlayable;
currentPlayable = AnimationClipPlayable.Create(graph, newAnimation);
if (isMirror == false)
{
if (nowMirror == true)
{
this.transform.position = new Vector3(-this.transform.position.x, this.transform.position.y, this.transform.position.z);
this.transform.eulerAngles = new Vector3(this.transform.eulerAngles.x, -this.transform.eulerAngles.y, this.transform.eulerAngles.z);
nowMirror = false;
}
mixer.ConnectInput(1, prePlayable, 0);
mixer.ConnectInput(0, currentPlayable, 0);
}
else
{
if (nowMirror == false)
{
this.transform.position = new Vector3(-this.transform.position.x, this.transform.position.y, this.transform.position.z);
this.transform.eulerAngles = new Vector3(this.transform.eulerAngles.x, -this.transform.eulerAngles.y, this.transform.eulerAngles.z);
nowMirror = true;
}
graph.Connect(prePlayable, 0, preScriptPlayable, 0);
mixer.ConnectInput(1, preScriptPlayable, 0);
graph.Connect(currentPlayable, 0, currentScriptPlayable, 0);
mixer.ConnectInput(0, currentScriptPlayable, 0);
}
float waitTime = Time.timeSinceLevelLoad + transitionTime;
yield return new WaitWhile(() => {
var diff = waitTime - Time.timeSinceLevelLoad;
if (diff <= 0)
{
mixer.SetInputWeight(1, 0);
mixer.SetInputWeight(0, 1);
return false;
}
else
{
var rate = Mathf.Clamp01(diff / transitionTime);
mixer.SetInputWeight(1, rate);
mixer.SetInputWeight(0, 1 - rate);
return true;
}
});
■要改善点1
この実装、isMirrorが変わったと同時にmixerの入力の両方を切り替えているので、isMirrorが切り替わった瞬間のポーズは、どうしても反転してしまう。
反転した後、旧モーションは反転前で、新モーションは反転後になるように、mixerの入力を順番に切り替える実装にするべき。
■要改善点2
長いモーションの場合、モーションの途中で左右反転したい場合もあるかも?
■要改善点3
そもそも左右反転をもっと簡単に実装できれば良いのだけれど、いまのところ他に良い方法が見当たらない。
・反転したデータを別途用意しておく→モーションデータ量が単純に倍になる。ディスク&メモリの無駄
・モデル自体のlocalScaleを反転させる→左右非対称のモデルでは不可
・Animation StateのMirror的な機能→現状、おそらくPlayable APIからは利用不可?
#sceneビューでは問題ないのにgameビューでは影にノイズが見える
Environment Lightでは問題なく、Directional LightやSpot Lightの影響がある程度強くなると、一部のモデルでのみ突然表示されます。
画像奥のモデルは問題なく表示されているように、モデルによって発生の閾値が異なります。
ビルド後のoculus quest実機では発生しません。
■原因
カメラでPost-Process Layer(Unity標準のPost Processing Stack v2)を使っていると発生する様子。
(以前は問題なかったので、何かをアップデートしたか、設定の変更で発生するようになったのかも?)
■対応
Post-process Volume側ですべての効果を無効化しても発生します。
以前は発生していなかったので、何かのバージョン変更か設定変更で解消すると思われすが、いまのところPost-Process Layerを使わないこと以外の改善案が見つかっていません。
実機では発生しないので無視することにします。