Last active
October 25, 2021 16:03
-
-
Save alankent/ee46bb20adce9c09be1a47dcb1179f91 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using UnityEditor; | |
using UnityEngine.Timeline; | |
using UnityEngine.Playables; | |
using System.Collections.Generic; | |
using UnityEditor.Timeline; | |
using uOSC; | |
using UnityEngine.Events; | |
using EVMC4U; | |
using VRM; | |
using System; | |
using System.IO; | |
using Entum; | |
using System.Reflection; | |
public class VmcWindow : EditorWindow | |
{ | |
[SerializeField] | |
int port = 39539; | |
private bool running = false; | |
private bool recording = false; | |
private VmcUOscServer server; | |
[MenuItem("Window/Alan Tools/VMC Receiver")] | |
public static void ShowWindow() | |
{ | |
// Find or create window so it appears on the screen. | |
EditorWindow.GetWindow<VmcWindow>("VMC"); | |
} | |
void OnGUI() | |
{ | |
EditorGUILayout.TextField("Status", StatusMessage); | |
Model = EditorGUILayout.ObjectField("Model", Model, typeof(GameObject), true) as GameObject; | |
if (Model == null) return; | |
var buttonStyle = EditorStyles.miniButton; | |
if (!running) | |
{ | |
GUILayout.BeginHorizontal("box"); | |
if (GUILayout.Button("Start Receiving VMC Packets", buttonStyle)) | |
{ | |
if (server == null) server = new VmcUOscServer(); | |
server.StartListening(port); | |
Start(); | |
running = true; | |
StatusMessage = "VMC receiver started"; | |
} | |
GUILayout.EndHorizontal(); | |
} | |
else | |
{ | |
GUILayout.BeginHorizontal("box"); | |
if (GUILayout.Button("Stop Receiving VMC Packets", buttonStyle)) | |
{ | |
if (recording) | |
{ | |
RecordEnd(); | |
} | |
running = false; | |
recording = false; | |
server.StopListening(); | |
StatusMessage = "VMC receiver stopped"; | |
} | |
GUILayout.EndHorizontal(); | |
GUILayout.BeginHorizontal("box"); | |
if (!recording) | |
{ | |
if (GUILayout.Button("Record", buttonStyle)) | |
{ | |
StatusMessage = "Recording started"; | |
RecordStart(); | |
recording = true; | |
} | |
} | |
else | |
{ | |
if (GUILayout.Button("Stop Recording", buttonStyle)) | |
{ | |
RecordEnd(); | |
recording = false; | |
StatusMessage = "Recording stopped"; | |
} | |
} | |
GUILayout.EndHorizontal(); | |
} | |
} | |
void Update() | |
{ | |
if (running && server != null && Model != null) | |
{ | |
// Read messages over VMC UPD connection. | |
server.ProcessMessages(); | |
// Move the character's bones etc. | |
ModelUpdate(); | |
// If recording, copy the values over to an animation clip (via EasyMotionRecorder code). | |
if (recording) | |
{ | |
RecordingUpdate(); | |
} | |
} | |
} | |
// ============== Based on uOscServer component. | |
// I needed to make some methods public, and allow port number to be changed. | |
public class VmcUOscServer | |
{ | |
#if NETFX_CORE | |
Udp udp_ = new Uwp.Udp(); | |
Thread thread_ = new Uwp.Thread(); | |
#else | |
uOSC.Udp udp_ = new uOSC.DotNet.Udp(); | |
Thread thread_ = new uOSC.DotNet.Thread(); | |
#endif | |
Parser parser_ = new Parser(); | |
public class DataReceiveEvent : UnityEvent<Message> { }; | |
public DataReceiveEvent onDataReceived { get; private set; } | |
public void StartListening(int port) | |
{ | |
onDataReceived = new DataReceiveEvent(); | |
udp_.StartServer(port); | |
thread_.Start(UpdateMessage); | |
} | |
public void StopListening() | |
{ | |
thread_.Stop(); | |
udp_.Stop(); | |
onDataReceived = null; | |
} | |
public void ProcessMessages() | |
{ | |
if (onDataReceived == null) return; | |
while (parser_.messageCount > 0) | |
{ | |
var message = parser_.Dequeue(); | |
onDataReceived.Invoke(message); | |
} | |
} | |
// Main body of thread (asynchronously reads messages from UDP, adding them to parser queue). | |
void UpdateMessage() | |
{ | |
while (udp_.messageCount > 0) | |
{ | |
var buf = udp_.Receive(); | |
int pos = 0; | |
parser_.Parse(buf, ref pos, buf.Length); | |
} | |
} | |
} | |
// =========================== Taken from class ExternalReceiver | |
// https://sabowl.sakura.ne.jp/gpsnmeajp/ | |
// [Header("ExternalReceiver v3.7")] | |
public GameObject Model = null; | |
public bool Freeze = false; //すべての同期を止める(撮影向け) | |
public bool PacktLimiter = true; //パケットフレーム数が一定値を超えるとき、パケットを捨てる | |
[Header("Root Synchronize Option")] | |
public Transform RootPositionTransform = null; //VR向けroot位置同期オブジェクト指定 | |
public Transform RootRotationTransform = null; //VR向けroot回転同期オブジェクト指定 | |
public bool RootPositionSynchronize = false; //AJK was true; //ルート座標同期(ルームスケール移動) | |
public bool RootRotationSynchronize = false; //AJK was true; //ルート回転同期 | |
public bool RootScaleOffsetSynchronize = false; //MRスケール適用 | |
[Header("Other Synchronize Option")] | |
public bool BlendShapeSynchronize = true; //表情等同期 | |
public bool BonePositionSynchronize = true; //ボーン位置適用(回転は強制) | |
[Header("Synchronize Cutoff Option")] | |
public bool HandPoseSynchronizeCutoff = false; //指状態反映オフ | |
public bool EyeBoneSynchronizeCutoff = false; //目ボーン反映オフ | |
[Header("Lowpass Filter Option")] | |
public bool BonePositionFilterEnable = false; //ボーン位置フィルタ | |
public bool BoneRotationFilterEnable = false; //ボーン回転フィルタ | |
public float BoneFilter = 0.7f; //ボーンフィルタ係数 | |
public bool BlendShapeFilterEnable = false; //BlendShapeフィルタ | |
public float BlendShapeFilter = 0.7f; //BlendShapeフィルタ係数 | |
[Header("Other Option")] | |
public bool HideInUncalibrated = false; //キャリブレーション出来ていないときは隠す | |
public bool SyncCalibrationModeWithScaleOffsetSynchronize = true; //キャリブレーションモードとスケール設定を連動させる | |
[Header("Status (Read only)")] | |
[SerializeField] | |
private string StatusMessage = ""; //状態メッセージ(Inspector表示用) | |
public string OptionString = ""; //VMCから送信されるオプション文字列 | |
// I removed automatic VRM file loading. | |
//AJK public string loadedVRMPath = ""; //読み込み済みVRMパス | |
//AJK public string loadedVRMName = ""; //読み込み済みVRM名前 | |
//AJK public GameObject LoadedModelParent = null; //読み込んだモデルの親 | |
public int LastPacketframeCounterInFrame = 0; //1フレーム中に受信したパケットフレーム数 | |
public int DropPackets = 0; //廃棄されたパケット(not パケットフレーム) | |
public Vector3 HeadPosition = Vector3.zero; | |
[Header("Daisy Chain")] | |
public GameObject[] NextReceivers = new GameObject[6]; //デイジーチェーン | |
//---Const--- | |
//rootパケット長定数(拡張判別) | |
const int RootPacketLengthOfScaleAndOffset = 8; | |
//---Private--- | |
private ExternalReceiverManager externalReceiverManager = null; | |
//フィルタ用データ保持変数 | |
private Vector3[] bonePosFilter = new Vector3[Enum.GetNames(typeof(HumanBodyBones)).Length]; | |
private Quaternion[] boneRotFilter = new Quaternion[Enum.GetNames(typeof(HumanBodyBones)).Length]; | |
private Dictionary<string, float> blendShapeFilterDictionaly = new Dictionary<string, float>(); | |
//通信状態保持変数 | |
private int Available = 0; //データ送信可能な状態か | |
private float time = 0; //送信時の時刻 | |
//モデル切替検出用reference保持変数 | |
private GameObject OldModel = null; | |
//ボーン情報取得 | |
Animator animator = null; | |
//VRMのブレンドシェーププロキシ | |
VRMBlendShapeProxy blendShapeProxy = null; | |
//ボーンENUM情報テーブル | |
Dictionary<string, HumanBodyBones> HumanBodyBonesTable = new Dictionary<string, HumanBodyBones>(); | |
//ボーン情報テーブル | |
Dictionary<HumanBodyBones, Vector3> HumanBodyBonesPositionTable = new Dictionary<HumanBodyBones, Vector3>(); | |
Dictionary<HumanBodyBones, Quaternion> HumanBodyBonesRotationTable = new Dictionary<HumanBodyBones, Quaternion>(); | |
//ブレンドシェープ変換テーブル | |
Dictionary<string, BlendShapeKey> StringToBlendShapeKeyDictionary = new Dictionary<string, BlendShapeKey>(); | |
Dictionary<BlendShapeKey, float> BlendShapeToValueDictionary = new Dictionary<BlendShapeKey, float>(); | |
//uOSCサーバー | |
//AJK uOSC.uOscServer server = null; | |
//エラー・無限ループ検出フラグ(trueで一切の受信を停止する) | |
bool shutdown = false; | |
//フレーム間パケットフレーム数測定 | |
int PacketCounterInFrame = 0; | |
//1フレームに30パケットフレーム来たら、同一フレーム内でそれ以上は受け取らない。 | |
const int PACKET_LIMIT_MAX = 30; | |
//読込中は読み込まない | |
bool isLoading = false; | |
//メッセージ処理一時変数struct(負荷対策) | |
Vector3 pos; | |
Quaternion rot; | |
Vector3 scale; | |
Vector3 offset; | |
public void Start() | |
{ | |
//nullチェック | |
if (NextReceivers == null) | |
{ | |
NextReceivers = new GameObject[0]; | |
} | |
//NextReciverのインターフェースを取得する | |
externalReceiverManager = new ExternalReceiverManager(NextReceivers); | |
//サーバーを取得 | |
//AJK was: server = GetComponent<uOSC.uOscServer>(); | |
//AJK But I needed my own VMC receiver. | |
if (server != null) | |
{ | |
//サーバーを初期化 | |
StatusMessage = "Waiting for VMC..."; | |
server.onDataReceived.AddListener(OnDataReceived); | |
} | |
else | |
{ | |
//デイジーチェーンスレーブモード | |
StatusMessage = "Waiting for Master..."; | |
} | |
} | |
//デイジーチェーンを更新 | |
public void UpdateDaisyChain() | |
{ | |
//nullチェック | |
if (NextReceivers == null) | |
{ | |
NextReceivers = new GameObject[0]; | |
} | |
externalReceiverManager.GetIExternalReceiver(NextReceivers); | |
} | |
//外部から通信状態を取得するための公開関数 | |
public int GetAvailable() | |
{ | |
return Available; | |
} | |
//外部から通信時刻を取得するための公開関数 | |
public float GetRemoteTime() | |
{ | |
return time; | |
} | |
// Renamed from Update() and called from Update() function above. | |
public void ModelUpdate() | |
{ | |
//エラー・無限ループ時は処理をしない | |
if (shutdown) { return; } | |
//Freeze有効時は動きを一切止める | |
if (Freeze) { return; } | |
LastPacketframeCounterInFrame = PacketCounterInFrame; | |
PacketCounterInFrame = 0; | |
//5.6.3p1などRunInBackgroundが既定で無効な場合Unityが極めて重くなるため対処 | |
Application.runInBackground = true; | |
//VRMモデルからBlendShapeProxyを取得(タイミングの問題) | |
if (blendShapeProxy == null && Model != null) | |
{ | |
blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>(); | |
} | |
//ルート位置がない場合 | |
if (RootPositionTransform == null && Model != null) | |
{ | |
//モデル姿勢をルート姿勢にする | |
RootPositionTransform = Model.transform; | |
} | |
//ルート回転がない場合 | |
if (RootRotationTransform == null && Model != null) | |
{ | |
//モデル姿勢をルート姿勢にする | |
RootRotationTransform = Model.transform; | |
} | |
//モデルがない場合はエラー表示をしておく(親切心) | |
if (Model == null) | |
{ | |
StatusMessage = "Model not found."; | |
return; | |
} | |
//モデルが更新されたときに関連情報を更新する | |
if (OldModel != Model && Model != null) | |
{ | |
animator = Model.GetComponent<Animator>(); | |
blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>(); | |
OldModel = Model; | |
Debug.Log("[ExternalReceiver] New model detected"); | |
//v0.56 BlendShape仕様変更対応 | |
//Debug.Log("-- Make BlendShapeProxy BSKey Table --"); | |
//BSキー値辞書の初期化(SetValueで無駄なキーが適用されるのを防止する) | |
BlendShapeToValueDictionary.Clear(); | |
//文字-BSキー辞書の初期化(キー情報の初期化) | |
StringToBlendShapeKeyDictionary.Clear(); | |
//全Clipsを取り出す | |
foreach (var c in blendShapeProxy.BlendShapeAvatar.Clips) | |
{ | |
string key = ""; | |
bool unknown = false; | |
//プリセットかどうかを調べる | |
if (c.Preset == BlendShapePreset.Unknown) | |
{ | |
//非プリセット(Unknown)であれば、Unknown用の名前変数を参照する | |
key = c.BlendShapeName; | |
unknown = true; | |
} | |
else | |
{ | |
//プリセットであればENUM値をToStringした値を利用する | |
key = c.Preset.ToString(); | |
unknown = false; | |
} | |
//非ケース化するために小文字変換する | |
string lowerKey = key.ToLower(); | |
//Debug.Log("Add: [key]->" + key + " [lowerKey]->" + lowerKey + " [clip]->" + c.ToString() + " [bskey]->"+c.Key.ToString() + " [unknown]->"+ unknown); | |
//小文字名-BSKeyで登録する | |
if (StringToBlendShapeKeyDictionary.ContainsKey(lowerKey)) | |
{ | |
Debug.Log("Blendshape Key already loaded: " + key + " [lowerKey]->" + lowerKey + " [clip]->" + c.ToString() + " Model.name " + Model.name); | |
} | |
else | |
{ | |
StringToBlendShapeKeyDictionary.Add(lowerKey, BlendShapeKey.CreateFrom(c)); | |
} | |
} | |
//メモ: プリセット同名の独自キー、独自キーのケース違いの重複は、共に区別しないと割り切る | |
/* | |
Debug.Log("-- Registered List --"); | |
foreach (var k in StringToBlendShapeKeyDictionary) | |
{ | |
Debug.Log("[k.Key]" + k.Key + " -> [k.Value.Name]" + k.Value.Name); | |
} | |
Debug.Log("-- End BlendShapeProxy BSKey Table --"); | |
*/ | |
} | |
BoneSynchronizeByTable(); | |
} | |
//データ受信イベント | |
private void OnDataReceived(uOSC.Message message) | |
{ | |
//チェーン数0としてデイジーチェーンを発生させる | |
MessageDaisyChain(ref message, 0); | |
} | |
//デイジーチェーン処理 | |
public void MessageDaisyChain(ref uOSC.Message message, int callCount) | |
{ | |
//Startされていない場合無視 | |
if (externalReceiverManager == null)// AJK removed Component checks: || enabled == false || gameObject.activeInHierarchy == false) | |
{ | |
return; | |
} | |
//エラー・無限ループ時は処理をしない | |
if (shutdown) | |
{ | |
return; | |
} | |
//パケットリミッターが有効な場合、一定以上のパケットフレーム/フレーム数を観測した場合、次のフレームまでパケットを捨てる | |
if (PacktLimiter && (LastPacketframeCounterInFrame > PACKET_LIMIT_MAX)) | |
{ | |
DropPackets++; | |
return; | |
} | |
//メッセージを処理 | |
if (!Freeze) | |
{ | |
//異常を検出して動作停止 | |
try | |
{ | |
ProcessMessage(ref message); | |
} | |
catch (Exception e) | |
{ | |
StatusMessage = "Error: Exception"; | |
Debug.LogError(" --- Communication Error ---"); | |
Debug.LogError(e.ToString()); | |
shutdown = true; | |
return; | |
} | |
} | |
//次のデイジーチェーンへ伝える | |
if (!externalReceiverManager.SendNextReceivers(message, callCount)) | |
{ | |
//無限ループ対策 | |
StatusMessage = "Infinite loop detected!"; | |
//以降の処理を全部停止 | |
shutdown = true; | |
} | |
} | |
//メッセージ処理本体 | |
private void ProcessMessage(ref uOSC.Message message) | |
{ | |
//メッセージアドレスがない、あるいはメッセージがない不正な形式の場合は処理しない | |
if (message.address == null || message.values == null) | |
{ | |
StatusMessage = "Bad message."; | |
return; | |
} | |
//ルート位置がない場合 | |
if (RootPositionTransform == null && Model != null) | |
{ | |
//モデル姿勢をルート姿勢にする | |
RootPositionTransform = Model.transform; | |
} | |
//ルート回転がない場合 | |
if (RootRotationTransform == null && Model != null) | |
{ | |
//モデル姿勢をルート姿勢にする | |
RootRotationTransform = Model.transform; | |
} | |
//モーションデータ送信可否 | |
if (message.address == "/VMC/Ext/OK" | |
&& (message.values[0] is int)) | |
{ | |
Available = (int)message.values[0]; | |
if (Available == 0) | |
{ | |
StatusMessage = "Waiting for [Load VRM]"; | |
} | |
//V2.5 キャリブレーション状態(長さ3以上) | |
if (message.values.Length >= 3) | |
{ | |
if ((message.values[1] is int) && (message.values[2] is int)) | |
{ | |
int calibrationState = (int)message.values[1]; | |
int calibrationMode = (int)message.values[2]; | |
//キャリブレーション出来ていないときは隠す | |
if (HideInUncalibrated && Model != null) | |
{ | |
Model.SetActive(calibrationState == 3); | |
} | |
//スケール同期をキャリブレーションと連動させる | |
if (SyncCalibrationModeWithScaleOffsetSynchronize) | |
{ | |
RootScaleOffsetSynchronize = !(calibrationMode == 0); //通常モードならオフ、MR系ならオン | |
} | |
} | |
} | |
return; | |
} | |
//データ送信時刻 | |
else if (message.address == "/VMC/Ext/T" | |
&& (message.values[0] is float)) | |
{ | |
time = (float)message.values[0]; | |
PacketCounterInFrame++; //フレーム中のパケットフレーム数を測定 | |
return; | |
} | |
//VRM自動読み込み | |
else if (message.address == "/VMC/Ext/VRM" | |
&& (message.values[0] is string) | |
&& (message.values[1] is string) | |
) | |
{ | |
string path = (string)message.values[0]; | |
string title = (string)message.values[1]; | |
return; | |
} | |
//オプション文字列 | |
else if (message.address == "/VMC/Ext/Opt" | |
&& (message.values[0] is string)) | |
{ | |
OptionString = (string)message.values[0]; | |
return; | |
} | |
//モデルがないか、モデル姿勢、ルート姿勢が取得できないなら以降何もしない | |
if (Model == null || Model.transform == null || RootPositionTransform == null || RootRotationTransform == null) | |
{ | |
return; | |
} | |
//Root姿勢 | |
if (message.address == "/VMC/Ext/Root/Pos" | |
&& (message.values[0] is string) | |
&& (message.values[1] is float) | |
&& (message.values[2] is float) | |
&& (message.values[3] is float) | |
&& (message.values[4] is float) | |
&& (message.values[5] is float) | |
&& (message.values[6] is float) | |
&& (message.values[7] is float) | |
) | |
{ | |
StatusMessage = "OK"; | |
pos.x = (float)message.values[1]; | |
pos.y = (float)message.values[2]; | |
pos.z = (float)message.values[3]; | |
rot.x = (float)message.values[4]; | |
rot.y = (float)message.values[5]; | |
rot.z = (float)message.values[6]; | |
rot.w = (float)message.values[7]; | |
//位置同期 | |
if (RootPositionSynchronize) | |
{ | |
RootPositionTransform.localPosition = pos; | |
} | |
//回転同期 | |
if (RootRotationSynchronize) | |
{ | |
RootRotationTransform.localRotation = rot; | |
} | |
//スケール同期とオフセット補正(v2.1拡張プロトコルの場合のみ) | |
if (RootScaleOffsetSynchronize && message.values.Length > RootPacketLengthOfScaleAndOffset | |
&& (message.values[8] is float) | |
&& (message.values[9] is float) | |
&& (message.values[10] is float) | |
&& (message.values[11] is float) | |
&& (message.values[12] is float) | |
&& (message.values[13] is float) | |
) | |
{ | |
scale.x = 1.0f / (float)message.values[8]; | |
scale.y = 1.0f / (float)message.values[9]; | |
scale.z = 1.0f / (float)message.values[10]; | |
offset.x = (float)message.values[11]; | |
offset.y = (float)message.values[12]; | |
offset.z = (float)message.values[13]; | |
Model.transform.localScale = scale; | |
RootPositionTransform.localPosition = Vector3.Scale(RootPositionTransform.localPosition, scale); | |
//位置同期が有効な場合のみオフセットを反映する | |
if (RootPositionSynchronize) | |
{ | |
offset = Vector3.Scale(offset, scale); | |
RootPositionTransform.localPosition -= offset; | |
} | |
} | |
else | |
{ | |
Model.transform.localScale = Vector3.one; | |
} | |
} | |
//ボーン姿勢 | |
else if (message.address == "/VMC/Ext/Bone/Pos" | |
&& (message.values[0] is string) | |
&& (message.values[1] is float) | |
&& (message.values[2] is float) | |
&& (message.values[3] is float) | |
&& (message.values[4] is float) | |
&& (message.values[5] is float) | |
&& (message.values[6] is float) | |
&& (message.values[7] is float) | |
) | |
{ | |
string boneName = (string)message.values[0]; | |
pos.x = (float)message.values[1]; | |
pos.y = (float)message.values[2]; | |
pos.z = (float)message.values[3]; | |
rot.x = (float)message.values[4]; | |
rot.y = (float)message.values[5]; | |
rot.z = (float)message.values[6]; | |
rot.w = (float)message.values[7]; | |
//Debug.Log("BONE=" + boneName + " POS=" + pos.ToString() + " ROT=" + rot.ToString()); | |
//Humanoidボーンに該当するボーンがあるか調べる | |
HumanBodyBones bone; | |
if (HumanBodyBonesTryParse(ref boneName, out bone)) | |
{ | |
//あれば位置と回転をキャッシュする | |
if (HumanBodyBonesPositionTable.ContainsKey(bone)) | |
{ | |
HumanBodyBonesPositionTable[bone] = pos; | |
} | |
else | |
{ | |
HumanBodyBonesPositionTable.Add(bone, pos); | |
} | |
if (HumanBodyBonesRotationTable.ContainsKey(bone)) | |
{ | |
HumanBodyBonesRotationTable[bone] = rot; | |
} | |
else | |
{ | |
HumanBodyBonesRotationTable.Add(bone, rot); | |
} | |
} | |
//受信と更新のタイミングは切り離した | |
} | |
//ブレンドシェープ同期 | |
else if (message.address == "/VMC/Ext/Blend/Val" | |
&& (message.values[0] is string) | |
&& (message.values[1] is float) | |
) | |
{ | |
//一旦変数に格納する | |
string key = (string)message.values[0]; | |
float value = (float)message.values[1]; | |
//BlendShapeフィルタが有効なら | |
if (BlendShapeFilterEnable) | |
{ | |
//フィルタテーブルに存在するか確認する | |
if (blendShapeFilterDictionaly.ContainsKey(key)) | |
{ | |
//存在する場合はフィルタ更新して値として反映する | |
blendShapeFilterDictionaly[key] = (blendShapeFilterDictionaly[key] * BlendShapeFilter) + value * (1.0f - BlendShapeFilter); | |
value = blendShapeFilterDictionaly[key]; | |
//if (key == "O") Debug.Log("BSFE1: key=" + key + " val=" + value); | |
} | |
else | |
{ | |
//存在しない場合はフィルタに登録する。値はそのまま | |
blendShapeFilterDictionaly.Add(key, value); | |
//if (key == "O") Debug.Log("BSFE2: key=" + key + " val=" + value); | |
} | |
} | |
if (BlendShapeSynchronize && blendShapeProxy != null) | |
{ | |
//v0.56 BlendShape仕様変更対応 | |
//辞書からKeyに変換し、Key値辞書に値を入れる | |
//通信で受信したキーを小文字に変換して非ケース化 | |
string lowerKey = key.ToLower(); | |
//キーに該当するBSKeyが存在するかチェックする | |
BlendShapeKey bskey; | |
if (StringToBlendShapeKeyDictionary.TryGetValue(lowerKey, out bskey)) | |
{ | |
//キーに対して値を登録する | |
BlendShapeToValueDictionary[bskey] = value; | |
//if (lowerKey == "o") Debug.Log("[lowerKey]->"+ lowerKey+" [bskey]->"+bskey.ToString()+" [value]->"+value); | |
} | |
else | |
{ | |
//そんなキーは無い | |
//AJK Debug.LogError("[lowerKey]->" + lowerKey + " is not found"); | |
} | |
} | |
} | |
//ブレンドシェープ適用 | |
else if (message.address == "/VMC/Ext/Blend/Apply") | |
{ | |
if (BlendShapeSynchronize && blendShapeProxy != null) | |
{ | |
blendShapeProxy.SetValues(BlendShapeToValueDictionary); | |
} | |
} | |
} | |
#if false // AJK | |
//モデル破棄 | |
public void DestroyModel() | |
{ | |
//存在すれば即破壊(異常顔防止) | |
if (Model != null) | |
{ | |
Destroy(Model); | |
Model = null; | |
} | |
if (LoadedModelParent != null) | |
{ | |
Destroy(LoadedModelParent); | |
LoadedModelParent = null; | |
} | |
} | |
#endif | |
//ボーン位置をキャッシュテーブルに基づいて更新 | |
private void BoneSynchronizeByTable() | |
{ | |
//キャッシュテーブルを参照 | |
foreach (var bone in HumanBodyBonesTable) | |
{ | |
//キャッシュされた位置・回転を適用 | |
if (HumanBodyBonesPositionTable.ContainsKey(bone.Value) && HumanBodyBonesRotationTable.ContainsKey(bone.Value)) | |
{ | |
BoneSynchronize(bone.Value, HumanBodyBonesPositionTable[bone.Value], HumanBodyBonesRotationTable[bone.Value]); | |
} | |
} | |
} | |
//ボーン位置同期 | |
private void BoneSynchronize(HumanBodyBones bone, Vector3 pos, Quaternion rot) | |
{ | |
//操作可能な状態かチェック | |
if (animator != null && bone != HumanBodyBones.LastBone) | |
{ | |
//ボーンによって操作を分ける | |
var t = animator.GetBoneTransform(bone); | |
if (t != null) | |
{ | |
//指ボーン | |
if (bone == HumanBodyBones.LeftIndexDistal || | |
bone == HumanBodyBones.LeftIndexIntermediate || | |
bone == HumanBodyBones.LeftIndexProximal || | |
bone == HumanBodyBones.LeftLittleDistal || | |
bone == HumanBodyBones.LeftLittleIntermediate || | |
bone == HumanBodyBones.LeftLittleProximal || | |
bone == HumanBodyBones.LeftMiddleDistal || | |
bone == HumanBodyBones.LeftMiddleIntermediate || | |
bone == HumanBodyBones.LeftMiddleProximal || | |
bone == HumanBodyBones.LeftRingDistal || | |
bone == HumanBodyBones.LeftRingIntermediate || | |
bone == HumanBodyBones.LeftRingProximal || | |
bone == HumanBodyBones.LeftThumbDistal || | |
bone == HumanBodyBones.LeftThumbIntermediate || | |
bone == HumanBodyBones.LeftThumbProximal || | |
bone == HumanBodyBones.RightIndexDistal || | |
bone == HumanBodyBones.RightIndexIntermediate || | |
bone == HumanBodyBones.RightIndexProximal || | |
bone == HumanBodyBones.RightLittleDistal || | |
bone == HumanBodyBones.RightLittleIntermediate || | |
bone == HumanBodyBones.RightLittleProximal || | |
bone == HumanBodyBones.RightMiddleDistal || | |
bone == HumanBodyBones.RightMiddleIntermediate || | |
bone == HumanBodyBones.RightMiddleProximal || | |
bone == HumanBodyBones.RightRingDistal || | |
bone == HumanBodyBones.RightRingIntermediate || | |
bone == HumanBodyBones.RightRingProximal || | |
bone == HumanBodyBones.RightThumbDistal || | |
bone == HumanBodyBones.RightThumbIntermediate || | |
bone == HumanBodyBones.RightThumbProximal) | |
{ | |
//指ボーンカットオフが有効でなければ | |
if (!HandPoseSynchronizeCutoff) | |
{ | |
//ボーン同期する。ただしフィルタはかけない | |
BoneSynchronizeSingle(t, ref bone, ref pos, ref rot, false, false); | |
} | |
} | |
//目ボーン | |
else if (bone == HumanBodyBones.LeftEye || | |
bone == HumanBodyBones.RightEye) | |
{ | |
//目ボーンカットオフが有効でなければ | |
if (!EyeBoneSynchronizeCutoff) | |
{ | |
//ボーン同期する。ただしフィルタはかけない | |
BoneSynchronizeSingle(t, ref bone, ref pos, ref rot, false, false); | |
} | |
} | |
else | |
{ | |
//ボーン同期する。フィルタは設定依存 | |
BoneSynchronizeSingle(t, ref bone, ref pos, ref rot, BonePositionFilterEnable, BoneRotationFilterEnable); | |
} | |
} | |
} | |
} | |
//1本のボーンの同期 | |
private void BoneSynchronizeSingle(Transform t, ref HumanBodyBones bone, ref Vector3 pos, ref Quaternion rot, bool posFilter, bool rotFilter) | |
{ | |
BoneFilter = Mathf.Clamp(BoneFilter, 0f, 1f); | |
//ボーン位置同期が有効か | |
if (BonePositionSynchronize) | |
{ | |
//ボーン位置フィルタが有効か | |
if (posFilter) | |
{ | |
bonePosFilter[(int)bone] = (bonePosFilter[(int)bone] * BoneFilter) + pos * (1.0f - BoneFilter); | |
t.localPosition = bonePosFilter[(int)bone]; | |
} | |
else | |
{ | |
t.localPosition = pos; | |
} | |
} | |
//ボーン回転フィルタが有効か | |
if (rotFilter) | |
{ | |
boneRotFilter[(int)bone] = Quaternion.Slerp(boneRotFilter[(int)bone], rot, 1.0f - BoneFilter); | |
t.localRotation = boneRotFilter[(int)bone]; | |
} | |
else | |
{ | |
t.localRotation = rot; | |
} | |
} | |
//ボーンENUM情報をキャッシュして高速化 | |
private bool HumanBodyBonesTryParse(ref string boneName, out HumanBodyBones bone) | |
{ | |
//ボーンキャッシュテーブルに存在するなら | |
if (HumanBodyBonesTable.ContainsKey(boneName)) | |
{ | |
//キャッシュテーブルから返す | |
bone = HumanBodyBonesTable[boneName]; | |
//ただしLastBoneは発見しなかったことにする(無効値として扱う) | |
if (bone == HumanBodyBones.LastBone) | |
{ | |
return false; | |
} | |
return true; | |
} | |
else | |
{ | |
//キャッシュテーブルにない場合、検索する | |
var res = EnumTryParse<HumanBodyBones>(boneName, out bone); | |
if (!res) | |
{ | |
//見つからなかった場合はLastBoneとして登録する(無効値として扱う)ことにより次回から検索しない | |
bone = HumanBodyBones.LastBone; | |
} | |
//キャシュテーブルに登録する | |
HumanBodyBonesTable.Add(boneName, bone); | |
return res; | |
} | |
} | |
//互換性を持ったTryParse | |
private static bool EnumTryParse<T>(string value, out T result) where T : struct | |
{ | |
#if NET_4_6 | |
return Enum.TryParse(value, out result); | |
#else | |
try | |
{ | |
result = (T)Enum.Parse(typeof(T), value, true); | |
return true; | |
} | |
catch | |
{ | |
result = default(T); | |
return false; | |
} | |
#endif | |
} | |
//} | |
// ========================================= Taken from EasyMotionRecorder. | |
// | |
/** | |
[EasyMotionRecorder] | |
Copyright (c) 2018 Duo.inc | |
This software is released under the MIT License. | |
http://opensource.org/licenses/mit-license.php | |
*/ | |
[SerializeField] | |
private Animator _animator; | |
[SerializeField] | |
private bool _recording; | |
[SerializeField] | |
protected int FrameIndex; | |
[SerializeField, Tooltip("普段はOBJECTROOTで問題ないです。特殊な機材の場合は変更してください")] | |
private MotionDataSettings.Rootbonesystem _rootBoneSystem = MotionDataSettings.Rootbonesystem.Objectroot; | |
[SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] | |
private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; | |
[SerializeField] | |
private HumanBodyBones IK_LeftFootBone = HumanBodyBones.LeftFoot; | |
[SerializeField] | |
private HumanBodyBones IK_RightFootBone = HumanBodyBones.RightFoot; | |
protected HumanoidPoses Poses; | |
protected float RecordedTime; | |
protected float StartTime; | |
private HumanPose _currentPose; | |
private HumanPoseHandler _poseHandler; | |
public Action OnRecordStart; | |
public Action OnRecordEnd; | |
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] | |
public float TargetFPS = 24.0f; // AJK was 60 | |
#if false | |
// AJK: Moved this into RecordingUpdate() method, as not sure how Window lifecycle management works. | |
// Use this for initialization | |
private void Awake() | |
{ | |
if (_animator == null) | |
{ | |
_animator = Model.GetComponent<Animator>(); | |
if (_animator == null) | |
{ | |
Debug.LogError("MotionDataRecorderにanimatorがセットされていません。MotionDataRecorderを削除します。"); | |
Destroy(this); | |
return; | |
} | |
} | |
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); | |
} | |
#endif | |
// Update is called once per frame | |
private void RecordingUpdate() | |
{ | |
if (Model == null || !_recording) return; | |
if (_animator == null) | |
{ | |
_animator = Model.GetComponent<Animator>(); | |
if (_animator == null) | |
{ | |
Debug.LogError("MotionDataRecorderにanimatorがセットされていません。MotionDataRecorderを削除します。"); | |
return; | |
} | |
} | |
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); | |
RecordedTime = Time.time - StartTime; | |
if (TargetFPS != 0.0f) | |
{ | |
var nextTime = (1.0f * (FrameIndex + 1)) / TargetFPS; | |
if (nextTime > RecordedTime) | |
{ | |
return; | |
} | |
if (FrameIndex % TargetFPS == 0) | |
{ | |
Debug.Log("Motion_FPS=" + 1 / (RecordedTime / FrameIndex)); | |
} | |
} | |
else | |
{ | |
if (Time.frameCount % Application.targetFrameRate == 0) | |
{ | |
Debug.Log("Motion_FPS=" + 1 / Time.deltaTime); | |
} | |
} | |
//現在のフレームのHumanoidの姿勢を取得 | |
_poseHandler.GetHumanPose(ref _currentPose); | |
//posesに取得した姿勢を書き込む | |
var serializedPose = new HumanoidPoses.SerializeHumanoidPose(); | |
switch (_rootBoneSystem) | |
{ | |
case MotionDataSettings.Rootbonesystem.Objectroot: | |
serializedPose.BodyRootPosition = _animator.transform.localPosition; | |
serializedPose.BodyRootRotation = _animator.transform.localRotation; | |
break; | |
case MotionDataSettings.Rootbonesystem.Hipbone: | |
serializedPose.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position; | |
serializedPose.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation; | |
Debug.LogWarning(_animator.GetBoneTransform(_targetRootBone).position); | |
break; | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); | |
var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); | |
var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation); | |
LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ); | |
RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ); | |
serializedPose.BodyPosition = bodyTQ.t; | |
serializedPose.BodyRotation = bodyTQ.q; | |
serializedPose.LeftfootIK_Pos = LeftFootTQ.t; | |
serializedPose.LeftfootIK_Rot = LeftFootTQ.q; | |
serializedPose.RightfootIK_Pos = RightFootTQ.t; | |
serializedPose.RightfootIK_Rot = RightFootTQ.q; | |
serializedPose.FrameCount = FrameIndex; | |
serializedPose.Muscles = new float[_currentPose.muscles.Length]; | |
serializedPose.Time = RecordedTime; | |
for (int i = 0; i < serializedPose.Muscles.Length; i++) | |
{ | |
serializedPose.Muscles[i] = _currentPose.muscles[i]; | |
} | |
SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose); | |
Poses.Poses.Add(serializedPose); | |
FrameIndex++; | |
} | |
/// <summary> | |
/// 録画開始 | |
/// </summary> | |
private void RecordStart() | |
{ | |
if (_recording) | |
{ | |
return; | |
} | |
Poses = ScriptableObject.CreateInstance<HumanoidPoses>(); | |
if (OnRecordStart != null) | |
{ | |
OnRecordStart(); | |
} | |
OnRecordEnd += WriteAnimationFile; | |
_recording = true; | |
RecordedTime = 0f; | |
StartTime = Time.time; | |
FrameIndex = 0; | |
} | |
/// <summary> | |
/// 録画終了 | |
/// </summary> | |
private void RecordEnd() | |
{ | |
if (!_recording) | |
{ | |
return; | |
} | |
if (OnRecordEnd != null) | |
{ | |
OnRecordEnd(); | |
} | |
OnRecordEnd -= WriteAnimationFile; | |
_recording = false; | |
} | |
private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) | |
{ | |
HumanBodyBones[] values = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[]; | |
foreach (HumanBodyBones b in values) | |
{ | |
if (b < 0 || b >= HumanBodyBones.LastBone) | |
{ | |
continue; | |
} | |
Transform t = animator.GetBoneTransform(b); | |
if (t != null) | |
{ | |
var bone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); | |
bone.Set(animator.transform, t); | |
pose.HumanoidBones.Add(bone); | |
} | |
} | |
} | |
protected virtual void WriteAnimationFile() | |
{ | |
#if UNITY_EDITOR | |
SafeCreateDirectory("Assets/Resources"); | |
//var path = string.Format("Assets/Resources/RecordMotion_{0}_{1:yyyy_MM_dd_HH_mm_ss}.asset", ShortenName(_animator.name), DateTime.Now); | |
//var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); | |
//LastClipFile = uniqueAssetPath; | |
//AJK AssetDatabase.CreateAsset(Poses, uniqueAssetPath); | |
//AJK AssetDatabase.Refresh(); | |
Poses.ExportHumanoidAnim(); // Write as humanoid animation clip, not the default EasyMotionRecorder export format. | |
StartTime = Time.time; | |
RecordedTime = 0f; | |
FrameIndex = 0; | |
#endif | |
} | |
private string ShortenName(string name) | |
{ | |
int i = 0; | |
while (i < name.Length) | |
{ | |
if (name[i] == '-' || name[i] == '.' || name[i] == '_' || name[i] == ' ') | |
{ | |
return name.Substring(0, i); | |
} | |
i++; | |
} | |
return name; | |
} | |
/// <summary> | |
/// 指定したパスにディレクトリが存在しない場合 | |
/// すべてのディレクトリとサブディレクトリを作成します | |
/// </summary> | |
public static DirectoryInfo SafeCreateDirectory(string path) | |
{ | |
return Directory.Exists(path) ? null : Directory.CreateDirectory(path); | |
} | |
public Animator CharacterAnimator | |
{ | |
get { return _animator; } | |
} | |
public class TQ | |
{ | |
public TQ(Vector3 translation, Quaternion rotation) | |
{ | |
t = translation; | |
q = rotation; | |
} | |
public Vector3 t; | |
public Quaternion q; | |
// Scale should always be 1,1,1 | |
} | |
public class AvatarUtility | |
{ | |
static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ) | |
{ | |
int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal); | |
if (humanId == (int)HumanBodyBones.LastBone) | |
throw new InvalidOperationException("Invalid human id."); | |
MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic); | |
if (methodGetAxisLength == null) | |
throw new InvalidOperationException("Cannot find GetAxisLength method."); | |
MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic); | |
if (methodGetPostRotation == null) | |
throw new InvalidOperationException("Cannot find GetPostRotation method."); | |
Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId }); | |
var goalTQ = new TQ(skeletonTQ.t, skeletonTQ.q * postRotation); | |
if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot) | |
{ | |
// Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght | |
// Both are equivalent but GetAxisLength is the generic way and work for all human bone | |
float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId }); | |
Vector3 footBottom = new Vector3(axislength, 0, 0); | |
goalTQ.t += (goalTQ.q * footBottom); | |
} | |
// IK goal are in avatar body local space | |
Quaternion invRootQ = Quaternion.Inverse(animatorBodyPositionRotation.q); | |
goalTQ.t = invRootQ * (goalTQ.t - animatorBodyPositionRotation.t); | |
goalTQ.q = invRootQ * goalTQ.q; | |
goalTQ.t /= humanScale; | |
return goalTQ; | |
} | |
static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal) | |
{ | |
HumanBodyBones humanId = HumanBodyBones.LastBone; | |
switch (avatarIKGoal) | |
{ | |
case AvatarIKGoal.LeftFoot: humanId = HumanBodyBones.LeftFoot; break; | |
case AvatarIKGoal.RightFoot: humanId = HumanBodyBones.RightFoot; break; | |
case AvatarIKGoal.LeftHand: humanId = HumanBodyBones.LeftHand; break; | |
case AvatarIKGoal.RightHand: humanId = HumanBodyBones.RightHand; break; | |
} | |
return humanId; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment