LeapMotion 突き抜けない手がほしい - 第2回 | Unity3D
前回は手がモデルに接触した際に、接触部に留まるオブジェクトをテスト的に可視化してみました。今回はその可視化したオブジェクトの位置にグラフィックハンドモデルを強制的に移動させてみます。
グラフィックハンドモデルの強制移動
LeapMotionのHandControllerでは、グラフィックハンドモデルをUpdateで移動させているようです。なので、LateUpdateて位置を上書きしてやれば良さそう。ではさっそく、なにも考えずに位置と回転をPalmの位置に合わせてみましょう。
画像:回転がずれる物理ハンドとグラフィックハンド
合わせてみると分かるんですが回転がずれます。物理ハンドはおっぱい側を向いてるのに、グラフィックハンドは反対方向に手のひらを向け、y軸も傾いているようです。しかも、右手と左手で回転のズレが違う模様。めんどくさい。。。最終的に次のようにするとピタッとあいました。また位置についてはPalmの位置に単純に合わせるとおっぱいと手の間に隙間が開くので、1cmほど手のひら方向にずらしました。
void ForceMoveGraphicsHand() { var handContainer = this.graphicsHandTransform.GetChild(1); // グラフィクスハンド回転追随 handContainer.rotation = this.SlerpPalmTransform.rotation; if (this.myHand.IsLeft) { handContainer.Rotate(new Vector3(0f, 270f, 0f)); } else { handContainer.Rotate(new Vector3(180f, 90f, 0f)); } // グラフィクスハンド位置追随 var palmDirection = this.myHand.PalmNormal.ToUnity(); palmDirection = new Vector3(palmDirection.x * -1, palmDirection.y, palmDirection.z * -1); handContainer.position = this.SlerpPalmTransform.position - palmDirection * -1; }
myHandは別途下記のように取得してます。
Hand GetMyHand() { Frame frame = this.handCtrler.GetFrame(); Hand myHand = null; if (frame != null) { var leapHand = this.rigidHand.GetLeapHand(); foreach (var hand in frame.Hands) { if (leapHand != null || leapHand.Id == hand.Id) { myHand = hand; break; } } } return myHand; }
そして完成したAlpha版がこちら
見えない物理演算用のハンドモデルはそのまま体を突き抜けるバージョンですが、動かしてみるとこうなります。
おっぱいに手が吸い付く!すごい!!グラフィックハンドの位置調整だけでも揉んでる感はかなり向上しました。
ただ実際の指のColliderはモデルを突き抜けているのでおっぱいの動きが怪しいです。見えている手もベースとなるColliderの挙動に位置と回転をあわせてるだけなので、Colliderが何かに引っかかってひっくり返ったりすると実際の手の向きとは関係なく画面内の手もひっくり返ります。まぁAlpha版だしね。
次は実際の当たり判定の位置調整を行う予定です。
LeapMotion 突き抜けない手がほしい - 第1回 | Unity3D
LeapMotionでMMD4Mecanimのモデルに触りたいと思った場合、ネット上に色々情報もあるので比較的簡単に実現できます。でも、何かが違うわけです。なんで手がモデルを突き抜けちゃうん?おっぱい揉むときも細心の注意を払って座標を気にしつつ揉むなんてヤだよ!もっと簡単におっぱい!
参考画像:おっぱい表層を突き抜け埋もれる手
突き抜けない手がほしい
突き抜けない手があれば安心しておっぱい揉めますよね?ということで、モデルを突き抜けずおっぱいにフィットする手を作っていこうと思います。完成するか否かは現時点でさっぱり分かりません。また完成しても、使い心地的に「コレじゃない」感が漂う可能性もあります。しかし男子たるもの、おっぱいの為なら頑張るべきだと思いました。
一応ヘボくても完成したら公開する予定です。
物理演算用とグラフィック用で1セット
LeapMotionのHandControllerは現実世界の手1本につき、物理演算用とグラフィック用の2つのハンドオブジェクトを生成します。物理用はColliderを搭載した不可視オブジェクト、グラフィック用はColliderなしの可視オブジェクトです。
RigidHandのColliderに注目
とりあえず、現状の手をモデルの体内に突っ込んで、物理演算用のハンドオブジェクト(RigidHand)のColliderがどんな挙動をするか見てみました。
画像:おっぱい表層に留まるPalmのCollider
胸付近にある一番大きいColliderが手の平(Palm)です。その他のColliderは指のものです。指のColliderはバラバラで荒ぶりまくりです。しかし、PalmのColliderはほぼ毎回衝突付近に留まるという事に気が付きました。
ということは、Palmがオブジェクトと接触中はグラフィック用ハンドの位置を強制的にPalmの位置に移動してやれば・・・。
現時点での仕様案
- 対象モデルはMMD4Mecanimにて作成したMMDモデルとする。
- Palmがオブジェクトに接触中は、グラフィック用ハンドをPalmの位置に強制移動させる。
- Palmがオブジェクトに接触中は、物理演算用ハンドのPalm以外のColliderを全て無効化する。
- でもそれだとおっぱいもめない。
- なので、Palmがオブジェクトに接触中はHandControllerを経由しない第2の物理演算用ハンドを接触中のPalmの位置に作成する。
- 作成した第2物理ハンドの各Colliderのpositionとrotationに、無効化中の物理演算用ハンドの各Collider情報を旨いこと入れてあげる。
- 全ての指ColliderはIsTriggerとする。(じゃないと接触時にColliderが荒ぶるので)
とりあえず、これでおっぱい揉めるんじゃないか!?( ゚∀゚)o彡゜おっぱい!おっぱい!
LeapMotionでハンドオブジェクトをプーリング | Unity3D
LeapMotionのHandControllerは手がセンサー内に入るとハンドオブジェクトを生成し、センサー外にでると破棄します。この生成と破棄のコストが気になりだしたら夜も寝れなくなったので、プーリングできるように改造しようと決意。
「なんかHandControllerにDestroy Handsっていうチェックボックスあるよ?これでいいんじゃない?」とか思いますよね。自分も思いました。で、実際にチェックを入れてみると。。。まぁ、そうなるわけです。誰が得するんだこの仕様。。。
LeapMotionのCore Assetsを改造
そんなわけで、Destroy Handsチェックボックスにチェックが入っていなかった場合はプーリングするように改造しました。改造元のバージョンは「Unity Core Assets v2.3.1」です。
ターゲットソースはHandController.csになります。CreateHand関数、DestroyHand関数及びCreateHand関数のコール元となるUpdateHandModels関数にも手をいれました。これで、手がセンサー範囲外にでるとハンドオブジェクトのGameObjectがDisable化され、再度センサー内に手がもどってくるとActive化されて再利用されるようになりました。
(本当はGameObjectの位置をカメラ範囲外に出すことで対応したかったんですが、色々あってGameObjectのactiveを弄ってます。)
一応ソースは下記の通りです。もし利用する場合はコメント文や関数名から場所を見つけて、コピペで置き換えて下さい。当然自己責任で!
UpdateHandModels関数
// Create the hand and initialized it if it doesn't exist yet. if (!all_hands.ContainsKey(leap_hand.Id) || all_hands[leap_hand.Id] == null) { int handNumber = (mirrorZAxis != leap_hand.IsLeft) ? 0 : 1; bool isRigidModel = model.GetType().Equals(typeof(RigidHand)); bool isPooled = isRigidModel ? handRigidModels[handNumber] != null : handRiggedModels[handNumber] != null; HandModel new_hand = CreateHand(model, handNumber); if (!isPooled) { new_hand.SetLeapHand(leap_hand); new_hand.MirrorZAxis(mirrorZAxis); new_hand.SetController(this); // Set scaling based on reference hand. float hand_scale = MM_TO_M * leap_hand.PalmWidth / new_hand.handModelPalmWidth; new_hand.transform.localScale = hand_scale * transform.lossyScale; new_hand.InitHand(); new_hand.UpdateHand(); } new_hand.gameObject.SetActive(true); all_hands[leap_hand.Id] = new_hand; } else {
CreateHand関数
/** Creates a new HandModel instance. */ HandModel[] handRigidModels = new HandModel[20]; HandModel[] handRiggedModels = new HandModel[20]; protected HandModel CreateHand(HandModel model, int handNumber) { bool isRigidModel = model.GetType().Equals(typeof(RigidHand)); HandModel hand_model = isRigidModel ? handRigidModels[handNumber] : handRiggedModels[handNumber]; if (hand_model == null) { hand_model = Instantiate(model, transform.position, transform.rotation) as HandModel; hand_model.gameObject.SetActive(true); Leap.Utils.IgnoreCollisions(hand_model.gameObject, gameObject); if (handParent != null) { hand_model.transform.SetParent(handParent.transform); } if (isRigidModel) handRigidModels[handNumber] = hand_model; else handRiggedModels[handNumber] = hand_model; } return hand_model; }
DestroyHand関数
/** * Destroys a HandModel instance if HandController.destroyHands is true (the default). * If you set destroyHands to false, you must destroy the hand instances elsewhere in your code. */ protected void DestroyHand(HandModel hand_model) { if (destroyHands) { bool isRigidModel = hand_model.GetType().Equals(typeof(RigidHand)); if (isRigidModel) { for (int i = 0; i < 2; i++) { if (handRigidModels[i] != null && handRigidModels[i].GetInstanceID() == hand_model.GetInstanceID()) { handRigidModels[i] = null; break; } } } else { for (int i = 0; i < 2; i++) { if (handRiggedModels[i] != null && handRiggedModels[i].GetInstanceID() == hand_model.GetInstanceID()) { handRiggedModels[i] = null; break; } } } hand_model.SetLeapHand(null); if (hand_model != null) Destroy(hand_model.gameObject); } else { hand_model.SetLeapHand(null); hand_model.gameObject.SetActive(false); } }
MMD4Mecanimにおける同一材質名の罠 | Unity3D
配布されているMMDモデルで、色違いバージョンが同梱されている場合。ありますよね。ちょっとだけ違うモデルが同梱されている場合。ありますよね。
それらをMMD4MecanimでFBX化する場合、大体の場合は問題無いんですが以下の場合は注意が必要です。
- 同一フォルダに複数別バージョンのモデルが入っていて、それぞれのモデルに「違うテクスチャを持つ同一材質名の材質」が存在する。
マテリアルが上書きされる
MMD4MecanimモデルのFBXを作成する際、作成されるFBXのマテリアルは同フォルダ内のMaterialsフォルダに保存されます。そして、そこに保存されるマテリアル名は材質名になる模様。
なので、同一材質名の材質を持つAモデルとBモデルが同フォルダにある場合、AのFBXを作成した後に、BのFBXを作成すると、Aのマテリアルが同一名のBのマテリアルで上書きされます。
で、Aモデルを表示させてみると、「あれれーおっかしいぞー」な状態に突入するわけです。
ダメだった対処法
実はFBXのModelオプションにマテリアル名の命名規則を変更できるオプション「Material Naming」があるんですね。そのオプション値の一つに「Model Name + Model's Material」ってのがあるんですよ奥さん!これでいいじゃん。
って思うじゃん。さっそく試してみるじゃん。。。
「うへへへ、お前も蝋人形にしてやろうかー」
こわい。。。だめじゃん!
そうダメなんですよ。蝋人形ですよ。AssetPostprocessor.OnPreprocessModelの中で「Model Name + Model's Material」に変えてもダメなんです。
大丈夫だった対処法
で、結局どうしたかというと、エディタ拡張でこうしました。
- AssetPostprocessor.OnPostprocessModel内で、「Materials」フォルダを「Materials + モデル名」にリネーム(MoveAsset)する。
ただ、上記だけだとMMD4MecanimによるFBX再作成処理が走った際にプレハブ化していたモデルのマテリアル参照が切れちゃうので、さらに下記の処理も追加しました。
- AssetPostprocessor.OnPreprocessModel内で、「Materials + モデル名」フォルダを「Materials」に一旦戻す。
これでOKです。下記のコードがそれですね。適当にEditorフォルダに置いてあげて下さい。
なお、利用は自己責任でお願いします。
using UnityEditor; using UnityEngine; using System.IO; using System.Text; public class MMD4MFBXImportProcess : AssetPostprocessor { void OnPreprocessModel() { if (!isMMD4Model()) return; MoveMaterialFolderByPre(); } void OnPostprocessModel(GameObject g) { if (!isMMD4Model()) return; MoveMaterialFolderByPost(g); } bool isMMD4Model() { var mi = assetImporter as ModelImporter; if (mi.assetPath.ToLower().EndsWith(".fbx")) { string tmp = mi.assetPath.Substring(0, mi.assetPath.Length - ".fbx".Length) + ".MMD4Mecanim.asset"; return File.Exists(tmp); } return false; } void MoveMaterialFolderByPre() { var mi = assetImporter as ModelImporter; string frompath = new StringBuilder().AppendFormat("{0}/{1}", Path.GetDirectoryName(mi.assetPath), "Materials").ToString(); string objectname = mi.assetPath.Substring( Path.GetDirectoryName(mi.assetPath).Length + 1, mi.assetPath.Length - Path.GetDirectoryName(mi.assetPath).Length - 1 - ".fbx".Length); string topath = new StringBuilder().AppendFormat("{0}_{1}", frompath, objectname).ToString(); if (Directory.Exists(topath)) { if (Directory.Exists(frompath)) { AssetDatabase.DeleteAsset(frompath); } AssetDatabase.MoveAsset(topath, frompath); } } void MoveMaterialFolderByPost(GameObject g) { var mi = assetImporter as ModelImporter; string frompath = new StringBuilder().AppendFormat("{0}/{1}", Path.GetDirectoryName(mi.assetPath), "Materials").ToString(); if (Directory.Exists(frompath)) { string topath = new StringBuilder().AppendFormat("{0}_{1}", frompath, g.name).ToString(); AssetDatabase.DeleteAsset(topath); AssetDatabase.MoveAsset(frompath, topath); } } }
MMD4Mecanimモデルにまばたきをさせる | MMD4MecanimFaciemBlink
MMD4MecanimFaciem用のまばたきツール「MMD4MecanimFaciemBlink」です。MMD4Mecanimモデルがまばたきしてくれます。
まばたき部のロジックははウダサンコウボウさんのMMD4MFaceBlinkを参考にしました。(勉強になりました)
ダウンロード
MMD4MecanimFaciemBlink
(2015/11/29 リンク先を変更しました)
動作に必要なもの
MMD4MecanimとMMD4MecanimFaciemが必要です。
MMD4MecanimモデルにFaciemDatabase、FaciemController、FaciemBlinkの3つがアタッチされている必要があります。
使い方
1. FaciemDatabaseでまばたき用の表情を作る。(表情名はなんでもOKです)
2. FaciemBlinkでその表情を選ぶ。
まばたきしない方
- FaciemControllerのアタッチを忘れていませんか?
- 現在の表情とまばたきの表情が同じだったりしませんか?
仕様
- まばたき用に指定した表情にてweightが0を超えるMorphをまばたきとして処理します。
- 現在の表情がIgnore by face に指定した表情の場合は、まばたきを行いません。
- 現在の表情にて、Ignore by morph に指定したモーフのweightが0を超えている場合は、まばたきを行いません。
今後
表情の変化中にまばたきした場合、若干の違和感があるので改善できればなー。と思ってます。
バグについて
バグを…バグをクレメンス…
エディタ拡張における値の変更チェック | Unity3D
GUI.changedは出来る奴だ
MMD4MecanimFaciemを作リ始めた当初、拡張したGUIで値が変更されたかどうかチェックするために「前の値を保存しておいて、GUIコントロールを表示した後の値と比較する」なんてことをやっていた訳ですが、もっと便利なGUI.changedなんてものがあると途中で気付き。。。
おそいよ!もっと早く教えてよ!と涙目になったわけです。
そんな悲劇を繰り返さないよう、一応紹介しておきます。
紹介と言っても簡単なので、公式Unityのリファレンスをペタっと。
まぁ、要はコントロールの値が変更されたらGUI.changedが勝手にtrueになるわけですね。
何故変更有無を知りたかったのか
2つ理由がありました。
1. シーンの変更フラグを建てたかった
普通に作ったエディタ拡張でコンポーネントの値を変更してもUnityEditorはシーンが変更されたと思ってくれない。SetDirtyを実行しているのにです。お陰で手動でのシーン保存を忘れてUnityEditorを終了させようものなら、「保存しますか?」の一言もなしにそれまでの変更内容が破棄されます。合掌。。。
なので、拡張エディタのGUIで値が変更されたらEditorApplication.MarkSceneDirty()を実行してシーン変更フラグを建てあげるわけです。そうすると、終了時に「保存しますか?」という気の利いた一言を投げかけてくれるようになります。やったぜ!
2. 無条件でSetDirtyを実行するとシーンビューがチラチラする
MMD4MecanimFaciemの開発中にあることに気づきました。シーンビューにおいて、なんだか知らないがMMD4MecanimModelのミクさんのコライダーが常時細かくチラチラしてる。と。
結局のところ、OnInspectorGUI関数の中で常にMMD4MecanimModelコンポーネントに対しSetDirtyを実行していたのですが、コレが原因でした。なので必要があるときだけSetDirtyした方がいいんだなと。まぁ、当たり前の事に改めて気付かされたわけですね。
で、これもGUI.changedのお陰で簡単に解決できました。チラチラしない。やったぜ!
見やすいMMD4Mecanim表情設定方法
表情がみえん!
MMD4Mecanimモデルの表情をMMD4MecanimFaciemで見ながら編集しようと思った場合、往々にしてメッシュ線やコライダー線がバリバリ描画されて「表情みえねーよ!」的な事になりがち。
でも、ちょっとしたことでクリアな画面で表情を舐めまわすように見ながら表情編集できるようになります。
解決方法
- 普通にMMD4Mecanimモデルを選択。例によって見えん!
- 通常のInspectorの右上にある鍵アイコンをクリックしてInspectorをロック。
- シーンビューの何もないところをクリック
これでOKです。
あとは好きなだけ、あんな表情やこんな表情をさせてあげて下さい。