diff --git a/.gitignore b/.gitignore index 442485f4..f3303d7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ -[Ll]ibrary/ +[Ll]ibrary/ [Tt]emp/ [Oo]bj/ [Bb]uild/ [Bb]uilds/ +[Uu]ser[Ss]ettings/ Logs/ +OpenVRInitError.txt Assets/AssetStoreTools* Assets/ExternalPlugins/RootMotion* Assets/ExternalPlugins/SteamVR* @@ -35,17 +37,17 @@ Assets/ExternalPlugins/UnityMemoryMappedFile/UnityMemoryMappedFile* Assets/VRoidSDK* Assets/Scenes/Example* Assets/Scenes/LoginExample* -ControlWindowWPF/ControlWindowWPF/VRoidHubWindow* -UnityMemoryMappedFile/PipeCommands_VRoidSDK* -Assets/Scripts/VRoidSDKConnector* -Assets/Tobii* -Assets/ExternalPlugins/ViveSR* -tobii* +#プラグインが使う外部SDK(再配布不可のためリポジトリには含めない。置き場所の説明だけ残す) +PluginProjects/*/SDK/* +!PluginProjects/*/SDK/README.md +#Tobii SDK同梱のネイティブDLL(リポジトリ直下のみ。プラグインの自前コードは対象外) +/tobii_gameintegration_*.dll SteamVR*csproj Backup* Assets/ExternalPlugins/uOSC* Assets/ExternalPlugins/MidiJack* Assets/ExternalPlugins/EasyDeviceDiscoveryProtocol* +FastSpringBone*csproj DepthFirstScheduler*csproj MeshUtility*csproj MToon*csproj @@ -59,10 +61,14 @@ VRM*csproj VMCMOD*csproj Oculus*csproj DVRSDK*csproj +VRoidSDK*csproj +VMC.VRoidHubSetup*csproj Assets/Resources* Assets/Scripts/DMMVRConnectConnector* ControlWindowWPF/ControlWindowWPF/DMMVRConnectWindow* UnityMemoryMappedFile/PipeCommands_DMMVRConnect* +#mocopi Receiver SDKに対してUnityが生成するcsproj(リポジトリ直下のみ) +/com.sony.mocopi.receiver.csproj # Visual Studio cache directory @@ -93,4 +99,21 @@ sysinfo.txt *.unitypackage common.json Assets/_TerrainAutoUpgrade/ -.vsconfig \ No newline at end of file +.vsconfig +/TrackerPositions.json + +#自動テスト用データ(テスト用VRM・ゴールデン・実行結果) +TestData/ + +#コード署名ツール(証明書・KMS設定・署名済みバイナリのキャッシュを含むためpushしない) +codesignkit/ + +#プラグインのビルド成果物(SDKのネイティブDLLを含むため配布物には入れるがpushしない) +BuildRootFiles/ControlPanel/Plugins/ +PluginProjects/**/bin/ +PluginProjects/**/obj/ +ControlWindowWPF/VMC.ControlPanel.PluginAPI/bin/ +ControlWindowWPF/VMC.ControlPanel.PluginAPI/obj/ + +#エディタ実行用にプラグインをコピーする場所(ビルド成果物・SDKのネイティブDLLを含む) +/Plugins/ diff --git a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs b/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs deleted file mode 100644 index ccb8ccb1..00000000 --- a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -#if UNIVRM_0_68_IMPORTER || UNIVRM_0_77_IMPORTER -using UniGLTF; -#endif -using UnityEngine; -using VRM; - -namespace DVRSDK.Avatar -{ - public class VRMLoader : IDisposable - { -#if UNIVRM_LEGACY_IMPORTER || UNIVRM_0_68_IMPORTER - private VRMImporterContext currentContext; - public GameObject Model => currentContext == null ? null : currentContext.Root; -#elif UNIVRM_0_77_IMPORTER - private VRMImporterContext currentContext; - private RuntimeGltfInstance currentInstance; - public GameObject Model => currentInstance == null ? null : currentInstance.Root; -#else - private IDisposable currentContext = null; - public GameObject Model = null; -#endif - - - /// - /// 読み込んだモデルを実際に表示する - /// - public void ShowMeshes() - { -#if UNIVRM_LEGACY_IMPORTER || UNIVRM_0_68_IMPORTER || UNIVRM_0_77_IMPORTER - if (Model == null) - throw new InvalidOperationException("Need to load VRM model first."); -#endif - -#if UNIVRM_LEGACY_IMPORTER || UNIVRM_0_68_IMPORTER - currentContext.ShowMeshes(); -#elif UNIVRM_0_77_IMPORTER - currentInstance.ShowMeshes(); -#else -#endif - -#if UNIVRM_0_68_IMPORTER - currentContext.DisposeOnGameObjectDestroyed(); -#endif - } - - /// - /// 同期でファイルからVRMモデルを読み込む - /// - /// ファイルのパス - /// VRMモデルのGameObject - public GameObject LoadVrmModelFromFile(string vrmFilePath) - { - // ファイルをByte配列に読み込みます - var bytes = File.ReadAllBytes(vrmFilePath); - - return LoadVrmModelFromByteArray(bytes); - } - - /// - /// 同期でファイルからVRMMetaObjectを読み込む - /// - /// - /// - /// - public VRMMetaObject LoadVrmMetaFromFile(string vrmFilePath, bool createThumbnail) - { - // ファイルをByte配列に読み込みます - var bytes = File.ReadAllBytes(vrmFilePath); - - return LoadVrmMetaFromByteArray(bytes, createThumbnail); - } - - /// - /// 非同期でファイルからVRMモデルを読み込む - /// - /// ファイルのパス - /// VRMモデルのGameObject - public async Task LoadVrmModelFromFileAsync(string vrmFilePath) - { - // ファイルをByte配列に読み込みます - var bytes = await ReadAllBytesAsync(vrmFilePath); - - return await LoadVrmModelFromByteArrayAsync(bytes); - } - - /// - /// 非同期でファイルからVRMMetaObjectを読み込む - /// - /// - /// - /// - public async Task LoadVrmMetaFromFileAsync(string vrmFilePath, bool createThumbnail) - { - // ファイルをByte配列に読み込みます - var bytes = await ReadAllBytesAsync(vrmFilePath); - - return await LoadVrmMetaFromByteArrayAsync(bytes, createThumbnail); - } - - /// - /// 同期でByte配列からVRMモデルを読み込む - /// - /// - /// - public GameObject LoadVrmModelFromByteArray(byte[] vrmByteArray) - { -#if UNIVRM_LEGACY_IMPORTER - InitializeVrmContextFromByteArray(vrmByteArray); - - // 同期処理で読み込みます - currentContext.Load(); - - // 読込が完了するとcontext.RootにモデルのGameObjectが入っています - var root = currentContext.Root; - - return root; -#elif UNIVRM_0_68_IMPORTER - var parser = new GltfParser(); - parser.ParseGlb(vrmByteArray); - - currentContext = new VRMImporterContext(parser); - currentContext.Load(); - - return currentContext.Root; -#elif UNIVRM_0_77_IMPORTER - var parser = new GlbLowLevelParser(string.Empty, vrmByteArray); - var data = parser.Parse(); - - currentContext = new VRMImporterContext(data); - currentInstance = currentContext.Load(); - - return currentInstance.Root; -#else - return null; -#endif - } - - /// - /// 同期でByte配列からVRMMetaObjectを読み込む - /// - /// - /// - /// - public VRMMetaObject LoadVrmMetaFromByteArray(byte[] vrmByteArray, bool createThumbnail) - { - InitializeVrmContextFromByteArray(vrmByteArray); - - return GetMeta(createThumbnail); - } - - /// - /// ConnectからロードしたモデルデータをVRM化する - /// - /// - /// - public object LoadVRMModelFromConnect(byte[] cachedData) - { - return LoadVrmModelFromByteArray(cachedData); - } - - /// - /// 非同期でByte配列からVRMモデルを読み込む - /// - /// - /// - public async Task LoadVrmModelFromByteArrayAsync(byte[] vrmByteArray) - { -#if UNIVRM_LEGACY_IMPORTER - await InitializeVrmContextFromByteArrayAsync(vrmByteArray); - - // 非同期処理(Task)で読み込みます - await currentContext.LoadAsyncTask(); - - // 読込が完了するとcontext.RootにモデルのGameObjectが入っています - var root = currentContext.Root; - - return root; -#elif UNIVRM_0_68_IMPORTER - var parser = new GltfParser(); - await Task.Run(() => - { - parser.ParseGlb(vrmByteArray); - }); - - currentContext = new VRMImporterContext(parser); - await currentContext.LoadAsync(); - - return currentContext.Root; -#elif UNIVRM_0_77_IMPORTER - var parser = new GlbLowLevelParser(string.Empty, vrmByteArray); - GltfData data = null; - - await Task.Run(() => - { - data = parser.Parse(); - }); - - currentContext = new VRMImporterContext(data); - currentInstance = await currentContext.LoadAsync(); - - return currentInstance.Root; -#else - return null; -#endif - } - - /// - /// 非同期でByte配列からVRMMetaObjectを読み込む - /// - /// - /// - /// - public async Task LoadVrmMetaFromByteArrayAsync(byte[] vrmByteArray, bool createThumbnail) - { - await InitializeVrmContextFromByteArrayAsync(vrmByteArray); - - return GetMeta(createThumbnail); - } - - /// - /// ConnectからロードしたモデルデータをVRM化する - /// - /// - /// - public async Task LoadVRMModelFromConnectAsync(byte[] cachedData) - { - return await LoadVrmModelFromByteArrayAsync(cachedData); - } - - /// - /// Byte配列からVRMImporterContextの初期化をします - /// - /// - public void InitializeVrmContextFromByteArray(byte[] vrmByteArray) - { -#if UNIVRM_LEGACY_IMPORTER - // VRMImporterContextがVRMを読み込む機能を提供します - currentContext = new VRMImporterContext(); - - // GLB形式でJSONを取得しParseします - currentContext.ParseGlb(vrmByteArray); -#elif UNIVRM_0_68_IMPORTER - var parser = new GltfParser(); - parser.ParseGlb(vrmByteArray); - currentContext = new VRMImporterContext(parser); -#else -#endif - } - - /// - /// 非同期でByte配列からVRMImporterContextの初期化をします - /// - /// - public async Task InitializeVrmContextFromByteArrayAsync(byte[] vrmByteArray) - { -#if UNIVRM_LEGACY_IMPORTER - // VRMImporterContextがVRMを読み込む機能を提供します - currentContext = new VRMImporterContext(); - - // GLB形式でJSONを取得しParseします - await Task.Run(() => currentContext.ParseGlb(vrmByteArray)); -#elif UNIVRM_0_68_IMPORTER - var parser = new GltfParser(); - await Task.Run(() => parser.ParseGlb(vrmByteArray)); - currentContext = new VRMImporterContext(parser); -#elif UNIVRM_0_77_IMPORTER - var parser = new GlbLowLevelParser(string.Empty, vrmByteArray); - GltfData data = null; - - await Task.Run(() => - { - data = parser.Parse(); - }); - - currentContext = new VRMImporterContext(data); - currentInstance = null; -#else -#endif - } - - /// - /// Metaデータの読み出し - /// - /// サムネイルを作成するかどうか - /// VRMMetaObject - public VRMMetaObject GetMeta(bool createThumbnail) - { -#if UNIVRM_LEGACY_IMPORTER || UNIVRM_0_68_IMPORTER || UNIVRM_0_77_IMPORTER - if (currentContext == null) - throw new InvalidOperationException("Need to initialize VRM model first."); - return currentContext.ReadMeta(createThumbnail); -#else - return null; -#endif - } - - // Byte列を得る - public async static Task ReadAllBytesAsync(string path) - { - byte[] result; - using (FileStream SourceStream = File.Open(path, FileMode.Open)) - { - result = new byte[SourceStream.Length]; - await SourceStream.ReadAsync(result, 0, (int)SourceStream.Length); - } - return result; - } - - public void SetupFirstPerson(Camera firstPersonCamera) - { - // HMDに顔が映りこまないようにFirstPersonの初期化 - var vrmFirstPerson = Model.GetComponent(); - if (vrmFirstPerson != null) vrmFirstPerson.Setup(); - - foreach (var camera in GameObject.FindObjectsOfType()) - { - camera.cullingMask = (camera == firstPersonCamera) - ? camera.cullingMask & ~(1 << VRMFirstPerson.THIRDPERSON_ONLY_LAYER) // ThirdPersonだけ無効 - : camera.cullingMask & ~(1 << VRMFirstPerson.FIRSTPERSON_ONLY_LAYER) // FirstPersonだけ無効 - ; - } - } - - public void Dispose() - { - currentContext?.Dispose(); - } - } -} diff --git a/Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs b/Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs deleted file mode 100644 index 944b4ccd..00000000 --- a/Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs +++ /dev/null @@ -1,120 +0,0 @@ -#if UNITY_EDITOR -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEditor; -using UnityEngine; - -namespace DVRSDK.Editor -{ - [InitializeOnLoad] - public class PluginVersionChecker : AssetPostprocessor - { - static PluginVersionChecker() - { - EditorApplication.wantsToQuit += Quit; - UpdateDefineSymbols(); - } - - private static List InitializeSymbols() - { - var symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';').ToList(); - - return symbols; - } - - private static void UpdateDefineSymbols() - { - var symbols = InitializeSymbols(); - var version = GetVRMVersion(); - - // Importer - symbols.Remove("UNIVRM_0_68_IMPORTER"); - symbols.Remove("UNIVRM_0_77_IMPORTER"); - symbols.Remove("UNIVRM_LEGACY_IMPORTER"); - if (version.Value.major == 0 && version.Value.minor < 68) - { - symbols.Add("UNIVRM_LEGACY_IMPORTER"); - } - else if (version.Value.major == 0 && version.Value.minor < 77) - { - symbols.Add("UNIVRM_0_68_IMPORTER"); - } - else - { - symbols.Add("UNIVRM_0_77_IMPORTER"); - } - - // Exporter - symbols.Remove("UNIVRM_0_71_EXPORTER"); - symbols.Remove("UNIVRM_0_75_EXPORTER"); - symbols.Remove("UNIVRM_0_79_EXPORTER"); - symbols.Remove("UNIVRM_LEGACY_EXPORTER"); - if (version.Value.major == 0 && version.Value.minor < 71) - { - symbols.Add("UNIVRM_LEGACY_EXPORTER"); - } - else if (version.Value.major == 0 && version.Value.minor < 75) - { - symbols.Add("UNIVRM_0_71_EXPORTER"); - } - else if (version.Value.major == 0 && version.Value.minor < 79) - { - symbols.Add("UNIVRM_0_75_EXPORTER"); - } - else - { - symbols.Add("UNIVRM_0_79_EXPORTER"); - } - - PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, string.Join(";", symbols)); - - EditorApplication.UnlockReloadAssemblies(); - } - - private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) - { - foreach (string str in importedAssets) - { - if (str.Contains("VRMImporter")) - { - UpdateDefineSymbols(); - } - } - } - - private static (int major, int minor, int patch)? GetVRMVersion() - { - var vrmVersionType = Type.GetType("VRM.VRMVersion"); - if (vrmVersionType == null) vrmVersionType = Type.GetType("VRM.VRMVersion, VRM"); - - if (vrmVersionType != null) - { - int major = GetPublicConstantValue(vrmVersionType, "MAJOR"); - int minor = GetPublicConstantValue(vrmVersionType, "MINOR"); - int patch = GetPublicConstantValue(vrmVersionType, "PATCH"); - - return (major, minor, patch); - } - else - { - return null; - } - } - - private static T GetPublicConstantValue(Type type, string constantName) - { - return (T)type.GetField(constantName, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).GetRawConstantValue(); - } - - static bool Quit() - { - var symbols = InitializeSymbols(); - PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, string.Join(";", symbols)); - - return true; - } - } -} -#endif diff --git a/Assets/ExternalPlugins/DVRSDK/Examples.meta b/Assets/ExternalPlugins/DVRSDK/Examples.meta deleted file mode 100644 index 0c00baa8..00000000 --- a/Assets/ExternalPlugins/DVRSDK/Examples.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 91bc369cca1daee43a9ebf805d99af28 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth.meta b/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth.meta deleted file mode 100644 index aa39c9fd..00000000 --- a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 3a9cdc8bd04606d49a37dd1c87005ed6 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts.meta b/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts.meta deleted file mode 100644 index b6f2a480..00000000 --- a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: e41644d8727f6fe42bb5f4ece61e53b1 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs b/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs deleted file mode 100644 index 46abaef8..00000000 --- a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using UnityEngine; - -namespace DVRSDK.Utilities -{ - [CreateAssetMenu(menuName = "DVRSDK/Create Configuration")] - public class SdkSettings : ScriptableObject - { - public string client_id; - } -} diff --git a/Assets/ExternalPlugins/VMC_Camera/Plugins/x86/VMC_CameraPlugin.dll b/Assets/ExternalPlugins/VMC_Camera/Plugins/x86/VMC_CameraPlugin.dll index 9376f858..d4c08b6e 100644 Binary files a/Assets/ExternalPlugins/VMC_Camera/Plugins/x86/VMC_CameraPlugin.dll and b/Assets/ExternalPlugins/VMC_Camera/Plugins/x86/VMC_CameraPlugin.dll differ diff --git a/Assets/ExternalPlugins/VMC_Camera/Plugins/x86_64/VMC_CameraPlugin.dll b/Assets/ExternalPlugins/VMC_Camera/Plugins/x86_64/VMC_CameraPlugin.dll index d57b9f73..c4c25374 100644 Binary files a/Assets/ExternalPlugins/VMC_Camera/Plugins/x86_64/VMC_CameraPlugin.dll and b/Assets/ExternalPlugins/VMC_Camera/Plugins/x86_64/VMC_CameraPlugin.dll differ diff --git a/Assets/ExternalPlugins/VirtualMotionTracker/VMTClient.cs b/Assets/ExternalPlugins/VirtualMotionTracker/VMTClient.cs index 206a60e5..ceeec404 100644 --- a/Assets/ExternalPlugins/VirtualMotionTracker/VMTClient.cs +++ b/Assets/ExternalPlugins/VirtualMotionTracker/VMTClient.cs @@ -1,4 +1,5 @@ //gpsnmeajp +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -21,6 +22,12 @@ void Start() SendRoomMatrixTemporary(); //とりあえず起動時にぶん投げておく } + private void SendToVmt(string address, params object[] values) + { + SendHook?.Invoke(address, values); + client.Send(address, values); + } + public int GetNo() { return vitrualTrackerNo; @@ -58,7 +65,7 @@ public void SendRoomMatrixTemporary() HmdMatrix34_t m = new HmdMatrix34_t(); OpenVR.ChaperoneSetup.GetWorkingStandingZeroPoseToRawTrackingPose(ref m); - client.Send("/VMT/SetRoomMatrix/Temporary", + SendToVmt("/VMT/SetRoomMatrix/Temporary", m.m0, m.m1, m.m2, m.m3, m.m4, m.m5, m.m6, m.m7, m.m8, m.m9, m.m10, m.m11); @@ -76,7 +83,7 @@ void Update() if (target != null) { //enable=1 - client.Send("/VMT/Room/Unity", (int)vitrualTrackerNo, (int)(enable ? 1 : 0), (float)0f, + SendToVmt("/VMT/Room/Unity", (int)vitrualTrackerNo, (int)(enable ? 1 : 0), (float)0f, (float)target.localPosition.x, (float)target.localPosition.y, (float)target.localPosition.z, @@ -91,7 +98,7 @@ void Update() private void disable() { //無効化処理 - client.Send("/VMT/Room/Unity", (int)vitrualTrackerNo, (int)0, (float)0f, + SendToVmt("/VMT/Room/Unity", (int)vitrualTrackerNo, (int)0, (float)0f, (float)0f, (float)0f, (float)0f, @@ -109,5 +116,12 @@ private void OnApplicationQuit() disable(); } } + + #region 自動テスト用フック + + /// 送信内容のキャプチャ用フック(通常の動作では誰も購読していない) + public static event Action SendHook; + + #endregion } } \ No newline at end of file diff --git a/Assets/Materials/Chroma.mat b/Assets/Materials/Chroma.mat index cb96560e..eeeb00e2 100644 --- a/Assets/Materials/Chroma.mat +++ b/Assets/Materials/Chroma.mat @@ -2,19 +2,24 @@ %TAG !u! tag:unity3d.com,2011: --- !u!21 &2100000 Material: - serializedVersion: 6 + serializedVersion: 8 m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} m_Name: Chroma m_Shader: {fileID: 4800000, guid: 9b5d23b59794b2e4ead81581306f58c1, type: 3} - m_ShaderKeywords: + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] m_LightmapFlags: 4 m_EnableInstancingVariants: 0 m_DoubleSidedGI: 0 m_CustomRenderQueue: -1 stringTagMap: {} disabledShaderPasses: [] + m_LockedProperties: m_SavedProperties: serializedVersion: 3 m_TexEnvs: @@ -74,6 +79,7 @@ Material: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} + m_Ints: [] m_Floats: - _BlendMode: 0 - _BumpScale: 1 @@ -111,3 +117,4 @@ Material: - _OutlineColor: {r: 0, g: 0, b: 0, a: 1} - _ShadeColor: {r: 0.96999997, g: 0.81, b: 0.86, a: 1} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Scenes/VirtualMotionCapture.unity b/Assets/Scenes/VirtualMotionCapture.unity index 1f7cc330..5599b520 100644 --- a/Assets/Scenes/VirtualMotionCapture.unity +++ b/Assets/Scenes/VirtualMotionCapture.unity @@ -38,12 +38,11 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.18028492, g: 0.22571525, b: 0.3069259, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -98,13 +97,13 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 1214600536} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 m_ObjectHideFlags: 0 m_BuildSettings: - serializedVersion: 2 + serializedVersion: 3 agentTypeID: 0 agentRadius: 0.5 agentHeight: 2 @@ -117,7 +116,9 @@ NavMeshSettings: cellSize: 0.16666667 manualTileSize: 0 tileSize: 256 - accuratePlacement: 0 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -147,79 +148,19 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1565122225} - {fileID: 2045526804} - {fileID: 1921040550} - {fileID: 1376895156} m_Father: {fileID: 814726122} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!1 &65266791 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 65266794} - - component: {fileID: 65266793} - - component: {fileID: 65266792} - m_Layer: 0 - m_Name: LipTracking - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &65266792 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 65266791} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 20b4e5c0c4847e8478b223793a2a5cd0, type: 3} - m_Name: - m_EditorClassIdentifier: - faceController: {fileID: 69806225} - controlWPFWindow: {fileID: 1407685704} ---- !u!114 &65266793 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 65266791} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 41f6d745d46210a49a0df9fe7d0d1102, type: 3} - m_Name: - m_EditorClassIdentifier: - EnableLip: 1 - EnableLipVersion: 1 ---- !u!4 &65266794 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 65266791} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 410353192} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &69806224 GameObject: m_ObjectHideFlags: 0 @@ -251,13 +192,12 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: EnableBlink: 0 - ViveProEyeEnabled: 0 + ExternalEyelidControlEnabled: 0 BlinkTimeMin: 1 BlinkTimeMax: 10 CloseAnimationTime: 0.06 OpenAnimationTime: 0.03 ClosingTime: 0.1 - BlendShapeClips: [] FacePresetName: --- !u!4 &69806226 Transform: @@ -266,12 +206,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 69806224} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -267.16342, y: -320.7936, z: -1107.1271} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1249928115} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &69806227 MonoBehaviour: @@ -325,12 +266,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 130023279} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 410353192} - m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &137141882 GameObject: @@ -357,12 +299,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 137141882} + serializedVersion: 2 m_LocalRotation: {x: -0.5, y: 0.5, z: 0.5, w: 0.5} m_LocalPosition: {x: 1, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1303375982} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: -90, y: 90, z: 0} --- !u!114 &137141884 MonoBehaviour: @@ -393,9 +336,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -582,12 +533,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 144144636} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -149.63986, y: -201.5098, z: -1171.8685} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1249928115} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &170076733 GameObject: @@ -665,6 +617,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &170076735 @@ -674,12 +627,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 170076733} + serializedVersion: 2 m_LocalRotation: {x: 0.76328206, y: -0.3904523, z: -0.024150606, w: 0.5141636} m_LocalPosition: {x: 0.1249, y: 1.5453, z: 0.262} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 130, y: 43, z: 75} --- !u!1 &210973285 GameObject: @@ -705,12 +659,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 210973285} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1962200053} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &210973287 MonoBehaviour: @@ -750,12 +705,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 241859925} + serializedVersion: 2 m_LocalRotation: {x: -0.5, y: 0.5, z: 0.5, w: 0.5} m_LocalPosition: {x: -1, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1303375982} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: -90, y: 90, z: 0} --- !u!114 &241859927 MonoBehaviour: @@ -786,9 +742,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -815,6 +779,52 @@ Camera: m_OcclusionCulling: 1 m_StereoConvergence: 10 m_StereoSeparation: 0.022 +--- !u!1 &277256230 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 277256231} + - component: {fileID: 277256232} + m_Layer: 0 + m_Name: MotionManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &277256231 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 277256230} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1177174446} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &277256232 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 277256230} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3fe96d37d49f9a24680cbb921ebbb8aa, type: 3} + m_Name: + m_EditorClassIdentifier: + controlWPFWindow: {fileID: 1407685704} + VirtualAvatars: [] --- !u!1 &282840810 GameObject: m_ObjectHideFlags: 0 @@ -839,12 +849,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 282840810} + serializedVersion: 2 m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} m_LocalPosition: {x: -1.311766, y: 0.8370192, z: 0.0127310455} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1128534204} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} --- !u!114 &282840816 MonoBehaviour: @@ -891,12 +902,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 283794701} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &283794703 MonoBehaviour: @@ -943,12 +955,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 286205153} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 7 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &286205155 MonoBehaviour: @@ -970,168 +983,7 @@ MonoBehaviour: BackCamera: {fileID: 462948917} PositionFixedCamera: {fileID: 680324446} ControlCamera: {fileID: 1420234149} ---- !u!1 &326004791 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 326004793} - - component: {fileID: 326004792} - - component: {fileID: 326004794} - - component: {fileID: 326004795} - m_Layer: 0 - m_Name: EyeTracking - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &326004792 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 326004791} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: c6fd25003140c0e47b39261ea2e0f470, type: 3} - m_Name: - m_EditorClassIdentifier: - MonitorPosition: {fileID: 0} - LookTarget: {fileID: 0} - StartPos: {x: 0, y: 0, z: 0} - ScaleX: 0.5 - ScaleY: 0.2 - OffsetX: 0 - OffsetY: 0 - CenterX: 0.5 - CenterY: 0.5 - Smoothing: 0.7 - controlWPFWindow: {fileID: 1407685704} - faceController: {fileID: 69806225} ---- !u!4 &326004793 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 326004791} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 410353192} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &326004794 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 326004791} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 1fd512d95b6700d44a5763b8c74b228a, type: 3} - m_Name: - m_EditorClassIdentifier: - MonitorPosition: {fileID: 0} - LookTarget: {fileID: 0} - StartPos: {x: 0, y: 0, z: 0} - ScaleX: 2 - ScaleY: 1.5 - OffsetX: 0 - OffsetY: 0 - CenterX: 0.5 - CenterY: 0.5 - Smoothing: 0.7 - controlWPFWindow: {fileID: 1407685704} - faceController: {fileID: 69806225} - UseEyelidMovements: 1 ---- !u!114 &326004795 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 326004791} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 47613961cde0a8b4aa4f79219b7e523c, type: 3} - m_Name: - m_EditorClassIdentifier: - EnableEye: 1 - EnableEyeDataCallback: 0 - EnableEyeVersion: 0 ---- !u!1 &389263777 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 389263780} - - component: {fileID: 389263779} - - component: {fileID: 389263778} - m_Layer: 0 - m_Name: ExternalReceiver - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &389263778 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 389263777} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ed41645692348410d84991648165334c, type: 3} - m_Name: - m_EditorClassIdentifier: - port: 39540 ---- !u!114 &389263779 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 389263777} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 5466e992f0e09e5489c93a853bf3b628, type: 3} - m_Name: - m_EditorClassIdentifier: - externalSender: {fileID: 1155637110} - MIDICCWrapper: {fileID: 1604201751} - receivePort: 39540 - statusString: - eddp: {fileID: 1549762413} - receiveBonesFlag: 0 - packets: 0 - BonePositionSynchronize: 1 ---- !u!4 &389263780 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 389263777} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 1210221202} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + CurrentLookTarget: {fileID: 0} --- !u!1 &410353191 GameObject: m_ObjectHideFlags: 0 @@ -1155,16 +1007,15 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 410353191} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 326004793} - - {fileID: 65266794} - {fileID: 483683250} - {fileID: 130023281} m_Father: {fileID: 0} - m_RootOrder: 12 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &462948916 GameObject: @@ -1211,12 +1062,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 462948916} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 1.3552814, y: 0.7924814, z: 0.07136068} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1128534204} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &483683248 GameObject: @@ -1247,7 +1099,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 49d7b96c4048b244085c84bc1c240b6e, type: 3} m_Name: m_EditorClassIdentifier: - handController: {fileID: 69806227} --- !u!4 &483683250 Transform: m_ObjectHideFlags: 0 @@ -1255,12 +1106,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 483683248} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 410353192} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &544322430 GameObject: @@ -1298,12 +1150,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 544322430} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1080383529} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &680324445 GameObject: @@ -1350,12 +1203,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 680324445} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 1.3552814, y: 0.7924814, z: 0.07136068} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1128534204} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &755659412 GameObject: @@ -1381,12 +1235,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 755659412} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 5 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &755659414 MonoBehaviour: @@ -1424,12 +1279,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 795349836} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &795349838 MonoBehaviour: @@ -1501,6 +1357,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &814726121 Canvas: m_ObjectHideFlags: 0 @@ -1518,7 +1375,9 @@ Canvas: m_OverrideSorting: 0 m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 m_SortingOrder: 0 m_TargetDisplay: 0 @@ -1532,10 +1391,10 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 54464775} m_Father: {fileID: 0} - m_RootOrder: 5 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -1566,12 +1425,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 836257371} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &836257373 MonoBehaviour: @@ -1612,9 +1472,17 @@ SphereCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 892957807} m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 m_IsTrigger: 0 + m_ProvidesContacts: 0 m_Enabled: 1 - serializedVersion: 2 + serializedVersion: 3 m_Radius: 0.5 m_Center: {x: 0, y: 0, z: 0} --- !u!23 &892957809 @@ -1628,10 +1496,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -1656,6 +1526,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &892957810 MeshFilter: m_ObjectHideFlags: 0 @@ -1671,12 +1542,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 892957807} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1000, y: 1000, z: 1000} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 4 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1044902095 GameObject: @@ -1710,12 +1582,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1044902095} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1080383528 GameObject: @@ -1740,15 +1613,16 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1080383528} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 544322432} - {fileID: 1549848323} - {fileID: 1596440256} m_Father: {fileID: 0} - m_RootOrder: 8 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1128534202 GameObject: @@ -1787,9 +1661,11 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1128534202} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 282840814} - {fileID: 680324450} @@ -1797,7 +1673,6 @@ Transform: - {fileID: 1404284883} - {fileID: 1420234143} m_Father: {fileID: 0} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1128534206 MonoBehaviour: @@ -1821,7 +1696,6 @@ GameObject: m_Component: - component: {fileID: 1155637111} - component: {fileID: 1155637110} - - component: {fileID: 1155637109} m_Layer: 0 m_Name: ExternalSender m_TagString: Untagged @@ -1829,20 +1703,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &1155637109 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1155637108} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d36dd9fcae25042bbb815255411ff524, type: 3} - m_Name: - m_EditorClassIdentifier: - address: 127.0.0.1 - port: 39539 --- !u!114 &1155637110 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1855,10 +1715,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: dc4032a37a493bf44b54ca33ea768822, type: 3} m_Name: m_EditorClassIdentifier: - uClient: {fileID: 0} + uClients: [] steamVR2Input: {fileID: 130023280} midiCCWrapper: {fileID: 1604201751} - externalReceiver: {fileID: 389263779} + externalReceiver: {fileID: 0} optionString: periodStatus: 1 periodRoot: 1 @@ -1874,12 +1734,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1155637108} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1210221202} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1177174445 GameObject: @@ -1904,9 +1765,11 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1177174445} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1044902097} - {fileID: 283794702} @@ -1916,8 +1779,9 @@ Transform: - {fileID: 755659413} - {fileID: 1218489916} - {fileID: 286205154} + - {fileID: 277256231} + - {fileID: 1451331751} m_Father: {fileID: 0} - m_RootOrder: 13 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1210221201 GameObject: @@ -1942,16 +1806,79 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1210221201} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1155637111} - - {fileID: 389263780} - - {fileID: 1571681380} m_Father: {fileID: 0} - m_RootOrder: 11 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!850595691 &1214600536 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 6 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 1 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVREnvironmentSampleCount: 512 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentImportanceSampling: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_PVRTiledBaking: 0 + m_NumRaysToShootPerTexel: -1 + m_RespectSceneVisibilityWhenBakingGI: 0 --- !u!1 &1218489915 GameObject: m_ObjectHideFlags: 0 @@ -1976,12 +1903,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1218489915} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 6 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1218489917 MonoBehaviour: @@ -2021,15 +1949,16 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1249928114} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 144144642} - {fileID: 69806226} - {fileID: 1793473875} m_Father: {fileID: 0} - m_RootOrder: 6 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1303375981 GameObject: @@ -2054,15 +1983,16 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1303375981} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 241859926} - {fileID: 137141883} - {fileID: 1312323655} m_Father: {fileID: 0} - m_RootOrder: 7 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1312323650 GameObject: @@ -2096,9 +2026,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -2132,12 +2070,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1312323650} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 1.3552814, y: 0.7924814, z: 0.07136068} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1303375982} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1312323656 MonoBehaviour: @@ -2177,13 +2116,14 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1350735252} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1770264196} m_Father: {fileID: 0} - m_RootOrder: 9 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1376895155 GameObject: @@ -2213,9 +2153,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 54464775} - m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.6666666, y: 0} m_AnchorMax: {x: 0.6666666, y: 1} @@ -2237,6 +2177,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2280,12 +2221,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1404284878} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 1.3552814, y: 0.7924814, z: 0.07136068} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1128534204} - m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1404284884 MonoBehaviour: @@ -2332,12 +2274,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1405842122} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1962200053} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1405842124 MonoBehaviour: @@ -2351,6 +2294,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 5922db7fa6394da4bb09ed9450b2e430, type: 3} m_Name: m_EditorClassIdentifier: + isDashboardActivated: 0 --- !u!1 &1407685703 GameObject: m_ObjectHideFlags: 0 @@ -2382,31 +2326,22 @@ MonoBehaviour: m_EditorClassIdentifier: IsBeta: 0 IsPreRelease: 0 - VersionString: v0.52f1r1b16 + VersionString: v0.61f1r1b4 LeftWristTransform: {fileID: 0} RightWristTransform: {fileID: 0} - CalibrationCamera: {fileID: 1312323656} BackgroundRenderer: {fileID: 892957809} GridCanvas: {fileID: 814726118} LipSync: {fileID: 144144637} faceController: {fileID: 69806225} - handController: {fileID: 69806227} - wristRotationFix: {fileID: 1793473874} - HandTrackerRoot: {fileID: 1128534204} - PelvisTrackerRoot: {fileID: 836257372} ExternalMotionSenderObject: {fileID: 1155637108} ExternalMotionReceiverObject: {fileID: 1210221201} externalMotionReceivers: [] + midiCCWrapper: {fileID: 1604201751} CriticalErrorCount: 0 + IsCriticalErrorCountOver: 0 vmtClient: {fileID: 1995326176} postProcessingManager: {fileID: 795349838} - EyeTracking_ViveProEyeComponent: {fileID: 0} - SRanipal_Eye_FrameworkComponent: {fileID: 0} - LipTracking_ViveComponent: {fileID: 0} - SRanipal_Lip_FrameworkComponent: {fileID: 0} midiCCBlendShape: {fileID: 1440382163} - calibrationState: 0 - lastCalibrateType: 0 lastLoadedConfigPath: easyDeviceDiscoveryProtocolManager: {fileID: 1549762413} modManager: {fileID: 755659414} @@ -2419,12 +2354,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1407685703} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: -1.3485818, y: 0.009173393, z: -1.6134627} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1420234142 GameObject: @@ -2454,12 +2390,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1420234142} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 1.3552814, y: 0.7924814, z: 0.07136068} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1128534204} - m_RootOrder: 4 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1420234144 MonoBehaviour: @@ -2577,9 +2514,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -2772,13 +2717,68 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1440382162} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1619438237} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1451331750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1451331751} + - component: {fileID: 1451331752} + m_Layer: 0 + m_Name: IKManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1451331751 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1451331750} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1177174446} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1451331752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1451331750} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 047ad6f77be3a2e4f9e9b4133f1fd63c, type: 3} + m_Name: + m_EditorClassIdentifier: + CalibrationState: 0 + LastCalibrateType: 0 + controlWPFWindow: {fileID: 1407685704} + HandController: {fileID: 69806227} + CalibrationCamera: {fileID: 1312323656} + wristRotationFix: {fileID: 1793473874} + HandTrackerRoot: {fileID: 1128534204} + PelvisTrackerRoot: {fileID: 836257372} + vrik: {fileID: 0} + generatedObject: {fileID: 1670762494} --- !u!1 &1549762411 GameObject: m_ObjectHideFlags: 0 @@ -2795,7 +2795,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!4 &1549762412 Transform: m_ObjectHideFlags: 0 @@ -2803,12 +2803,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1549762411} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 4 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1549762413 MonoBehaviour: @@ -2824,7 +2825,7 @@ MonoBehaviour: m_EditorClassIdentifier: window: {fileID: 1407685704} externalSender: {fileID: 1155637110} - externalReceiver: {fileID: 389263779} + externalReceiver: {fileID: 0} myname: Virtual Motion Capture found: 0 requesterEnable: 1 @@ -2866,12 +2867,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1549848321} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1080383529} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1565122224 GameObject: @@ -2901,9 +2903,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 54464775} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0.3333333} m_AnchorMax: {x: 1, y: 0.3333333} @@ -2925,6 +2927,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2944,71 +2947,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1565122224} m_CullTransparentMesh: 0 ---- !u!1 &1571681379 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1571681380} - - component: {fileID: 1571681382} - - component: {fileID: 1571681381} - m_Layer: 0 - m_Name: ExternalReceiver2 - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1571681380 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1571681379} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 1210221202} - m_RootOrder: 2 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &1571681381 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1571681379} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ed41645692348410d84991648165334c, type: 3} - m_Name: - m_EditorClassIdentifier: - port: 39540 ---- !u!114 &1571681382 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1571681379} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 5466e992f0e09e5489c93a853bf3b628, type: 3} - m_Name: - m_EditorClassIdentifier: - externalSender: {fileID: 1155637110} - MIDICCWrapper: {fileID: 1604201751} - receivePort: 39541 - statusString: - eddp: {fileID: 1549762413} - receiveBonesFlag: 0 - packets: 0 - BonePositionSynchronize: 1 --- !u!1 &1596440252 GameObject: m_ObjectHideFlags: 0 @@ -3036,9 +2974,17 @@ MeshCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1596440252} m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 m_IsTrigger: 0 + m_ProvidesContacts: 0 m_Enabled: 1 - serializedVersion: 4 + serializedVersion: 5 m_Convex: 0 m_CookingOptions: 30 m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} @@ -3053,10 +2999,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -3081,6 +3029,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1596440255 MeshFilter: m_ObjectHideFlags: 0 @@ -3096,12 +3045,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1596440252} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1080383529} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1604201750 GameObject: @@ -3269,12 +3219,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1604201750} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1619438237} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1619438236 GameObject: @@ -3299,14 +3250,46 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1619438236} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1604201752} - {fileID: 1440382164} m_Father: {fileID: 0} - m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1670762493 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1670762494} + m_Layer: 0 + m_Name: GeneratedObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1670762494 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1670762493} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1770264194 GameObject: @@ -3339,7 +3322,6 @@ MonoBehaviour: m_EditorClassIdentifier: controlWPFWindow: {fileID: 1407685704} modManager: {fileID: 755659414} - sdkConfiguration: {fileID: 11400000, guid: dd276808621df4e2380660ade912baf3, type: 2} --- !u!4 &1770264196 Transform: m_ObjectHideFlags: 0 @@ -3347,12 +3329,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1770264194} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1350735253} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1793473873 GameObject: @@ -3384,8 +3367,10 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: ik: {fileID: 0} - ElbowFixWeight: 0.5 - UpperArmFixWeight: 0.4 + controlWPFWindow: {fileID: 0} + UpperArmWeight: 0.2 + ForearmWeight: 0.57 + maxAccumulatedTwist: 300 --- !u!4 &1793473875 Transform: m_ObjectHideFlags: 0 @@ -3393,12 +3378,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1793473873} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1249928115} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1921040549 GameObject: @@ -3428,9 +3414,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 54464775} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.3333333, y: 0} m_AnchorMax: {x: 0.3333333, y: 1} @@ -3452,6 +3438,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -3494,14 +3481,15 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1962200052} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1405842123} - {fileID: 210973286} m_Father: {fileID: 0} - m_RootOrder: 14 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1995326174 GameObject: @@ -3530,12 +3518,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1995326174} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1177174446} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1995326176 MonoBehaviour: @@ -3618,9 +3607,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 54464775} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0.6666666} m_AnchorMax: {x: 1, y: 0.6666666} @@ -3642,6 +3631,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -3661,3 +3651,23 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2045526803} m_CullTransparentMesh: 0 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 170076735} + - {fileID: 1128534204} + - {fileID: 836257372} + - {fileID: 1407685705} + - {fileID: 892957811} + - {fileID: 814726122} + - {fileID: 1249928115} + - {fileID: 1303375982} + - {fileID: 1080383529} + - {fileID: 1350735253} + - {fileID: 1619438237} + - {fileID: 1210221202} + - {fileID: 410353192} + - {fileID: 1177174446} + - {fileID: 1962200053} + - {fileID: 1670762494} diff --git a/Assets/Scripts/AnimationController.cs b/Assets/Scripts/AnimationController.cs index 2ac75fd0..1b96333d 100644 --- a/Assets/Scripts/AnimationController.cs +++ b/Assets/Scripts/AnimationController.cs @@ -8,6 +8,7 @@ public class AnimationController public class AnimationItem { public float Time { get; set; } //アニメーションにかける時間 + public float StartTime { get; set; } //シーケンス先頭からこのアニメーションが始まるまでの時間 public float StartValue { get; set; } public float EndValue { get; set; } public System.Action SetAction { get; set; } @@ -29,26 +30,19 @@ public void Initialize() private bool isStart = false; private float startTime = 0.0f; + private int currentIndex = 0; //今実行中のアニメーションの位置 public System.Action ResetAction { get; set; } public List AnimationItems = new List(); - public Dictionary CurrentAnimationItems = new Dictionary(); //Key:開始時間 - - - private AnimationItem EndLastItem = null; - private AnimationItem CurrentItem = null; - private void InitializeAnimation() { - CurrentAnimationItems.Clear(); - EndLastItem = null; - CurrentItem = null; + currentIndex = 0; var starttime = 0.0f; foreach (var item in AnimationItems) { item.Initialize(); - CurrentAnimationItems.Add(starttime, item); + item.StartTime = starttime; starttime += item.Time == 0.0f ? 0.0001f : item.Time; } } @@ -82,62 +76,57 @@ public void ClearAnimations() public void StopAnimations() { isStart = false; - lastitem = null; + currentIndex = 0; } - private AnimationItem lastitem = null; - public bool Next() { if (isStart == false) { isStart = true; - startTime = Time.time; + startTime = CurrentTime; InitializeAnimation(); } - var elapsedTime = Time.time - startTime; - var addTime = 0.0f; - foreach (var item in CurrentAnimationItems) + var elapsedTime = CurrentTime - startTime; + + //処理落ちで飛び越したアニメーションは、順番に終了値を適用してから先へ進む。 + //飛ばしたままにすると中間状態(まばたきなら目を閉じたまま)で固まってしまう + while (currentIndex < AnimationItems.Count) { - addTime = item.Key + item.Value.Time; //すべてのアニメーションの時間+今のアニメーション時間 - if (addTime >= elapsedTime) - {//経過時間がまだアニメーションの終了時間に届いていない間(アニメーション中) - //Debug.Log($"AnimationTime:{elapsedTime}"); - if (lastitem != null && EndLastItem != lastitem) - {//前回のアニメーションが終わりまで行ってない場合があるので100%で実行 - lastitem.RunAction(lastitem.EndValue); - EndLastItem = lastitem; - } - if (CurrentItem != item.Value) - { - if (lastitem != null) - {//前回のアニメーションが終わりまで行ってない場合があるので100%で実行 - lastitem.RunAction(lastitem.EndValue); - EndLastItem = lastitem; - } - //新しいアニメーションになったときには時間にかかわらずきちんと最初の値を使う - item.Value.RunAction(item.Value.StartValue); - CurrentItem = item.Value; - } - else - { - var currentTime = item.Value.Time + (elapsedTime - addTime); - var setvalue = item.Value.StartValue + (item.Value.EndValue - item.Value.StartValue) * (currentTime / item.Value.Time); - item.Value.RunAction(setvalue); - } - lastitem = item.Value; - return true; - } + var skipItem = AnimationItems[currentIndex]; + if (skipItem.StartTime + skipItem.Time >= elapsedTime) break; + skipItem.RunAction(skipItem.EndValue); + currentIndex++; } - //最後までアニメーションしたとき - if (lastitem != null) - {//最後のアニメーションが終わりまで行ってない場合があるので100%で実行 - lastitem.RunAction(lastitem.EndValue); + //最後まで到達したとき。上のループですべてのアニメーションが終了値まで進んでいるので、 + //どれだけ処理落ちしても最終状態(まばたきなら目を開いた状態)で終わる + if (currentIndex >= AnimationItems.Count) + { + isStart = false; + currentIndex = 0; + return false; } - isStart = false; - return false; + + //処理落ちしていても、その時点の経過時間に対応する値を適用する + //(先頭の値に戻すと、飛び越した分だけアニメーションが巻き戻ってしまう) + var item = AnimationItems[currentIndex]; + var rate = item.Time > 0.0f ? Mathf.Clamp01((elapsedTime - item.StartTime) / item.Time) : 1.0f; + item.RunAction(item.StartValue + (item.EndValue - item.StartValue) * rate); + return true; } + + #region 自動テスト用フック + + /// + /// 現在時刻の取得元。処理落ち(フレーム落ち)を決定論的に再現するために自動テストから差し替える。 + /// 通常の動作ではnullで、Time.realtimeSinceStartupが使われる。 + /// + internal static System.Func TestTimeProvider = null; + + private static float CurrentTime => TestTimeProvider != null ? TestTimeProvider() : Time.realtimeSinceStartup; + + #endregion } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Avatar/BoneBendGoal.cs b/Assets/Scripts/Avatar/BoneBendGoal.cs new file mode 100644 index 00000000..d8741b98 --- /dev/null +++ b/Assets/Scripts/Avatar/BoneBendGoal.cs @@ -0,0 +1,65 @@ +using RootMotion.FinalIK; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using VMC; + +public class BoneBendGoal : MonoBehaviour +{ + public string Name; + + public Transform UpperBone; // UpperArm等 + public Transform LowerBone; // LowerArm等 + public Transform EndBone; // Hand等 + public Transform BendGoal; + + private Guid eventId; + + public void Start() + { + if (Name == null) Name = name; + eventId = IKManager.Instance.AddOnPostUpdate(1, OnPostUpdate); + } + + public void SetBones(string name, Transform upperBone, Transform lowerBone, Transform endBone, Transform bendGoal) + { + Name = name; + UpperBone = upperBone; + LowerBone = lowerBone; + EndBone = endBone; + BendGoal = bendGoal; + } + + void OnDestroy() + { + IKManager.Instance.RemoveOnPostUpdate(eventId); + } + + private void OnPostUpdate() + { + if (enabled == false) return; + if (IKManager.Instance.vrik == null) return; + + Quaternion currentEndBoneRotation = EndBone.rotation; + + // 回転軸 + Vector3 bendAxis = (EndBone.position - UpperBone.position); + //LowerBoneから回転軸までの垂線 + Vector3 lowerBonePerpendicularAxis = PerpendicularAxis(UpperBone.position, EndBone.position, LowerBone.position); + //BendGoalから回転軸までの垂線 + Vector3 bendGoalPerpendicularAxis = PerpendicularAxis(UpperBone.position, EndBone.position, BendGoal.position); + + //二つの垂線間の角度 + float angle = Vector3.SignedAngle(lowerBonePerpendicularAxis, bendGoalPerpendicularAxis, bendAxis); + + UpperBone.Rotate(bendAxis, angle, Space.World); + EndBone.rotation = currentEndBoneRotation; + } + + // 線分ABまでの点Pからの垂線ベクトルを取得 + private Vector3 PerpendicularAxis(Vector3 a, Vector3 b, Vector3 p) + { + return a + Vector3.Project(p - a, b - a) - p; + } +} \ No newline at end of file diff --git a/Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs.meta b/Assets/Scripts/Avatar/BoneBendGoal.cs.meta similarity index 83% rename from Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs.meta rename to Assets/Scripts/Avatar/BoneBendGoal.cs.meta index d549aed5..0fdb145d 100644 --- a/Assets/ExternalPlugins/DVRSDK/Editor/PluginVersionChecker.cs.meta +++ b/Assets/Scripts/Avatar/BoneBendGoal.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 82ee960a678fa004d91a5e029a6b0747 +guid: 2b025194f1e77af43ba5a87f11d10807 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Avatar/Calibrator.cs b/Assets/Scripts/Avatar/Calibrator.cs index d21f175b..e16491c0 100644 --- a/Assets/Scripts/Avatar/Calibrator.cs +++ b/Assets/Scripts/Avatar/Calibrator.cs @@ -29,13 +29,13 @@ public static void Calibrate(VRIK ik, Settings settings, Transform HMDTransform, { if (!ik.solver.initiated) { - Debug.LogError("Can not calibrate before VRIK has initiated."); + Debug.LogError("[Calib Fail] Can not calibrate before VRIK has initiated."); return; } if (HMDTransform == null) { - Debug.LogError("Can not calibrate VRIK without the head tracker."); + Debug.LogError("[Calib Fail] Can not calibrate VRIK without the head tracker."); return; } @@ -212,7 +212,7 @@ private static Transform CalibrateLeg(VRIKCalibrator.Settings settings, Transfor //leg.bendGoal = null; //leg.bendGoalWeight = 0f; } - public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null) + public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null, TrackingPoint ChestTrackingPoint = null) { Transform HMDTransform = HMDTrackingPoint?.TargetTransform; Transform PelvisTransform = PelvisTrackingPoint?.TargetTransform; @@ -224,17 +224,18 @@ public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform f Transform RightElbowTransform = RightElbowTrackingPoint?.TargetTransform; Transform LeftKneeTransform = LeftKneeTrackingPoint?.TargetTransform; Transform RightKneeTransform = RightKneeTrackingPoint?.TargetTransform; + Transform ChestTransform = ChestTrackingPoint?.TargetTransform; if (!ik.solver.initiated) { - Debug.LogError("Can not calibrate before VRIK has initiated."); + Debug.LogError("[Calib Fail] Can not calibrate before VRIK has initiated."); yield break; } if (HMDTransform == null) { - Debug.LogError("Can not calibrate VRIK without the head tracker."); + Debug.LogError("[Calib Fail] Can not calibrate VRIK without the head tracker."); yield break; } @@ -259,6 +260,7 @@ public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform f if (RightElbowTransform != null) RightElbowTransform.parent = handTrackerRoot; if (LeftKneeTransform != null) LeftKneeTransform.parent = footTrackerRoot; if (RightKneeTransform != null) RightKneeTransform.parent = footTrackerRoot; + if (ChestTransform != null) ChestTransform.parent = handTrackerRoot; HandSwapManagerScript handSwapManagerScript = GameObject.Find("HandSwapManager").GetComponent(); handSwapManagerScript.WPFLeftHandTransform = LeftHandTransform; //左手登録 @@ -428,6 +430,18 @@ public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform f yield return new WaitForEndOfFrame(); + // Chest + if (ChestTransform != null && ik.references.chest != null) + { + var chestBone = ik.references.chest; + var bendGoal = new GameObject("ChestBendGoal").transform; + bendGoal.parent = ChestTransform; + bendGoal.position = chestBone.position + ik.references.root.forward * 0.5f; + ik.solver.spine.chestGoal = bendGoal; + ik.solver.spine.chestGoalWeight = 1.0f; + ik.solver.spine.chestClampWeight = 0f; + } + // Left Hand if (LeftHandTransform != null) { @@ -607,7 +621,7 @@ public static IEnumerator CalibrateScaled(Transform handTrackerRoot, Transform f ik.solver.locomotion.weight = PelvisTransform == null && LeftFootTransform == null && RightFootTransform == null ? 1f : 0f; } - public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null) + public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null, TrackingPoint ChestTrackingPoint = null) { Transform HMDTransform = HMDTrackingPoint?.TargetTransform; Transform PelvisTransform = PelvisTrackingPoint?.TargetTransform; @@ -619,17 +633,18 @@ public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transfor Transform RightElbowTransform = RightElbowTrackingPoint?.TargetTransform; Transform LeftKneeTransform = LeftKneeTrackingPoint?.TargetTransform; Transform RightKneeTransform = RightKneeTrackingPoint?.TargetTransform; + Transform ChestTransform = ChestTrackingPoint?.TargetTransform; if (!ik.solver.initiated) { - Debug.LogError("Can not calibrate before VRIK has initiated."); + Debug.LogError("[Calib Fail] Can not calibrate before VRIK has initiated."); yield break; } if (HMDTransform == null) { - Debug.LogError("Can not calibrate VRIK without the head tracker."); + Debug.LogError("[Calib Fail] Can not calibrate VRIK without the head tracker."); yield break; } @@ -658,6 +673,7 @@ public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transfor if (RightElbowTransform != null) RightElbowTransform.parent = handTrackerRoot; if (LeftKneeTransform != null) LeftKneeTransform.parent = footTrackerRoot; if (RightKneeTransform != null) RightKneeTransform.parent = footTrackerRoot; + if (ChestTransform != null) ChestTransform.parent = handTrackerRoot; //コントローラーの場合手首までのオフセットを追加 if (LeftHandOffset == Vector3.zero) @@ -813,6 +829,18 @@ public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transfor yield return new WaitForEndOfFrame(); + // Chest + if (ChestTransform != null && ik.references.chest != null) + { + var chestBone = ik.references.chest; + var bendGoal = new GameObject("ChestBendGoal").transform; + bendGoal.parent = ChestTransform; + bendGoal.position = chestBone.position + ik.references.root.forward * 0.5f; + ik.solver.spine.chestGoal = bendGoal; + ik.solver.spine.chestGoalWeight = 1.0f; + ik.solver.spine.chestClampWeight = 0f; + } + // Left Hand if (LeftHandTransform != null) { @@ -1004,7 +1032,7 @@ public static IEnumerator CalibrateFixedHand(Transform handTrackerRoot, Transfor ik.solver.locomotion.weight = PelvisTransform == null && LeftFootTransform == null && RightFootTransform == null ? 1f : 0f; } - public static IEnumerator CalibrateFixedHandWithGround(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null) + public static IEnumerator CalibrateFixedHandWithGround(Transform handTrackerRoot, Transform footTrackerRoot, VRIK ik, VRIKCalibrator.Settings settings, Vector3 LeftHandOffset, Vector3 RightHandOffset, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null, TrackingPoint ChestTrackingPoint = null) { Transform HMDTransform = HMDTrackingPoint?.TargetTransform; Transform PelvisTransform = PelvisTrackingPoint?.TargetTransform; @@ -1016,17 +1044,18 @@ public static IEnumerator CalibrateFixedHandWithGround(Transform handTrackerRoot Transform RightElbowTransform = RightElbowTrackingPoint?.TargetTransform; Transform LeftKneeTransform = LeftKneeTrackingPoint?.TargetTransform; Transform RightKneeTransform = RightKneeTrackingPoint?.TargetTransform; + Transform ChestTransform = ChestTrackingPoint?.TargetTransform; if (!ik.solver.initiated) { - Debug.LogError("Can not calibrate before VRIK has initiated."); + Debug.LogError("[Calib Fail] Can not calibrate before VRIK has initiated."); yield break; } if (HMDTransform == null) { - Debug.LogError("Can not calibrate VRIK without the head tracker."); + Debug.LogError("[Calib Fail] Can not calibrate VRIK without the head tracker."); yield break; } @@ -1055,6 +1084,7 @@ public static IEnumerator CalibrateFixedHandWithGround(Transform handTrackerRoot if (RightElbowTransform != null) RightElbowTransform.parent = handTrackerRoot; if (LeftKneeTransform != null) LeftKneeTransform.parent = footTrackerRoot; if (RightKneeTransform != null) RightKneeTransform.parent = footTrackerRoot; + if (ChestTransform != null) ChestTransform.parent = handTrackerRoot; //コントローラーの場合手首までのオフセットを追加 if (LeftHandOffset == Vector3.zero) @@ -1210,6 +1240,18 @@ public static IEnumerator CalibrateFixedHandWithGround(Transform handTrackerRoot yield return new WaitForEndOfFrame(); + // Chest + if (ChestTransform != null && ik.references.chest != null) + { + var chestBone = ik.references.chest; + var bendGoal = new GameObject("ChestBendGoal").transform; + bendGoal.parent = ChestTransform; + bendGoal.position = chestBone.position + ik.references.root.forward * 0.5f; + ik.solver.spine.chestGoal = bendGoal; + ik.solver.spine.chestGoalWeight = 1.0f; + ik.solver.spine.chestClampWeight = 0f; + } + // Left Hand if (LeftHandTransform != null) { diff --git a/Assets/Scripts/Avatar/DynamicOVRLipSync.cs b/Assets/Scripts/Avatar/DynamicOVRLipSync.cs index bcc35f82..d9b87000 100644 --- a/Assets/Scripts/Avatar/DynamicOVRLipSync.cs +++ b/Assets/Scripts/Avatar/DynamicOVRLipSync.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using UnityEngine; -using VRM; +using UniVRM10; namespace VMC { @@ -21,7 +21,6 @@ public class DynamicOVRLipSync : OVRLipSyncContextBase // smoothing amount public int SmoothAmount = 100; - private GameObject VRMmodel; public bool EnableLipSync = false; @@ -40,6 +39,46 @@ public class DynamicOVRLipSync : OVRLipSyncContextBase public string selectedDevice = null; + /// + /// visemeの重みをしきい値・強調・最大値のみ・MaxLevelの順で加工して表情へ反映する。 + /// マイク入力の取得とは切り離してある。 + /// + private void ApplyVisemes(ExpressionPreset[] presets, float[] visemes) + { + if (faceController == null) return; + + int maxindex = 0; + float maxvisemes = 0; + for (int i = 0; i < presets.Length; i++) + { + if (visemes[i] < WeightThreashold) visemes[i] = 0; + if (maxvisemes < visemes[i]) + { + maxindex = i; + maxvisemes = visemes[i]; + } + } + + if (MaxWeightEmphasis) + { + visemes[maxindex] = Mathf.Clamp(visemes[maxindex] * 3, 0.0f, 1.0f); + } + + if (MaxWeightEnable) + { + for (int i = 0; i < presets.Length; i++) + { + if (i != maxindex) visemes[i] = 0.0f; + } + } + + for (int i = 0; i < presets.Length; i++) + { + visemes[i] *= MaxLevel; + } + faceController.MixPresets(nameof(DynamicOVRLipSync), presets, visemes); + } + public string[] GetMicrophoneDevices() => Microphone.devices; public void SetMicrophoneDevice(string device) { @@ -51,11 +90,6 @@ public void SetMicrophoneDevice(string device) micSelected = true; } - public void ImportVRMmodel(GameObject vrmmodel) - { - VRMmodel = vrmmodel; - } - // Use this for initialization void Start() { @@ -89,56 +123,27 @@ void Update() OVRLipSync.Frame frame = GetCurrentPhonemeFrame(); if (frame != null) { - //あ OVRLipSync.Viseme.aa; BlendShapePreset.A; - //い OVRLipSync.Viseme.ih; BlendShapePreset.I; - //う OVRLipSync.Viseme.ou; BlendShapePreset.U; - //え OVRLipSync.Viseme.E; BlendShapePreset.E; - //お OVRLipSync.Viseme.oh; BlendShapePreset.O; - var presets = new BlendShapePreset[] { - BlendShapePreset.A, - BlendShapePreset.I, - BlendShapePreset.U, - BlendShapePreset.E, - BlendShapePreset.O, - }; + //あ OVRLipSync.Viseme.aa; ExpressionPreset.aa; + //い OVRLipSync.Viseme.ih; ExpressionPreset.ih; + //う OVRLipSync.Viseme.ou; ExpressionPreset.ou; + //え OVRLipSync.Viseme.E; ExpressionPreset.ee; + //お OVRLipSync.Viseme.oh; ExpressionPreset.oh; + var presets = new ExpressionPreset[] { + ExpressionPreset.aa, + ExpressionPreset.ih, + ExpressionPreset.ou, + ExpressionPreset.ee, + ExpressionPreset.oh, + }; var visemes = new float[] { - frame.Visemes[(int)OVRLipSync.Viseme.aa], - frame.Visemes[(int)OVRLipSync.Viseme.ih], - frame.Visemes[(int)OVRLipSync.Viseme.ou], - frame.Visemes[(int)OVRLipSync.Viseme.E], - frame.Visemes[(int)OVRLipSync.Viseme.oh], - }; - - int maxindex = 0; - float maxvisemes = 0; - for (int i = 0; i < presets.Length; i++) - { - if (visemes[i] < WeightThreashold) visemes[i] = 0; - if (maxvisemes < visemes[i]) - { - maxindex = i; - maxvisemes = visemes[i]; - } - } + frame.Visemes[(int)OVRLipSync.Viseme.aa], + frame.Visemes[(int)OVRLipSync.Viseme.ih], + frame.Visemes[(int)OVRLipSync.Viseme.ou], + frame.Visemes[(int)OVRLipSync.Viseme.E], + frame.Visemes[(int)OVRLipSync.Viseme.oh], + }; - if (MaxWeightEmphasis) - { - visemes[maxindex] = Mathf.Clamp(visemes[maxindex] * 3, 0.0f, 1.0f); - } - - if (MaxWeightEnable) - { - for (int i = 0; i < presets.Length; i++) - { - if (i != maxindex) visemes[i] = 0.0f; - } - } - - for (int i = 0; i < presets.Length; i++) - { - visemes[i] *= MaxLevel; - } - faceController.MixPresets(nameof(DynamicOVRLipSync), presets, visemes); + ApplyVisemes(presets, visemes); //Debug.Log("Visemes:" + string.Join(",", frame.Visemes.Select(d => d.ToString()))); } @@ -284,5 +289,20 @@ void OnDisable() { StopMicrophone(); } + + #region 自動テスト用フック + + /// マイク入力の代わりにvisemeを直接与える。あ・い・う・え・お の順 + internal void Test_ApplyVisemes(float aa, float ih, float ou, float ee, float oh) + { + var presets = new[] + { + ExpressionPreset.aa, ExpressionPreset.ih, ExpressionPreset.ou, + ExpressionPreset.ee, ExpressionPreset.oh, + }; + ApplyVisemes(presets, new[] { aa, ih, ou, ee, oh }); + } + + #endregion } } \ No newline at end of file diff --git a/Assets/Scripts/Avatar/EyeTracking.meta b/Assets/Scripts/Avatar/EyeTracking.meta deleted file mode 100644 index 88970c45..00000000 --- a/Assets/Scripts/Avatar/EyeTracking.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 989bd0f0a453b334cba60c1d839db73a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs b/Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs deleted file mode 100644 index b61ba94c..00000000 --- a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using Tobii.Gaming; -using UnityEngine; -using UnityMemoryMappedFile; - -namespace VMC -{ - public class EyeTracking_Tobii : MonoBehaviour - { - - public GameObject MonitorPosition; - public GameObject LookTarget; - public Vector3 StartPos; - - public float ScaleX = 0.5f; - public float ScaleY = 0.2f; - public float OffsetX = 0.0f; - public float OffsetY = 0.0f; - public float CenterX = 0.5f; - public float CenterY = 0.5f; - public float Smoothing = 0.7f; - private Vector3 oldPoint; - private bool isFirst = true; - public ControlWPFWindow controlWPFWindow; - public FaceController faceController; - private Action faceBeforeApply = null; - private bool isValidPosition = false; - - // Use this for initialization - void Start() - { - VMCEvents.OnModelLoaded += ModelLoaded; - controlWPFWindow.SetEyeTracking_TobiiOffsetsAction += SetEyeTracking_TobiiOffsets; - controlWPFWindow.EyeTracking_TobiiCalibrationAction += EyeTracking_TobiiCalibration; - } - - private void ModelLoaded(GameObject currentModel) - { - Calibration(currentModel, true); - } - - private void SetEyeTracking_TobiiOffsets(PipeCommands.SetEyeTracking_TobiiOffsets offsets) - { - ScaleX = offsets.ScaleHorizontal; - ScaleY = offsets.ScaleVertical; - OffsetX = offsets.OffsetHorizontal; - OffsetY = offsets.OffsetVertical; - } - - private void EyeTracking_TobiiCalibration(GameObject currentModel) - { - Calibration(currentModel, false); - } - - private void Calibration(GameObject currentModel, bool fromSetting) - { - if (currentModel == null) return; - if (TobiiAPI.IsConnected == false) return; - var animator = currentModel.GetComponent(); - var head = animator.GetBoneTransform(HumanBodyBones.Head); - //モデルの頭の前方50cm地点にモニターがあることにする - if (MonitorPosition == null) MonitorPosition = new GameObject("Tobii_MonitorPosition"); - MonitorPosition.transform.parent = null; - if (fromSetting) - { - var centerPos = controlWPFWindow.GetEyeTracking_TobiiLocalPosition(MonitorPosition.transform); - CenterX = centerPos.x; - CenterY = centerPos.y; - } - else - { - MonitorPosition.transform.position = head.position + head.forward * 0.5f; //頭の前方50cm - MonitorPosition.transform.rotation = head.rotation; - var gazePoint = GazeViewportToMonitorViewport(TobiiAPI.GetGazePoint().Viewport); - CenterX = gazePoint.x; - CenterY = gazePoint.y; - controlWPFWindow.SetEyeTracking_TobiiPosition(MonitorPosition.transform, CenterX, CenterY); - } - if (LookTarget == null) LookTarget = new GameObject("LookTarget"); - LookTarget.transform.parent = MonitorPosition.transform; - LookTarget.transform.localRotation = Quaternion.identity; - LookTarget.transform.localPosition = new Vector3(0, 0, 0f); - var vrmLookAtHead = currentModel.GetComponent(); - if (faceBeforeApply != null) faceController.BeforeApply -= faceBeforeApply; - faceBeforeApply = () => - { - if (LookTarget == null) return; - if (vrmLookAtHead.Head == null) return; - if (isValidPosition == false) return; - vrmLookAtHead.Target = LookTarget.transform; - vrmLookAtHead.LookWorldPosition(); - vrmLookAtHead.Target = null; - }; - faceController.BeforeApply += faceBeforeApply; - StartPos = LookTarget.transform.localPosition; - isFirst = true; - } - - //viewportはウインドウ左下基準0~1.0 - private Vector2 GazeViewportToMonitorViewport(Vector2 viewport) - { - var monitorw = Screen.currentResolution.width; - var monitorh = Screen.currentResolution.height; - var windowrect = NativeMethods.GetUnityWindowPosition(); - var winx = windowrect.left; - var winbottom = windowrect.bottom; - var winw = windowrect.right - windowrect.left; - var winh = windowrect.bottom - windowrect.top; - var clientw = Screen.width; - var clienth = Screen.height; - var borderw = (winw - clientw) / 2; - var titleh = winh - borderw - clienth; - var clientx = winx + borderw; - var clientbottom = (monitorh - winbottom) + borderw; - var tmpx = clientw * viewport.x; - var tmpy = clienth * viewport.y; - var realx = tmpx + clientx; - var realy = tmpy + clientbottom; - var viewportx = realx / monitorw; - var viewporty = realy / monitorh; - return new Vector2(viewportx, viewporty); - } - - // Update is called once per frame - void Update() - { - if (TobiiAPI.IsConnected && LookTarget != null && MonitorPosition != null) - { - //var headPose = TobiiAPI.GetHeadPose(); - //if (headPose.IsRecent()) - //{ - // MonitorPosition.transform.localRotation = Quaternion.Lerp(MonitorPosition.transform.localRotation, Quaternion.Inverse(headPose.Rotation), Time.unscaledDeltaTime * 10f); - //} - - var gazePoint = TobiiAPI.GetGazePoint(); - isValidPosition = gazePoint.IsValid; - if (isValidPosition) - { - var gazePointToMonitor = GazeViewportToMonitorViewport(TobiiAPI.GetGazePoint().Viewport); - - Vector3 gazePointInWorld = new Vector3(StartPos.x + ((gazePointToMonitor.x - CenterX) * ScaleX) + OffsetX, StartPos.y + ((gazePointToMonitor.y - CenterY) * ScaleY) + OffsetY, StartPos.z); - LookTarget.transform.localPosition = Smoothify(gazePointInWorld); - } - } - } - private Vector3 Smoothify(Vector3 point) - { - if (isFirst) - { - oldPoint = point; - isFirst = false; - } - - var smoothedPoint = new Vector3( - point.x * (1.0f - Smoothing) + oldPoint.x * Smoothing, - point.y * (1.0f - Smoothing) + oldPoint.y * Smoothing, - point.z); - - oldPoint = smoothedPoint; - - return smoothedPoint; - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs b/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs deleted file mode 100644 index 29e67630..00000000 --- a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityMemoryMappedFile; -using ViveSR.anipal.Eye; - -namespace VMC -{ - public class EyeTracking_ViveProEye : MonoBehaviour - { - - public GameObject MonitorPosition; - public GameObject LookTarget; - public Vector3 StartPos; - - public float ScaleX = 2.0f; - public float ScaleY = 1.5f; - public float OffsetX = 0.0f; - public float OffsetY = 0.0f; - public float CenterX = 0.5f; - public float CenterY = 0.5f; - public float Smoothing = 0.7f; - private Vector3 oldPoint; - private bool isFirst = true; - public ControlWPFWindow controlWPFWindow; - public FaceController faceController; - private Action faceBeforeApply; - public bool UseEyelidMovements = true; - - private GameObject currentModel; - - private Dictionary EyeWeightings = new Dictionary(); - - // Use this for initialization - void Awake() - { - VMCEvents.OnModelLoaded += ModelLoaded; - controlWPFWindow.SetEyeTracking_ViveProEyeOffsetsAction += SetEyeTracking_ViveProEyeOffsets; - controlWPFWindow.SetEyeTracking_ViveProEyeUseEyelidMovementsAction += SetEyeTracking_ViveProEyeUseEyelidMovements; - controlWPFWindow.EyeTracking_ViveProEyeComponent = this; - controlWPFWindow.SRanipal_Eye_FrameworkComponent = GetComponent(); - enabled = false; - } - - private void ModelLoaded(GameObject currentModel) - { - ModelInitialize(currentModel); - } - - private void SetEyeTracking_ViveProEyeOffsets(PipeCommands.SetEyeTracking_ViveProEyeOffsets offsets) - { - ScaleX = offsets.ScaleHorizontal; - ScaleY = offsets.ScaleVertical; - OffsetX = offsets.OffsetHorizontal; - OffsetY = offsets.OffsetVertical; - if (MonitorPosition != null) - { - MonitorPosition.transform.localScale = new Vector3(ScaleX, ScaleY, 1); - MonitorPosition.transform.localPosition = new Vector3(OffsetX, OffsetY, 0); - } - } - - private void SetEyeTracking_ViveProEyeUseEyelidMovements(PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements useEyelidMovements) - { - UseEyelidMovements = useEyelidMovements.Use; - if (UseEyelidMovements == false) - { - faceController.SetBlink_L(0.0f); - faceController.SetBlink_R(0.0f); - } - faceController.ViveProEyeEnabled = UseEyelidMovements; - } - - private void ModelInitialize(GameObject currentModel) - { - if (currentModel == null) return; - if (this.currentModel == currentModel) return; - this.currentModel = currentModel; - var animator = currentModel.GetComponent(); - var head = animator.GetBoneTransform(HumanBodyBones.Head); - //モデルの頭の子に目線向ける先を設定 - if (MonitorPosition == null) MonitorPosition = new GameObject("ViveProEye_MonitorPosition"); - MonitorPosition.transform.parent = head; - MonitorPosition.transform.localRotation = Quaternion.identity; - MonitorPosition.transform.localScale = new Vector3(ScaleX, ScaleY, 1); - MonitorPosition.transform.localPosition = new Vector3(OffsetX, OffsetY, 0); - if (LookTarget == null) LookTarget = new GameObject("LookTarget"); - LookTarget.transform.parent = MonitorPosition.transform; - LookTarget.transform.localRotation = Quaternion.identity; - LookTarget.transform.localPosition = new Vector3(0, 0, 1f); //すべて0地点にすると目が荒ぶる - var vrmLookAtHead = currentModel.GetComponent(); - if (faceBeforeApply != null) faceController.BeforeApply -= faceBeforeApply; - faceBeforeApply = () => - { - if ((SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING && - SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) || - SRanipal_Eye_Framework.Status == SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT || enabled == false) return; - vrmLookAtHead.Target = LookTarget.transform; - vrmLookAtHead.LookWorldPosition(); - vrmLookAtHead.Target = null; - }; - faceController.BeforeApply += faceBeforeApply; - StartPos = LookTarget.transform.localPosition; - isFirst = true; - } - - // Update is called once per frame - void Update() - { - if (Camera.main == null) return; - - if ((SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING && - SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) || - SRanipal_Eye_Framework.Status == SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) return; - - //まぶた - bool isLeftEyeActive = false; - bool isRightEyeActive = false; - float leftEyeOpenness = 1.0f; - float rightEyeOpenness = 1.0f; - if (SRanipal_Eye_Framework.Status == SRanipal_Eye_Framework.FrameworkStatus.WORKING) - { - isLeftEyeActive = SRanipal_Eye.GetEyeOpenness(EyeIndex.LEFT, out leftEyeOpenness); - isRightEyeActive = SRanipal_Eye.GetEyeOpenness(EyeIndex.RIGHT, out rightEyeOpenness); - } - - if (isLeftEyeActive || isRightEyeActive) - { - EyeWeightings[EyeShape.Eye_Left_Blink] = 1 - leftEyeOpenness; - EyeWeightings[EyeShape.Eye_Right_Blink] = 1 - rightEyeOpenness; - UpdateEyeShapes(EyeWeightings); - } - else - { - for (int i = 0; i < (int)EyeShape.Max; ++i) - { - bool isBlink = ((EyeShape)i == EyeShape.Eye_Left_Blink || (EyeShape)i == EyeShape.Eye_Right_Blink); - EyeWeightings[(EyeShape)i] = isBlink ? 1 : 0; - } - - UpdateEyeShapes(EyeWeightings); - - return; - } - - //目線 - Vector3 GazeOriginCombinedLocal, GazeDirectionCombinedLocal = Vector3.zero; - if (SRanipal_Eye.GetGazeRay(GazeIndex.COMBINE, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { } - else if (SRanipal_Eye.GetGazeRay(GazeIndex.LEFT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { } - else if (SRanipal_Eye.GetGazeRay(GazeIndex.RIGHT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { } - if (LookTarget != null) LookTarget.transform.localPosition = Smoothify(GazeDirectionCombinedLocal); - - } - public void UpdateEyeShapes(Dictionary eyeWeightings) - { - if (UseEyelidMovements == false) return; - if (LookTarget == null) return; - foreach (var weightings in eyeWeightings) - { - EyeShape eyeShape = weightings.Key; - - if (eyeShape == EyeShape.Eye_Left_Blink) - { - faceController.SetBlink_L(weightings.Value); - } - else if (eyeShape == EyeShape.Eye_Right_Blink) - { - faceController.SetBlink_R(weightings.Value); - } - } - } - - private Vector3 Smoothify(Vector3 point) - { - if (isFirst) - { - oldPoint = point; - isFirst = false; - } - - var smoothedPoint = new Vector3( - point.x * (1.0f - Smoothing) + oldPoint.x * Smoothing, - point.y * (1.0f - Smoothing) + oldPoint.y * Smoothing, - point.z); - - oldPoint = smoothedPoint; - - return smoothedPoint; - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs.meta b/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs.meta deleted file mode 100644 index badc27fa..00000000 --- a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_ViveProEye.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1fd512d95b6700d44a5763b8c74b228a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Avatar/FaceController.cs b/Assets/Scripts/Avatar/FaceController.cs index 190e1101..ad66e178 100644 --- a/Assets/Scripts/Avatar/FaceController.cs +++ b/Assets/Scripts/Avatar/FaceController.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -using VRM; +using UniVRM10; +//using VRM; namespace VMC { @@ -9,10 +10,11 @@ public class FaceController : MonoBehaviour { private GameObject VRMmodel; - private VRMBlendShapeProxy proxy; + private Vrm10RuntimeExpression vrm10RuntimeExpression; public bool EnableBlink = false; - public bool ViveProEyeEnabled = false; + //外部デバイス(アイトラッキングプラグイン等)がまぶたを制御している間は自動まばたきを抑制する + public bool ExternalEyelidControlEnabled = false; private bool stopBlink = false; public bool StopBlink @@ -39,12 +41,12 @@ public bool StopBlink private bool IsSetting = false; - public List BlendShapeClips; //読み込んだモデルの表情のキー一覧 + public IReadOnlyList BlendShapeClips = new List(); //読み込んだモデルの表情のキー一覧 public System.Action BeforeApply; - private BlendShapePreset defaultFace = BlendShapePreset.Neutral; - public BlendShapePreset DefaultFace + private ExpressionPreset defaultFace = ExpressionPreset.neutral; + public ExpressionPreset DefaultFace { get { return defaultFace; } set @@ -52,28 +54,28 @@ public BlendShapePreset DefaultFace if (defaultFace != value) { //前回の表情を消しておく - if (proxy != null) + if (vrm10RuntimeExpression != null) { - if (defaultFace != BlendShapePreset.Unknown) + if (defaultFace != ExpressionPreset.custom) { SetFace(defaultFace, 0.0f, StopBlink); } else if (string.IsNullOrEmpty(FacePresetName) == false) { - SetFace(BlendShapeKey.CreateUnknown(FacePresetName), 0.0f, StopBlink); + SetFace(ExpressionKey.CreateCustom(FacePresetName), 0.0f, StopBlink); } } defaultFace = value; //新しい表情を設定する - if (proxy != null) + if (vrm10RuntimeExpression != null) { - if (defaultFace != BlendShapePreset.Unknown) + if (defaultFace != ExpressionPreset.custom) { SetFace(defaultFace, 1.0f, StopBlink); } else if (string.IsNullOrEmpty(FacePresetName) == false) { - SetFace(BlendShapeKey.CreateUnknown(FacePresetName), 1.0f, StopBlink); + SetFace(ExpressionKey.CreateCustom(FacePresetName), 1.0f, StopBlink); } } } @@ -83,99 +85,117 @@ public BlendShapePreset DefaultFace private AnimationController animationController; - private Dictionary CurrentShapeKeys; - private Dictionary> AccumulateShapeKeys = new Dictionary>(); - private Dictionary> OverwriteShapeKeys = new Dictionary>(); - private BlendShapeKey NeutralKey = BlendShapeKey.CreateFromPreset(BlendShapePreset.Neutral); + private Dictionary CurrentShapeKeys; + private Dictionary> AccumulateShapeKeys = new Dictionary>(); + private Dictionary> OverwriteShapeKeys = new Dictionary>(); + private ExpressionKey NeutralKey = ExpressionKey.CreateFromPreset(ExpressionPreset.neutral); - private Dictionary BlendShapeKeyString = new Dictionary(); + private Dictionary BlendShapeKeyString = new Dictionary(); private Dictionary KeyUpperCaseDictionary = new Dictionary(); public string GetCaseSensitiveKeyName(string upperCase) { if (KeyUpperCaseDictionary.Count == 0) { - foreach (var presetName in System.Enum.GetNames(typeof(BlendShapePreset))) + //VRM1.0のプリセット名(happy, aa, blinkLeft ...) + foreach (var presetName in System.Enum.GetNames(typeof(ExpressionPreset))) { KeyUpperCaseDictionary[presetName.ToUpper()] = presetName; } + //VRM0.xのプリセット名(Joy, A, Blink_L ...)。 + //この関数はv0.48より前の設定ファイルの移行で使われ、当時の表情名はVRM0.x形式なので + //こちらを後に登録して優先させる(BLINK等、大文字にすると衝突する名前がある) + foreach (var vrm0Name in VRM10CompatibleNames.PresetToVrm0Names.Values) + { + KeyUpperCaseDictionary[vrm0Name.ToUpper()] = vrm0Name; + } } return KeyUpperCaseDictionary.ContainsKey(upperCase) ? KeyUpperCaseDictionary[upperCase] : upperCase; } - public void ImportVRMmodel(GameObject vrmmodel) - { - VRMmodel = vrmmodel; - proxy = null; - InitializeProxy(); - } - private void Start() { - var dict = new Dictionary(); + var dict = new Dictionary(); foreach (var clip in BlendShapeClips) { - dict.Add(clip.Key, 0.0f); + dict.Add(clip, 0.0f); } CurrentShapeKeys = dict; CreateAnimation(); } + private void OnEnable() + { + VMCEvents.OnCurrentModelChanged += OnCurrentModelChanged; + } + + private void OnDisable() + { + VMCEvents.OnCurrentModelChanged -= OnCurrentModelChanged; + } + + private void OnCurrentModelChanged(GameObject model) + { + VRMmodel = model; + vrm10RuntimeExpression = null; + InitializeProxy(); + } + private void CreateAnimation() { if (animationController == null) animationController = new AnimationController(); - if (proxy != null) + if (vrm10RuntimeExpression != null) { animationController.ClearAnimations(); - animationController.AddResetAction(() => MixPreset("Blink", BlendShapePreset.Blink, 0.0f)); + animationController.AddResetAction(() => MixPreset("Blink", ExpressionPreset.blink, 0.0f)); animationController.AddWait(null, () => BlinkTimeMin + Random.value * (BlinkTimeMax - BlinkTimeMin)); - animationController.AddAnimation(CloseAnimationTime, 0.0f, 1.0f, v => MixPreset("Blink", BlendShapePreset.Blink, v)); + animationController.AddAnimation(CloseAnimationTime, 0.0f, 1.0f, v => MixPreset("Blink", ExpressionPreset.blink, v)); animationController.AddWait(ClosingTime); - animationController.AddAnimation(OpenAnimationTime, 1.0f, 0.0f, v => MixPreset("Blink", BlendShapePreset.Blink, v)); + animationController.AddAnimation(OpenAnimationTime, 1.0f, 0.0f, v => MixPreset("Blink", ExpressionPreset.blink, v)); } } public void SetBlink_L(float value) { - if (ViveProEyeEnabled == false) + if (ExternalEyelidControlEnabled == false) { - MixPreset("Blink", BlendShapePreset.Blink, 0.0f); + MixPreset("Blink", ExpressionPreset.blink, 0.0f); } if (StopBlink) { - MixPreset("Blink_L", BlendShapePreset.Blink_L, 0.0f); + MixPreset("Blink_L", ExpressionPreset.blinkLeft, 0.0f); } else { - MixPreset("Blink_L", BlendShapePreset.Blink_L, value); + MixPreset("Blink_L", ExpressionPreset.blinkLeft, value); } } public void SetBlink_R(float value) { - if (ViveProEyeEnabled == false) + if (ExternalEyelidControlEnabled == false) { - MixPreset("Blink", BlendShapePreset.Blink, 0.0f); + MixPreset("Blink", ExpressionPreset.blink, 0.0f); } if (StopBlink) { - MixPreset("Blink_R", BlendShapePreset.Blink_L, 0.0f); + MixPreset("Blink_R", ExpressionPreset.blinkLeft, 0.0f); } else { - MixPreset("Blink_R", BlendShapePreset.Blink_R, value); + MixPreset("Blink_R", ExpressionPreset.blinkRight, value); } } private void SetFaceNeutral() { //表情をデフォルトに戻す - if (proxy != null) + if (vrm10RuntimeExpression != null) { - var keys = new List(); + var keys = new List(); var values = new List(); foreach (var clip in BlendShapeClips) { - var shapekey = clip.Key; + var shapekey = clip; if (shapekey.Equals(NeutralKey)) { values.Add(1.0f); @@ -202,26 +222,34 @@ public void EndSetting() IsSetting = false; } - public void SetFace(BlendShapePreset preset, float strength, bool stopBlink) + public void SetFace(ExpressionPreset preset, float strength, bool stopBlink) { - SetFace(BlendShapeKey.CreateFromPreset(preset), strength, stopBlink); + SetFace(ExpressionKey.CreateFromPreset(preset), strength, stopBlink); } - public void SetFace(BlendShapeKey key, float strength, bool stopBlink) + public void SetFace(ExpressionKey key, float strength, bool stopBlink) { - SetFace(new List { key }, new List { strength }, stopBlink); + SetFace(new List { key }, new List { strength }, stopBlink); } public void SetFace(List keys, List strength, bool stopBlink) { - if (proxy != null) + if (vrm10RuntimeExpression != null) { if (keys.Any(d => BlendShapeKeyString.ContainsKey(d) == false)) { - var convertKeys = keys.Select(d => GetCaseSensitiveKeyName(d)) - .Where(d => BlendShapeKeyString.ContainsKey(d)) - .Select(d => BlendShapeKeyString[d]).ToList(); - SetFace(convertKeys, strength, stopBlink); + var convertKeys = new List(); + var convertValues = new List(); + for (int i = 0; i < keys.Count; i++) + { + var caseSensitiveKeyName = GetCaseSensitiveKeyName(keys[i]); + if (BlendShapeKeyString.ContainsKey(caseSensitiveKeyName)) + { + convertKeys.Add(BlendShapeKeyString[caseSensitiveKeyName]); + convertValues.Add(strength[i]); + } + } + SetFace(convertKeys, convertValues, stopBlink); } else { @@ -230,15 +258,15 @@ public void SetFace(List keys, List strength, bool stopBlink) } } - public void SetFace(List keys, List strength, bool stopBlink) + public void SetFace(List keys, List strength, bool stopBlink) { - if (proxy != null) + if (vrm10RuntimeExpression != null) { StopBlink = stopBlink; - var dict = new Dictionary(); + var dict = new Dictionary(); foreach (var clip in BlendShapeClips) { - dict.Add(clip.Key, 0.0f); + dict.Add(clip, 0.0f); } //dict[NeutralKey] = 1.0f; for (int i = 0; i < keys.Count; i++) @@ -250,17 +278,17 @@ public void SetFace(List keys, List strength, bool stopBli } } - public void MixPreset(string presetName, BlendShapePreset preset, float value) + public void MixPreset(string presetName, ExpressionPreset preset, float value) { MixPresets(presetName, new[] { preset }, new[] { value }); } - public void MixPresets(string presetName, BlendShapePreset[] presets, float[] values) + public void MixPresets(string presetName, ExpressionPreset[] presets, float[] values) { - MixPresets(presetName, presets.Select(d => BlendShapeKey.CreateFromPreset(d)).ToArray(), values); + MixPresets(presetName, presets.Select(d => ExpressionKey.CreateFromPreset(d)).ToArray(), values); } - public void MixPreset(string presetName, BlendShapeKey preset, float value) + public void MixPreset(string presetName, ExpressionKey preset, float value) { MixPresets(presetName, new[] { preset }, new[] { value }); } @@ -269,10 +297,18 @@ public void MixPresets(string presetName, string[] keys, float[] values) { if (keys.Any(d => BlendShapeKeyString.ContainsKey(d) == false)) { - var convertKeys = keys.Select(d => GetCaseSensitiveKeyName(d)) - .Where(d => BlendShapeKeyString.ContainsKey(d)) - .Select(d => BlendShapeKeyString[d]).ToArray(); - MixPresets(presetName, convertKeys, values); + var convertKeys = new List(); + var convertValues = new List(); + for (int i = 0; i < keys.Length; i++) + { + var caseSensitiveKeyName = GetCaseSensitiveKeyName(keys[i]); + if (BlendShapeKeyString.ContainsKey(caseSensitiveKeyName)) + { + convertKeys.Add(BlendShapeKeyString[caseSensitiveKeyName]); + convertValues.Add(values[i]); + } + } + MixPresets(presetName, convertKeys.ToArray(), convertValues.ToArray()); } else { @@ -280,48 +316,48 @@ public void MixPresets(string presetName, string[] keys, float[] values) } } - public void MixPresets(string presetName, BlendShapeKey[] presets, float[] values) + public void MixPresets(string presetName, ExpressionKey[] presets, float[] values) { - if (proxy == null) return; + if (vrm10RuntimeExpression == null) return; if (CurrentShapeKeys == null) return; if (AccumulateShapeKeys.ContainsKey(presetName) == false) { - AccumulateShapeKeys.Add(presetName, new Dictionary()); + AccumulateShapeKeys.Add(presetName, new Dictionary()); } var presetDictionary = AccumulateShapeKeys[presetName]; presetDictionary.Clear(); - //Mixしたい表情を合成する + //Mixしたい表情を合成する(VRM0名とVRM1名の両方で同じ表情が送られてくる場合があるため重複キーを許容する) for (int i = 0; i < presets.Length; i++) { var presetKey = presets[i]; - presetDictionary.Add(presetKey, values[i]); + presetDictionary[presetKey] = values[i]; } } - public void OverwritePresets(string presetName, BlendShapeKey[] presets, float[] values) + public void OverwritePresets(string presetName, ExpressionKey[] presets, float[] values) { - if (proxy == null) return; + if (vrm10RuntimeExpression == null) return; if (CurrentShapeKeys == null) return; if (OverwriteShapeKeys.ContainsKey(presetName) == false) { - OverwriteShapeKeys.Add(presetName, new Dictionary()); + OverwriteShapeKeys.Add(presetName, new Dictionary()); } var presetDictionary = OverwriteShapeKeys[presetName]; presetDictionary.Clear(); - //上書きしたい表情を追加する + //上書きしたい表情を追加する(重複キーを許容する) for (int i = 0; i < presets.Length; i++) { var presetKey = presets[i]; - presetDictionary.Add(presetKey, values[i]); + presetDictionary[presetKey] = values[i]; } } private void AccumulateBlendShapes() { - if (proxy == null) return; - var accumulatedValues = new Dictionary(); + if (vrm10RuntimeExpression == null) return; + var accumulatedValues = new Dictionary(); //ベースの表情を設定する(使わない表情には全て0が入っている) foreach (var shapeKey in CurrentShapeKeys) { @@ -359,32 +395,41 @@ private void AccumulateBlendShapes() } } - //全ての表情をSetValuesで1度に反映させる - proxy.SetValues(accumulatedValues); - - //SetValuesは内部でApplyまで行うためApply不要 + //全ての表情をSetWeightsで1度に反映させる + vrm10RuntimeExpression.SetWeights(accumulatedValues); } private void InitializeProxy() { - proxy = VRMmodel.GetComponent(); + var vrm10Instance = VRMmodel != null ? VRMmodel.GetComponent() : null; + vrm10RuntimeExpression = vrm10Instance != null ? vrm10Instance.Runtime.Expression : null; + + //モデル入れ替え時に前のモデルのキーが残らないようにクリアする + BlendShapeKeyString.Clear(); + KeyUpperCaseDictionary.Clear(); + //すべての表情の名称一覧を取得 - if (proxy != null) + if (vrm10RuntimeExpression != null) { - BlendShapeClips = proxy.BlendShapeAvatar.Clips; + BlendShapeClips = vrm10RuntimeExpression.ExpressionKeys; foreach (var clip in BlendShapeClips) { - if (clip.Preset == BlendShapePreset.Unknown) + BlendShapeKeyString[clip.Name] = clip; + KeyUpperCaseDictionary[clip.Name.ToUpper()] = clip.Name; + } + + // VRM 0.x互換の名称(Joy, A, Blink_L等)でも参照できるようにする + // (モデル側に同名のカスタム表情がある場合はそちらを優先) + foreach (var pair in VRM10CompatibleNames.PresetToVrm0Names) + { + var vrm0Name = pair.Value; + if (BlendShapeKeyString.ContainsKey(vrm0Name) == false) { - //非プリセット(Unknown)であれば、Unknown用の名前変数を参照する - BlendShapeKeyString[clip.BlendShapeName] = clip.Key; - KeyUpperCaseDictionary[clip.BlendShapeName.ToUpper()] = clip.BlendShapeName; + BlendShapeKeyString[vrm0Name] = ExpressionKey.CreateFromPreset(pair.Key); } - else + if (KeyUpperCaseDictionary.ContainsKey(vrm0Name.ToUpper()) == false) { - //プリセットであればENUM値をToStringした値を利用する - BlendShapeKeyString[clip.Preset.ToString()] = clip.Key; - KeyUpperCaseDictionary[clip.Preset.ToString().ToUpper()] = clip.Preset.ToString(); + KeyUpperCaseDictionary[vrm0Name.ToUpper()] = vrm0Name; } } } @@ -396,39 +441,47 @@ private void InitializeProxy() // Update is called once per frame void Update() { - if (VRMmodel != null) + if (VRMmodel == null) return; + + if (IsSetting == false) { - if (proxy == null) - { - InitializeProxy(); - } - if (IsSetting == false) + if (EnableBlink && ExternalEyelidControlEnabled == false) { - if (EnableBlink && ViveProEyeEnabled == false) + isReset = false; + if (StopBlink == false) { - isReset = false; - if (StopBlink == false) - { - if (animationController?.Next() == false) - {//最後まで行ったら値更新のためにアニメーション作り直す - CreateAnimation(); - } + if (animationController?.Next() == false) + {//最後まで行ったら値更新のためにアニメーション作り直す + CreateAnimation(); } } - else + } + else + { + if (isReset == false) { - if (isReset == false) - { - isReset = true; - animationController?.Reset(); - } + isReset = true; + animationController?.Reset(); } - } - - AccumulateBlendShapes(); } + AccumulateBlendShapes(); + } + + #region 自動テスト用フック + + /// + /// 全ての入力源(加算・上書き)の表情をクリアする。 + /// AccumulateShapeKeys/OverwriteShapeKeysは入力源ごとに値を保持し続けるため、 + /// これを呼ばないと前のシナリオの表情が次のシナリオに残る。 + /// + internal void Test_ClearAllMixes() + { + AccumulateShapeKeys.Clear(); + OverwriteShapeKeys.Clear(); } + + #endregion } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Avatar/FinalIKCalibrator.cs b/Assets/Scripts/Avatar/FinalIKCalibrator.cs index 727fb10a..5cd6b996 100644 --- a/Assets/Scripts/Avatar/FinalIKCalibrator.cs +++ b/Assets/Scripts/Avatar/FinalIKCalibrator.cs @@ -1,28 +1,27 @@ -using RootMotion; using RootMotion.FinalIK; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Valve.VR; -using VRM; namespace VMC { -#if UNITY_EDITOR [Serializable] public class TrackerPosition { public Vector3 Position; public Quaternion Rotation; + public ETrackedDeviceClass DeviceClass; public TrackerPosition() { } - public TrackerPosition(Transform source) + public TrackerPosition(TrackingPoint source) { if (source == null) return; - Position = source.position; - Rotation = source.rotation; + Position = source.TargetTransform.position; + Rotation = source.TargetTransform.rotation; + DeviceClass = source.DeviceClass; } } @@ -34,44 +33,64 @@ public class TrackerPositions public TrackerPosition Pelvis; public TrackerPosition LeftFoot; public TrackerPosition RightFoot; + public TrackerPosition LeftElbow; + public TrackerPosition RightElbow; + public TrackerPosition LeftKnee; + public TrackerPosition RightKnee; + public TrackerPosition Chest; } -#endif public class FinalIKCalibrator { + public enum CalibrateMode + { + Ipose, + Tpose, + } + + private static List GeneratedGameObjects = new List(); + + public static void ClearGeneratedGameObjects() + { + foreach (var gameObject in GeneratedGameObjects) + { + if (gameObject != null) GameObject.DestroyImmediate(gameObject); + } + + GeneratedGameObjects.Clear(); + } + /// - /// 通常モード、Iポーズキャリブレーション + /// 通常モード、I/Tポーズキャリブレーション /// - public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform footTrackerRoot, VRIK vrik, VRIKCalibrator.Settings settings, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null) + public static IEnumerator Calibrate(CalibrateMode calibrateMode, Transform handTrackerRoot, Transform footTrackerRoot, VRIK vrik, VRIKCalibrator.Settings settings, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null, TrackingPoint ChestTrackingPoint = null, Transform generatedObject = null) { var currentModel = vrik.transform; #if UNITY_EDITOR var trackerPositions = new TrackerPositions { - Head = new TrackerPosition(HMDTrackingPoint?.TargetTransform), - LeftHand = new TrackerPosition(LeftHandTrackingPoint?.TargetTransform), - RightHand = new TrackerPosition(RightHandTrackingPoint?.TargetTransform), - Pelvis = new TrackerPosition(PelvisTrackingPoint?.TargetTransform), - LeftFoot = new TrackerPosition(LeftFootTrackingPoint?.TargetTransform), - RightFoot = new TrackerPosition(RightFootTrackingPoint?.TargetTransform), + Head = new TrackerPosition(HMDTrackingPoint), + LeftHand = new TrackerPosition(LeftHandTrackingPoint), + RightHand = new TrackerPosition(RightHandTrackingPoint), + Pelvis = new TrackerPosition(PelvisTrackingPoint), + LeftFoot = new TrackerPosition(LeftFootTrackingPoint), + RightFoot = new TrackerPosition(RightFootTrackingPoint), }; var trackerPositionsJson = JsonUtility.ToJson(trackerPositions); GUIUtility.systemCopyBuffer = trackerPositionsJson; #endif - vrik.enabled = false; - yield return null; //それぞれのトラッカーを正しいルートに移動 if (HMDTrackingPoint != null) HMDTrackingPoint.TargetTransform.parent = footTrackerRoot; - else throw new Exception("Head tracker not found"); + else { Debug.LogError("[Calib Fail] Head tracker not found"); yield break; } if (LeftHandTrackingPoint != null) LeftHandTrackingPoint.TargetTransform.parent = handTrackerRoot; - else throw new Exception("Left hand tracker not found"); + else { Debug.LogError("[Calib Fail] Left hand tracker not found"); yield break; } if (RightHandTrackingPoint != null) RightHandTrackingPoint.TargetTransform.parent = handTrackerRoot; - else throw new Exception("Right hand tracker not found"); + else { Debug.LogError("[Calib Fail] Right hand tracker not found"); yield break; } if (PelvisTrackingPoint != null) PelvisTrackingPoint.TargetTransform.parent = footTrackerRoot; if (LeftFootTrackingPoint != null) LeftFootTrackingPoint.TargetTransform.parent = footTrackerRoot; if (RightFootTrackingPoint != null) RightFootTrackingPoint.TargetTransform.parent = footTrackerRoot; @@ -79,21 +98,25 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo if (RightElbowTrackingPoint != null) RightElbowTrackingPoint.TargetTransform.parent = handTrackerRoot; if (LeftKneeTrackingPoint != null) LeftKneeTrackingPoint.TargetTransform.parent = footTrackerRoot; if (RightKneeTrackingPoint != null) RightKneeTrackingPoint.TargetTransform.parent = footTrackerRoot; + if (ChestTrackingPoint != null) ChestTrackingPoint.TargetTransform.parent = handTrackerRoot; + + vrik.enabled = false; + ClearGeneratedGameObjects(); + + yield return null; var headTarget = HMDTrackingPoint; - var leftHandTarget = LeftHandTrackingPoint; - var rightHandTarget = RightHandTrackingPoint; - var leftHandTargetTransform = leftHandTarget.TargetTransform; - var rightHandTargetTransform = rightHandTarget.TargetTransform; + var leftHandTargetTransform = LeftHandTrackingPoint.TargetTransform; + var rightHandTargetTransform = RightHandTrackingPoint.TargetTransform; //IKの手のターゲットは手首なのでトラッカーに手首までのオフセットを設定 if (LeftHandTrackingPoint.DeviceClass == ETrackedDeviceClass.Controller) { //コントローラーの場合手首までのオフセットを追加 - var offset = new GameObject("LeftWristOffset").transform; + var offset = CreateGameObject("LeftWristOffset").transform; offset.parent = leftHandTargetTransform; offset.localPosition = new Vector3(-0.04f, 0.04f, -0.15f); offset.localRotation = Quaternion.Euler(60, 0, 90); @@ -105,21 +128,28 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo //トラッカーの場合設定のオフセットを適用 //お互いのトラッカー同士を向き合わせてオフセットを適用する - var leftWristLookAtTransform = new GameObject("LeftWristLookAt").transform; + var leftWristLookAtTransform = CreateGameObject("LeftWristLookAt").transform; leftWristLookAtTransform.SetParent(leftHandTargetTransform); leftWristLookAtTransform.localPosition = Vector3.zero; leftWristLookAtTransform.localRotation = Quaternion.identity; leftWristLookAtTransform.LookAt(rightHandTargetTransform); var leftWrist = new GameObject("LeftWristOffset").transform; leftWrist.parent = leftWristLookAtTransform; - leftWrist.localPosition = new Vector3(0, Settings.Current.LeftHandTrackerOffsetToBodySide, Settings.Current.LeftHandTrackerOffsetToBottom); + if (calibrateMode == CalibrateMode.Ipose) + { + leftWrist.localPosition = new Vector3(0, Settings.Current.LeftHandTrackerOffsetToBodySide, Settings.Current.LeftHandTrackerOffsetToBottom); + } + else if (calibrateMode == CalibrateMode.Tpose) + { + leftWrist.localPosition = new Vector3(0, Settings.Current.LeftHandTrackerOffsetToBottom, Settings.Current.LeftHandTrackerOffsetToBodySide); + } leftWrist.localRotation = Quaternion.Euler(Vector3.zero); leftHandTargetTransform = leftWrist; } if (RightHandTrackingPoint.DeviceClass == ETrackedDeviceClass.Controller) { - var offset = new GameObject("RightWristOffset").transform; + var offset = CreateGameObject("RightWristOffset").transform; offset.parent = rightHandTargetTransform; offset.localPosition = new Vector3(0.04f, 0.04f, -0.15f); offset.localRotation = Quaternion.Euler(60, 0, -90); @@ -131,52 +161,108 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo //トラッカーの場合設定のオフセットを適用 //お互いのトラッカー同士を向き合わせてオフセットを適用する - var rightWristLookAtTransform = new GameObject("RightWristLookAt").transform; + var rightWristLookAtTransform = CreateGameObject("RightWristLookAt").transform; rightWristLookAtTransform.SetParent(rightHandTargetTransform); rightWristLookAtTransform.localPosition = Vector3.zero; rightWristLookAtTransform.localRotation = Quaternion.identity; rightWristLookAtTransform.LookAt(leftHandTargetTransform); var rightWrist = new GameObject("RightWristOffset").transform; rightWrist.parent = rightWristLookAtTransform; - rightWrist.localPosition = new Vector3(0, Settings.Current.RightHandTrackerOffsetToBodySide, Settings.Current.RightHandTrackerOffsetToBottom); + if (calibrateMode == CalibrateMode.Ipose) + { + rightWrist.localPosition = new Vector3(0, Settings.Current.RightHandTrackerOffsetToBodySide, Settings.Current.RightHandTrackerOffsetToBottom); + } + else if (calibrateMode == CalibrateMode.Tpose) + { + rightWrist.localPosition = new Vector3(0, Settings.Current.RightHandTrackerOffsetToBottom, Settings.Current.RightHandTrackerOffsetToBodySide); + } rightWrist.localRotation = Quaternion.Euler(Vector3.zero); rightHandTargetTransform = rightWrist; } + float realHeight = 1.7f; - //手の高さから身長を算出する - // B21 橈骨茎突高 797.7 - // B1 身長 1654.7 - // ratio = B1 / B21 = 2.0743387238310141657264635828006 - // 補正値 0.93694267481427684002272492175445 - var handHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; - var realHeight = handHeight * 2.07434f * 0.93694f; - + //頭がHMDの場合 + if (headTarget.DeviceClass == ETrackedDeviceClass.HMD) + { + //目の高さから身長を算出する + // A30 頭頂・内眼角距離 124.6 + // B1 身長 1654.7 + // realHeight = (HMDHeight / (B1 - A30)) * A30 + HMDHeight + var hmdHeight = headTarget.TargetTransform.position.y; + realHeight = (hmdHeight / (1.6547f - 0.1246f)) * 0.1246f + hmdHeight; + } + else + { + //手の高さから身長を算出する + if (calibrateMode == CalibrateMode.Ipose) + { + // B21 橈骨茎突高 797.7 + // B1 身長 1654.7 + // ratio = B1 / B21 = 2.0743387238310141657264635828006 + // 補正値 0.93694267481427684002272492175445 + var handHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; + realHeight = handHeight * 2.07434f * 1.03f; + } + else if (calibrateMode == CalibrateMode.Tpose) + { + // B5 頚窩高 1352.1 + // B1 身長 1654.7 + // ratio = B1 / B5 = 1.2238000147918053398417276828637 + var handHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; + realHeight = handHeight * 1.2238f; + } + } Debug.Log($"UserHeight:{realHeight}"); + IKManager.Instance.CalibrationResult.UserHeight = realHeight; + + if (Settings.Current.EnableOverrideBodyHeight) + { + realHeight = Settings.Current.OverrideBodyHeight; + Debug.Log($"Override UserHeight:{realHeight}"); + } // トラッカー全体のスケールを手の位置に合わせる // スケールを動かしてから位置を取らないとモデルの位置がずれる var leftHand = vrik.references.leftHand; var rightHand = vrik.references.rightHand; - var leftUpperArm = vrik.references.leftUpperArm; - var rightUpperArm = vrik.references.rightUpperArm; - var modelHandDistance = Vector3.Distance(leftHand.position, leftUpperArm.position) + Vector3.Distance(leftUpperArm.position, rightUpperArm.position) + Vector3.Distance(rightUpperArm.position, rightHand.position); - - //身長比 (realHeight / B1) - var realHeightRatio = realHeight / 1.6547f; - //C7 上腕長 301.2 - var realUpperArmLength = realHeightRatio * 0.3012f; - //C8 前腕長 240.5 - var realLowerArmLength = realHeightRatio * 0.2405f; - //D7 肩峰幅 378.8 - var realShoulderWidth = realHeightRatio * 0.3788f; - //補正値 0.90461508349542846218505245206182 - var realHandDistance = (realShoulderWidth + realUpperArmLength * 2 + realLowerArmLength * 2) * 0.90461f; - var wscale = modelHandDistance / realHandDistance; - handTrackerRoot.localScale = new Vector3(wscale, wscale, wscale); - footTrackerRoot.localScale = new Vector3(wscale, wscale, wscale); - - Debug.Log($"wscale:{wscale}"); + var offsetScale = 1.0f; + if (calibrateMode == CalibrateMode.Ipose) + { + var leftUpperArm = vrik.references.leftUpperArm; + var rightUpperArm = vrik.references.rightUpperArm; + var modelHandDistance = Vector3.Distance(leftHand.position, leftUpperArm.position) + Vector3.Distance(leftUpperArm.position, rightUpperArm.position) + Vector3.Distance(rightUpperArm.position, rightHand.position); + + Debug.Log($"modelHandDistance:{modelHandDistance}"); + + //身長比 (realHeight / B1) + var realHeightRatio = realHeight / 1.6547f; + //C7 上腕長 301.2 + var realUpperArmLength = realHeightRatio * 0.3012f; + //C8 前腕長 240.5 + var realLowerArmLength = realHeightRatio * 0.2405f; + //D7 肩峰幅 378.8 + var realShoulderWidth = realHeightRatio * 0.3788f; + + var realHandDistance = (realShoulderWidth + realUpperArmLength * 2 + realLowerArmLength * 2); + + Debug.Log($"realHandDistance:{realHandDistance} realUpperArmLength:{realUpperArmLength} realLowerArmLength:{realLowerArmLength} realShoulderWidth:{realShoulderWidth}"); + + //補正値 1.15 + var realScale = modelHandDistance / realHandDistance; + offsetScale = realScale * 1.15f; + } + else if (calibrateMode == CalibrateMode.Tpose) + { + var modelHandDistance = Vector3.Distance(leftHand.position, rightHand.position); + var realHandDistance = Vector3.Distance(leftHandTargetTransform.position, rightHandTargetTransform.position); + var realScale = modelHandDistance / realHandDistance; + offsetScale = realScale * 1.00f; + } + handTrackerRoot.localScale = new Vector3(offsetScale, offsetScale, offsetScale); + footTrackerRoot.localScale = new Vector3(offsetScale, offsetScale, offsetScale); + + Debug.Log($"wscale:{offsetScale}"); //モデルの体の中心を取っておく var handcenterposition = Vector3.Lerp(vrik.references.leftHand.position, vrik.references.rightHand.position, 0.5f); @@ -190,18 +276,36 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo // リアル手と手の中心から少し後ろに下げた位置 - var scaledCenterPosition = Vector3.Lerp(leftHandTargetTransform.position, rightHandTargetTransform.position, 0.5f) + hmdForwardAngle.normalized * (realHeight * wscale * 0.0043f); + var scaledCenterPosition = Vector3.Lerp(leftHandTargetTransform.position, rightHandTargetTransform.position, 0.5f); + + if (calibrateMode == CalibrateMode.Ipose) + { + scaledCenterPosition = Vector3.Lerp(leftHandTargetTransform.position, rightHandTargetTransform.position, 0.5f) - hmdForwardAngle.normalized * (realHeight * offsetScale * 0.03f); + } + else if (calibrateMode == CalibrateMode.Tpose) + { + scaledCenterPosition = Vector3.Lerp(leftHandTargetTransform.position, rightHandTargetTransform.position, 0.5f) - hmdForwardAngle.normalized * (realHeight * offsetScale * 0.02f); + } + + //頭がHMDの場合 + if (headTarget.DeviceClass == ETrackedDeviceClass.HMD) + { + scaledCenterPosition = headTarget.TargetTransform.position - hmdForwardAngle.normalized * (realHeight * offsetScale * 0.043f); + } + + Debug.Log($"scaledCenterPosition:({scaledCenterPosition.x}, {scaledCenterPosition.y}, {scaledCenterPosition.z})"); //身長から腰の位置を算出する - // B12 臍高 965.7 + // B13 上前腸骨棘高 891.9 + // B14 恥骨結合上縁高 809.2 // B1 身長 1654.7 - // ratio = B13 / B1 = 0.58361032211276968634797848552608 - //腰補正値 1.031572735f - var realPelvisHeight = realHeight * 0.58361f * 1.031572735f; + // ratio = (B13 + (B14 - B13) * 0.?) / B1 = 0.51402066839910557805040188553816 + //腰補正値 0.95 + var realPelvisHeight = realHeight * (809.2f + (891.9f - 809.2f) * 0.67f) / 1654.7f * 0.95f; Debug.Log($"realPelvisHeight:{realPelvisHeight}"); - var scaledPelvisPosition = new Vector3(scaledCenterPosition.x, realPelvisHeight * wscale, scaledCenterPosition.z); + var scaledPelvisPosition = new Vector3(scaledCenterPosition.x, realPelvisHeight * offsetScale, scaledCenterPosition.z); Debug.Log($"scaledPelvisHeight:{scaledPelvisPosition.y}"); @@ -214,10 +318,10 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo if (pelvisTargetTransform != null) { - var pelvisRotatePoint = new GameObject("PelvisRotatePoint").transform; + var pelvisRotatePoint = CreateGameObject("PelvisRotatePoint").transform; pelvisRotatePoint.SetParent(pelvisTargetTransform); - pelvisRotatePoint.position = scaledPelvisPosition; - pelvisRotatePoint.localRotation = Quaternion.identity; + pelvisRotatePoint.position = scaledPelvisPosition + new Vector3(0, Settings.Current.PelvisOffsetAdjustY, Settings.Current.PelvisOffsetAdjustZ); + pelvisRotatePoint.rotation = Quaternion.identity; pelvisTargetTransform = pelvisRotatePoint; } @@ -229,26 +333,44 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo var footTrackerOffset = new Vector3(0, modelPelvisHeight - scaledPelvisPosition.y, 0); footTrackerRoot.position = footTrackerOffset; - //腕をおろしているのでアバターもおろしておく - vrik.references.leftShoulder.localEulerAngles = new Vector3(0, 0, 0); - vrik.references.leftUpperArm.localEulerAngles = new Vector3(0, 0, 80); - vrik.references.leftForearm.localEulerAngles = new Vector3(0, 0, 5); - vrik.references.leftHand.localEulerAngles = new Vector3(0, 0, 0); - vrik.references.rightShoulder.localEulerAngles = new Vector3(0, 0, 0); - vrik.references.rightUpperArm.localEulerAngles = new Vector3(0, 0, -80); - vrik.references.rightForearm.localEulerAngles = new Vector3(0, 0, -5); - vrik.references.rightHand.localEulerAngles = new Vector3(0, 0, 0); + if (calibrateMode == CalibrateMode.Ipose) + { + //腕をおろしているのでアバターもおろしておく + vrik.references.leftShoulder.localEulerAngles = new Vector3(0, 0, 0); + vrik.references.leftUpperArm.localEulerAngles = new Vector3(0, 0, 80); + vrik.references.leftForearm.localEulerAngles = new Vector3(0, 0, 5); + vrik.references.leftHand.localEulerAngles = new Vector3(0, 0, 0); + vrik.references.rightShoulder.localEulerAngles = new Vector3(0, 0, 0); + vrik.references.rightUpperArm.localEulerAngles = new Vector3(0, 0, -80); + vrik.references.rightForearm.localEulerAngles = new Vector3(0, 0, -5); + vrik.references.rightHand.localEulerAngles = new Vector3(0, 0, 0); + } + else if (calibrateMode == CalibrateMode.Tpose) + { + //手のひら正面向けてるのでアバターも向けておく + vrik.references.leftHand.Rotate(new Vector3(-90, 0, 0)); + vrik.references.rightHand.Rotate(new Vector3(-90, 0, 0)); + } //手と肘トラッカーを手の高さにオフセット var modelHandHeight = (vrik.references.leftHand.position.y + vrik.references.rightHand.position.y) / 2f; var realHandHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; var handTrackerOffset = new Vector3(0, modelHandHeight - realHandHeight, 0); - handTrackerRoot.position = handTrackerOffset; + var realLeftHandHeight = leftHandTargetTransform.position.y; + var realRightHandHeight = rightHandTargetTransform.position.y; + var TposeOffset = realHeight * 0.025f; + var leftHandTrackerOffset = new Vector3(0, modelHandHeight - realLeftHandHeight + TposeOffset, 0); + var rightHandTrackerOffset = new Vector3(0, modelHandHeight - realRightHandHeight + TposeOffset, 0); + if (calibrateMode == CalibrateMode.Ipose) + { + handTrackerRoot.position = handTrackerOffset; + } // Head //頭の位置は1cm前後後ろに下げる - var headOffsetPosition = new Vector3(vrik.references.head.position.x, vrik.references.head.position.y, vrik.references.head.position.z - (realHeight * 0.01f * wscale)); - var headOffset = CreateTransform("HeadIKTarget", true, headTarget.TargetTransform, headOffsetPosition, vrik.references.head.rotation); + var headTargetTransform = headTarget.TargetTransform; + var headOffsetPosition = new Vector3(vrik.references.head.position.x, vrik.references.head.position.y, vrik.references.head.position.z - (realHeight * 0.01f * offsetScale)); + var headOffset = CreateTransform("HeadIKTarget", headTargetTransform, headOffsetPosition, vrik.references.head.rotation); vrik.solver.spine.headTarget = headOffset; vrik.solver.spine.positionWeight = 1f; vrik.solver.spine.rotationWeight = 1f; @@ -260,8 +382,13 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo vrik.solver.spine.maxRootAngle = 20; // LeftHand - var leftHandOffset = CreateTransform("LeftHandIKTarget", true, leftHandTargetTransform, vrik.references.leftHand); + if (calibrateMode == CalibrateMode.Tpose) + { + handTrackerRoot.position = leftHandTrackerOffset; + } + var leftHandOffset = CreateTransform("LeftHandIKTarget", leftHandTargetTransform, vrik.references.leftHand); leftHandOffset.localPosition = Vector3.zero; + vrik.solver.leftArm.target = leftHandOffset; vrik.solver.leftArm.positionWeight = 1f; vrik.solver.leftArm.rotationWeight = 1f; @@ -273,8 +400,13 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo // RightHand - var rightHandOffset = CreateTransform("RightHandIKTarget", true, rightHandTargetTransform, vrik.references.rightHand); + if (calibrateMode == CalibrateMode.Tpose) + { + handTrackerRoot.position = rightHandTrackerOffset; + } + var rightHandOffset = CreateTransform("RightHandIKTarget", rightHandTargetTransform, vrik.references.rightHand); rightHandOffset.localPosition = Vector3.zero; + vrik.solver.rightArm.target = rightHandOffset; vrik.solver.rightArm.positionWeight = 1f; vrik.solver.rightArm.rotationWeight = 1f; @@ -291,12 +423,13 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo if (pelvisTargetTransform != null) { //実際の回転位置のY方向を補正して付いてくるオブジェクト(上下方向補正) - var PelvisAdjustFollower = new GameObject("PelvisAdjustFollower"); + var PelvisAdjustFollower = CreateGameObject("PelvisAdjustFollower"); PelvisAdjustFollower.AddComponent().Initialize(pelvisTargetTransform, true, true); pelvisTargetTransform = PelvisAdjustFollower.transform; + pelvisTargetTransform.SetParent(generatedObject); //pelvisTargetTransformは腰の回転軸位置(UpperLeg)に居るので、子のIKターゲットはHip位置にオフセットして指定 - var pelvisOffset = CreateTransform("PelvisIKTarget", true, pelvisTargetTransform, pelvisTargetTransform.position + UpperLegOffset, vrik.references.pelvis.rotation); + var pelvisOffset = CreateTransform("PelvisIKTarget", pelvisTargetTransform, pelvisTargetTransform.position + UpperLegOffset, vrik.references.pelvis.rotation); vrik.solver.spine.pelvisTarget = pelvisOffset; vrik.solver.spine.pelvisPositionWeight = 1f; @@ -305,16 +438,25 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo vrik.solver.plantFeet = false; vrik.solver.spine.neckStiffness = 0f; vrik.solver.spine.maxRootAngle = 180f; - - //頭が腰に近づいたときに猫背になりすぎないように (Final IK v2.1~) - vrik.solver.spine.useAnimatedHeadHeightWeight = 1.0f; - vrik.solver.spine.useAnimatedHeadHeightRange = 0.001f; - vrik.solver.spine.animatedHeadHeightBlend = 0.28f; + vrik.solver.spine.minHeadHeight = -100f; } // 腰のトラッキングを調整 vrik.solver.spine.maintainPelvisPosition = 0; // アバターによって腰がグリングリンするのが直ります + // Chest + var chestTargetTransform = ChestTrackingPoint?.TargetTransform; + + if (chestTargetTransform != null && vrik.references.chest != null) + { + var chestBone = vrik.references.chest; + var bendGoal = CreateTransform("ChestBendGoal", chestTargetTransform); + bendGoal.position = chestBone.position + currentModel.forward * 0.5f; + vrik.solver.spine.chestGoal = bendGoal; + vrik.solver.spine.chestGoalWeight = 1.0f; + vrik.solver.spine.chestClampWeight = 0f; + } + var leftFootTargetTransform = LeftFootTrackingPoint?.TargetTransform; var rightFootTargetTransform = RightFootTrackingPoint?.TargetTransform; @@ -337,12 +479,12 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo if (leftFootTargetTransform != null) { var footBone = vrik.references.leftToes != null ? vrik.references.leftToes : vrik.references.leftFoot; - var leftFootOffset = CreateTransform("LeftFootIKTarget", true, leftFootTargetTransform, footBone); + var leftFootOffset = CreateTransform("LeftFootIKTarget", leftFootTargetTransform, footBone); vrik.solver.leftLeg.target = leftFootOffset; vrik.solver.leftLeg.positionWeight = 1f; vrik.solver.leftLeg.rotationWeight = 1f; - var bendGoal = CreateTransform("LeftFootBendGoal", true, leftFootTargetTransform); + var bendGoal = CreateTransform("LeftFootBendGoal", leftFootTargetTransform); bendGoal.position = footBone.position + currentModel.forward + currentModel.up; vrik.solver.leftLeg.bendGoal = bendGoal; vrik.solver.leftLeg.bendGoalWeight = 0.7f; @@ -351,12 +493,13 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo else { // アバターの足の位置についていくオブジェクト(FinalIKの処理順に影響を受けない) - var leftFootFollow = CreateTransform("LeftFootFollowObject", true, null, vrik.references.leftFoot); + var leftFootFollow = CreateTransform("LeftFootFollowObject", null, vrik.references.leftFoot); + leftFootFollow.SetParent(generatedObject); var follower = leftFootFollow.gameObject.AddComponent(); follower.Target = vrik.references.leftFoot; // 腰の子に膝のBendGoal設定用(足トラッカーが無いとき利用される) - var bendGoalTarget = CreateTransform("LeftFootBendGoalTarget", true, leftFootFollow); + var bendGoalTarget = CreateTransform("LeftFootBendGoalTarget", leftFootFollow); bendGoalTarget.localPosition = new Vector3(0, 0.4f, 2); // 正面2m 高さ40cm bendGoalTarget.localRotation = Quaternion.identity; vrik.solver.leftLeg.bendGoal = bendGoalTarget; @@ -366,12 +509,12 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo if (rightFootTargetTransform != null) { var footBone = vrik.references.rightToes != null ? vrik.references.rightToes : vrik.references.rightFoot; - var rightFootOffset = CreateTransform("RightFootIKTarget", true, rightFootTargetTransform, footBone); + var rightFootOffset = CreateTransform("RightFootIKTarget", rightFootTargetTransform, footBone); vrik.solver.rightLeg.target = rightFootOffset; vrik.solver.rightLeg.positionWeight = 1f; vrik.solver.rightLeg.rotationWeight = 1f; - var bendGoal = CreateTransform("RightFootBendGoal", true, rightFootTargetTransform); + var bendGoal = CreateTransform("RightFootBendGoal", rightFootTargetTransform); bendGoal.position = footBone.position + currentModel.forward + currentModel.up; vrik.solver.rightLeg.bendGoal = bendGoal; vrik.solver.rightLeg.bendGoalWeight = 0.7f; @@ -380,412 +523,184 @@ public static IEnumerator CalibrateIpose(Transform handTrackerRoot, Transform fo else { // アバターの足の位置についていくオブジェクト(FinalIKの処理順に影響を受けない) - var rightFootFollow = CreateTransform("RightFootFollowObject", true, null, vrik.references.rightFoot); + var rightFootFollow = CreateTransform("RightFootFollowObject", null, vrik.references.rightFoot); + rightFootFollow.SetParent(generatedObject); var follower = rightFootFollow.gameObject.AddComponent(); follower.Target = vrik.references.rightFoot; // 腰の子に膝のBendGoal設定用(足トラッカーが無いとき利用される) - var bendGoalTarget = CreateTransform("RightFootBendGoalTarget", true, rightFootFollow); + var bendGoalTarget = CreateTransform("RightFootBendGoalTarget", rightFootFollow); bendGoalTarget.localPosition = new Vector3(0, 0.4f, 2); // 正面2m 高さ40cm bendGoalTarget.localRotation = Quaternion.identity; vrik.solver.rightLeg.bendGoal = bendGoalTarget; vrik.solver.rightLeg.bendGoalWeight = 1.0f; } - // 腰トラッカーか両足トラッカーがある場合VRIKRootControllerを使用しないと - // (特に)180度後ろを向いたときに正しい膝の方向計算ができません - if (pelvisTargetTransform != null || (leftFootTargetTransform != null && rightFootTargetTransform != null)) + var leftElbowTargetTransform = LeftElbowTrackingPoint?.TargetTransform; + var rightElbowTargetTransform = RightElbowTrackingPoint?.TargetTransform; + + // Left Elbow + if (leftElbowTargetTransform != null) { - var vrikRootController = vrik.references.root.gameObject.AddComponent(); + var leftArmBendGoalTarget = CreateTransform("LeftArmBendGoalTarget", leftElbowTargetTransform, vrik.references.leftForearm); + if (calibrateMode == CalibrateMode.Ipose) + { + leftArmBendGoalTarget.position += -currentModel.forward * 0.1f; + } + if (calibrateMode == CalibrateMode.Tpose) + { + leftArmBendGoalTarget.position += -currentModel.up * 0.1f; + } + if (vrik.solver.leftArm.bendGoal != null) GameObject.Destroy(vrik.solver.leftArm.bendGoal.gameObject); + vrik.solver.leftArm.bendGoal = leftArmBendGoalTarget; + vrik.solver.leftArm.bendGoalWeight = 1.0f; } - if (pelvisTargetTransform != null) + // Right Elbow + if (rightElbowTargetTransform != null) { - var pelvisWeightAdjuster = vrik.references.root.gameObject.GetComponent(); - if (pelvisWeightAdjuster == null) pelvisWeightAdjuster = vrik.references.root.gameObject.AddComponent(); - pelvisWeightAdjuster.vrik = vrik; + var rightArmBendGoalTarget = CreateTransform("RightArmBendGoalTarget", rightElbowTargetTransform, vrik.references.rightForearm); + if (calibrateMode == CalibrateMode.Ipose) + { + rightArmBendGoalTarget.position += -currentModel.forward * 0.1f; + } + if (calibrateMode == CalibrateMode.Tpose) + { + rightArmBendGoalTarget.position += -currentModel.up * 0.1f; + } + if (vrik.solver.rightArm.bendGoal != null) GameObject.Destroy(vrik.solver.rightArm.bendGoal.gameObject); + vrik.solver.rightArm.bendGoal = rightArmBendGoalTarget; + vrik.solver.rightArm.bendGoalWeight = 1.0f; } - //wristRotationFix = currentModel.AddComponent(); - //wristRotationFix.SetVRIK(vrik); - + var leftKneeTargetTransform = LeftKneeTrackingPoint?.TargetTransform; + var rightKneeTargetTransform = RightKneeTrackingPoint?.TargetTransform; - vrik.enabled = true; - - vrik.solver.IKPositionWeight = 1.0f; - - vrik.UpdateSolverExternal(); - - //DebugSphere(leftHandTargetTransform); - //DebugSphere(rightHandTargetTransform); - //DebugSphere(leftHand); - //DebugSphere(rightHand); - //DebugSphere(headOffset); - //DebugSphere(vrik.references.head); - //DebugSphere(pelvisTargetTransform); - //DebugSphere(vrik.references.pelvis); - //DebugSphere(leftFootTargetTransform); - //DebugSphere(vrik.references.leftFoot); - //DebugSphere(rightFootTargetTransform); - //DebugSphere(vrik.references.rightFoot); - - yield return null; - } - - /// - /// 通常モード、Tポーズキャリブレーション - /// - public static IEnumerator CalibrateTpose(Transform handTrackerRoot, Transform footTrackerRoot, VRIK vrik, VRIKCalibrator.Settings settings, TrackingPoint HMDTrackingPoint, TrackingPoint PelvisTrackingPoint = null, TrackingPoint LeftHandTrackingPoint = null, TrackingPoint RightHandTrackingPoint = null, TrackingPoint LeftFootTrackingPoint = null, TrackingPoint RightFootTrackingPoint = null, TrackingPoint LeftElbowTrackingPoint = null, TrackingPoint RightElbowTrackingPoint = null, TrackingPoint LeftKneeTrackingPoint = null, TrackingPoint RightKneeTrackingPoint = null) - { - var currentModel = vrik.transform; - -#if UNITY_EDITOR - var trackerPositions = new TrackerPositions + // Left Knee + if (leftKneeTargetTransform != null) { - Head = new TrackerPosition(HMDTrackingPoint?.TargetTransform), - LeftHand = new TrackerPosition(LeftHandTrackingPoint?.TargetTransform), - RightHand = new TrackerPosition(RightHandTrackingPoint?.TargetTransform), - Pelvis = new TrackerPosition(PelvisTrackingPoint?.TargetTransform), - LeftFoot = new TrackerPosition(LeftFootTrackingPoint?.TargetTransform), - RightFoot = new TrackerPosition(RightFootTrackingPoint?.TargetTransform), - }; - - var trackerPositionsJson = JsonUtility.ToJson(trackerPositions); - GUIUtility.systemCopyBuffer = trackerPositionsJson; -#endif - - vrik.enabled = false; - yield return null; - - //それぞれのトラッカーを正しいルートに移動 - if (HMDTrackingPoint != null) HMDTrackingPoint.TargetTransform.parent = footTrackerRoot; - else throw new Exception("Head tracker not found"); - if (LeftHandTrackingPoint != null) LeftHandTrackingPoint.TargetTransform.parent = handTrackerRoot; - else throw new Exception("Left hand tracker not found"); - if (RightHandTrackingPoint != null) RightHandTrackingPoint.TargetTransform.parent = handTrackerRoot; - else throw new Exception("Right hand tracker not found"); - if (PelvisTrackingPoint != null) PelvisTrackingPoint.TargetTransform.parent = footTrackerRoot; - if (LeftFootTrackingPoint != null) LeftFootTrackingPoint.TargetTransform.parent = footTrackerRoot; - if (RightFootTrackingPoint != null) RightFootTrackingPoint.TargetTransform.parent = footTrackerRoot; - if (LeftElbowTrackingPoint != null) LeftElbowTrackingPoint.TargetTransform.parent = handTrackerRoot; - if (RightElbowTrackingPoint != null) RightElbowTrackingPoint.TargetTransform.parent = handTrackerRoot; - if (LeftKneeTrackingPoint != null) LeftKneeTrackingPoint.TargetTransform.parent = footTrackerRoot; - if (RightKneeTrackingPoint != null) RightKneeTrackingPoint.TargetTransform.parent = footTrackerRoot; - - - var headTarget = HMDTrackingPoint; - - var leftHandTarget = LeftHandTrackingPoint; - var rightHandTarget = RightHandTrackingPoint; - var leftHandTargetTransform = leftHandTarget.TargetTransform; - var rightHandTargetTransform = rightHandTarget.TargetTransform; + //膝が内曲がりになる時があるので前方にオフセット + var leftCalfOffset = CreateGameObject("leftCalfOffset").transform; + leftCalfOffset.parent = vrik.references.leftCalf; + leftCalfOffset.position = vrik.references.leftCalf.position + currentModel.forward * 0.1f; - //IKの手のターゲットは手首なのでトラッカーに手首までのオフセットを設定 + var leftLegBendGoalTarget = CreateTransform("LeftLegBendGoalTarget", leftKneeTargetTransform, leftCalfOffset); + if (vrik.solver.leftLeg.bendGoal != null) GameObject.Destroy(vrik.solver.leftLeg.bendGoal.gameObject); - if (LeftHandTrackingPoint.DeviceClass == ETrackedDeviceClass.Controller) - { - //コントローラーの場合手首までのオフセットを追加 - var offset = new GameObject("LeftWristOffset").transform; - offset.parent = leftHandTargetTransform; - offset.localPosition = new Vector3(-0.04f, 0.04f, -0.15f); - offset.localRotation = Quaternion.Euler(60, 0, 90); - offset.localScale = Vector3.one; - leftHandTargetTransform = offset; + var boneBendGoal = leftLegBendGoalTarget.gameObject.AddComponent(); + boneBendGoal.SetBones("LeftLeg", vrik.references.leftThigh, leftCalfOffset, vrik.references.leftFoot, leftLegBendGoalTarget); + vrik.solver.leftLeg.bendGoal = leftLegBendGoalTarget; } - else if (LeftHandTrackingPoint.DeviceClass == ETrackedDeviceClass.GenericTracker) + + // Right Knee + if (rightKneeTargetTransform != null) { - //トラッカーの場合設定のオフセットを適用 + //膝が内曲がりになる時があるので前方にオフセット + var rightCalfOffset = CreateGameObject("rightCalfOffset").transform; + rightCalfOffset.parent = vrik.references.rightCalf; + rightCalfOffset.position = vrik.references.rightCalf.position + currentModel.forward * 0.1f; - //お互いのトラッカー同士を向き合わせてオフセットを適用する - var leftWristLookAtTransform = new GameObject("LeftWristLookAt").transform; - leftWristLookAtTransform.SetParent(leftHandTargetTransform); - leftWristLookAtTransform.localPosition = Vector3.zero; - leftWristLookAtTransform.localRotation = Quaternion.identity; - leftWristLookAtTransform.LookAt(rightHandTargetTransform); - var leftWrist = new GameObject("LeftWristOffset").transform; - leftWrist.parent = leftWristLookAtTransform; - leftWrist.localPosition = new Vector3(0, Settings.Current.LeftHandTrackerOffsetToBottom, Settings.Current.LeftHandTrackerOffsetToBodySide); - leftWrist.localRotation = Quaternion.Euler(Vector3.zero); - leftHandTargetTransform = leftWrist; + var rightLegBendGoalTarget = CreateTransform("RightLegBendGoalTarget", rightKneeTargetTransform, rightCalfOffset); + if (vrik.solver.rightLeg.bendGoal != null) GameObject.Destroy(vrik.solver.rightLeg.bendGoal.gameObject); + + var boneBendGoal = rightLegBendGoalTarget.gameObject.AddComponent(); + boneBendGoal.SetBones("RightLeg", vrik.references.rightThigh, rightCalfOffset, vrik.references.rightFoot, rightLegBendGoalTarget); + vrik.solver.rightLeg.bendGoal = rightLegBendGoalTarget; } - if (RightHandTrackingPoint.DeviceClass == ETrackedDeviceClass.Controller) + //TrackingWatcherにWeight設定用アクションを設定 + SetTrackingWatcher(HMDTrackingPoint, weight => { - var offset = new GameObject("RightWristOffset").transform; - offset.parent = rightHandTargetTransform; - offset.localPosition = new Vector3(0.04f, 0.04f, -0.15f); - offset.localRotation = Quaternion.Euler(60, 0, -90); - offset.localScale = Vector3.one; - rightHandTargetTransform = offset; - } - else if (RightHandTrackingPoint.DeviceClass == ETrackedDeviceClass.GenericTracker) + //Do noting + }); + SetTrackingWatcher(LeftHandTrackingPoint, weight => { - //トラッカーの場合設定のオフセットを適用 - - //お互いのトラッカー同士を向き合わせてオフセットを適用する - var rightWristLookAtTransform = new GameObject("RightWristLookAt").transform; - rightWristLookAtTransform.SetParent(rightHandTargetTransform); - rightWristLookAtTransform.localPosition = Vector3.zero; - rightWristLookAtTransform.localRotation = Quaternion.identity; - rightWristLookAtTransform.LookAt(leftHandTargetTransform); - var rightWrist = new GameObject("RightWristOffset").transform; - rightWrist.parent = rightWristLookAtTransform; - rightWrist.localPosition = new Vector3(0, Settings.Current.RightHandTrackerOffsetToBottom, Settings.Current.RightHandTrackerOffsetToBodySide); - rightWrist.localRotation = Quaternion.Euler(Vector3.zero); - rightHandTargetTransform = rightWrist; - } - - - //手の高さから身長を算出する - // B5 頚窩高 1352.1 - // B1 身長 1654.7 - // ratio = B1 / B5 = 1.2238000147918053398417276828637 - var handHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; - var realHeight = handHeight * 1.2238f; - - Debug.Log($"UserHeight:{realHeight}"); - - - // トラッカー全体のスケールを手の位置に合わせる - // スケールを動かしてから位置を取らないとモデルの位置がずれる - var leftHand = vrik.references.leftHand; - var rightHand = vrik.references.rightHand; - var modelHandDistance = Vector3.Distance(leftHand.position, rightHand.position); - var realHandDistance = Vector3.Distance(leftHandTargetTransform.position, rightHandTargetTransform.position); - var wscale = modelHandDistance / realHandDistance; - handTrackerRoot.localScale = new Vector3(wscale, wscale, wscale); - footTrackerRoot.localScale = new Vector3(wscale, wscale, wscale); - - - //モデルの体の中心を取っておく - var handcenterposition = Vector3.Lerp(vrik.references.leftHand.position, vrik.references.rightHand.position, 0.5f); - handcenterposition = new Vector3(handcenterposition.x, vrik.references.root.position.y, handcenterposition.z); - var pelviscenterposition = new Vector3(vrik.references.pelvis.position.x, vrik.references.root.position.y, vrik.references.pelvis.position.z); - var modelpelviscenterdistance = Vector3.Distance(pelviscenterposition, vrik.references.root.position) * ((pelviscenterposition.z - vrik.references.root.position.z >= 0) ? 1 : -1); - - //両手間のベクトルと上方向の外積が体の正面なので体を回転させる - Vector3 hmdForwardAngle = Vector3.Cross(Vector3.up, leftHandTargetTransform.position - rightHandTargetTransform.position); - currentModel.rotation = Quaternion.LookRotation(hmdForwardAngle); - - - // リアル手と手の中心位置 - var centerposition = Vector3.Lerp(leftHandTargetTransform.position, rightHandTargetTransform.position, 0.5f); - - //身長から腰の位置を算出する - // B12 臍高 965.7 - // B1 身長 1654.7 - // ratio = B13 / B1 = 0.58361032211276968634797848552608 - //腰補正値 0.03303499793413632250187205544126 * B1 - var realPelvisHeight = realHeight * 0.58361f; - var realPelvisPosition = new Vector3(centerposition.x, realPelvisHeight, centerposition.z) + currentModel.forward * (realHeight * 0.02f); - - Debug.Log($"realPelvisHeight:{realPelvisHeight}"); - - var scaledPelvisPosition = new Vector3(centerposition.x, realPelvisHeight * wscale, centerposition.z) + currentModel.forward * (realHeight * 0.02f * wscale); - //アバターの腰のXZ位置をリアルの腰の位置に合わせる - currentModel.position = new Vector3(scaledPelvisPosition.x, currentModel.position.y, scaledPelvisPosition.z) + currentModel.forward * modelpelviscenterdistance; - - //HipボーンはChestボーンとUpperLegに繋がっているので、その場で回転させるとChestを下に引っ張り、UpperLegを上に引っ張ることになるので、UpperLegを中心に回転するようにオフセット設定してかかとが浮かないようにする - var UpperLegOffsetY = vrik.references.pelvis.position.y - (vrik.references.leftThigh.position.y + vrik.references.rightThigh.position.y) / 2; - var UpperLegOffsetZ = vrik.references.pelvis.position.z - (vrik.references.leftThigh.position.z + vrik.references.rightThigh.position.z) / 2; - Debug.Log($"UpperLegOffsetY:{UpperLegOffsetY} UpperLegOffsetZ:{UpperLegOffsetZ}"); - - scaledPelvisPosition += currentModel.forward * UpperLegOffsetZ + currentModel.up * UpperLegOffsetY; - - - - Debug.Log($"wscale:{wscale}"); - Debug.Log($"scaledPelvisHeight:{scaledPelvisPosition.y}"); - - //頭と腰と足と膝と胸のトラッカーを腰の高さにオフセット - var modelPelvisHeight = vrik.references.pelvis.position.y; - var footTrackerOffset = new Vector3(0, modelPelvisHeight - scaledPelvisPosition.y, 0); - footTrackerRoot.position = footTrackerOffset; - - //手と肘トラッカーを手の高さにオフセット - var modelHandHeight = (vrik.references.leftHand.position.y + vrik.references.rightHand.position.y) / 2f; - var realHandHeight = (leftHandTargetTransform.position.y + rightHandTargetTransform.position.y) / 2f; - var realLeftHandHeight = leftHandTargetTransform.position.y; - var realRightHandHeight = rightHandTargetTransform.position.y; - var handTrackerOffset = new Vector3(0, modelHandHeight - realHandHeight, 0); - var leftHandTrackerOffset = new Vector3(0, modelHandHeight - realLeftHandHeight, 0); - var rightHandTrackerOffset = new Vector3(0, modelHandHeight - realRightHandHeight, 0); - - - vrik.enabled = true; - - - // Head - //頭の位置は1cm前後後ろに下げる - var headOffsetPosition = new Vector3(vrik.references.head.position.x, vrik.references.head.position.y, vrik.references.head.position.z - (realHeight * 0.01f * wscale)); - var headOffset = CreateTransform("HeadIKTarget", true, headTarget.TargetTransform, headOffsetPosition, vrik.references.head.rotation); - vrik.solver.spine.headTarget = headOffset; - vrik.solver.spine.positionWeight = 1f; - vrik.solver.spine.rotationWeight = 1f; - - // 頭のトラッキングの補正度合いを変更 - vrik.solver.spine.minHeadHeight = 0; - vrik.solver.spine.neckStiffness = 0.1f; - vrik.solver.spine.headClampWeight = 0; - vrik.solver.spine.maxRootAngle = 20; - - //手のひら正面向けてるのでアバターも向けておく - vrik.references.leftHand.Rotate(new Vector3(-90, 0, 0)); - vrik.references.rightHand.Rotate(new Vector3(-90, 0, 0)); - - // LeftHand - handTrackerRoot.position = leftHandTrackerOffset; - var leftHandOffset = CreateTransform("LeftHandIKTarget", true, leftHandTargetTransform, vrik.references.leftHand); - vrik.solver.leftArm.target = leftHandOffset; - vrik.solver.leftArm.positionWeight = 1f; - vrik.solver.leftArm.rotationWeight = 1f; - - //肩が回りすぎないように - vrik.solver.leftArm.shoulderRotationMode = IKSolverVR.Arm.ShoulderRotationMode.FromTo; - vrik.solver.leftArm.shoulderRotationWeight = 0.3f; - vrik.solver.leftArm.shoulderTwistWeight = 0.7f; - - - // RightHand - handTrackerRoot.position = rightHandTrackerOffset; - var rightHandOffset = CreateTransform("RightHandIKTarget", true, rightHandTargetTransform, vrik.references.rightHand); - vrik.solver.rightArm.target = rightHandOffset; - vrik.solver.rightArm.positionWeight = 1f; - vrik.solver.rightArm.rotationWeight = 1f; - - //肩が回りすぎないように - vrik.solver.rightArm.shoulderRotationMode = IKSolverVR.Arm.ShoulderRotationMode.FromTo; - vrik.solver.rightArm.shoulderRotationWeight = 0.25f; - vrik.solver.rightArm.shoulderTwistWeight = 0.7f; - - - //手の回転軸を少し上に補正 - handTrackerRoot.position = handTrackerOffset + Vector3.up * (realHeight * 0.0145f); - - // Pelvis - var pelvisTargetTransform = PelvisTrackingPoint?.TargetTransform; - if (pelvisTargetTransform != null) + vrik.solver.leftArm.positionWeight = weight; + vrik.solver.leftArm.rotationWeight = weight; + }); + SetTrackingWatcher(RightHandTrackingPoint, weight => { - var pelvisOffset = CreateTransform("PelvisIKTarget", true, pelvisTargetTransform, scaledPelvisPosition + footTrackerOffset, vrik.references.pelvis.rotation); - - - //var PositionSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); - //PositionSphere.transform.position = scaledPelvisPosition + footTrackerOffset; - //PositionSphere.transform.localScale = new Vector3(1f, 0.1f, 0.1f); - - vrik.solver.spine.pelvisTarget = pelvisOffset; - vrik.solver.spine.pelvisPositionWeight = 1f; - vrik.solver.spine.pelvisRotationWeight = 1f; - - vrik.solver.plantFeet = false; - vrik.solver.spine.neckStiffness = 0f; - vrik.solver.spine.maxRootAngle = 180f; - - //頭が腰に近づいたときに猫背になりすぎないように (Final IK v2.1~) - vrik.solver.spine.useAnimatedHeadHeightWeight = 1.0f; - vrik.solver.spine.useAnimatedHeadHeightRange = 0.001f; - vrik.solver.spine.animatedHeadHeightBlend = 0.28f; - } - - // 腰のトラッキングを調整 - vrik.solver.spine.maintainPelvisPosition = 0; // アバターによって腰がグリングリンするのが直ります - - var leftFootTargetTransform = LeftFootTrackingPoint?.TargetTransform; - var rightFootTargetTransform = RightFootTrackingPoint?.TargetTransform; - - if (leftFootTargetTransform != null || rightFootTargetTransform != null) + vrik.solver.rightArm.positionWeight = weight; + vrik.solver.rightArm.rotationWeight = weight; + }); + SetTrackingWatcher(PelvisTrackingPoint, weight => { - // 足トラッカーがあるときはLocomotion無効 - vrik.solver.locomotion.weight = 0.0f; - - if (pelvisTargetTransform != null) - { - //足も腰もある時は頭のPositionを弱くして背骨を曲がりにくくする - vrik.solver.spine.positionWeight = 1.0f; - } - } - else + vrik.solver.spine.pelvisPositionWeight = weight; + vrik.solver.spine.pelvisRotationWeight = weight; + }); + SetTrackingWatcher(LeftFootTrackingPoint, weight => { - // 足トラッカーが無いとき - } - - if (leftFootTargetTransform != null) + //Do noting + }); + SetTrackingWatcher(RightFootTrackingPoint, weight => { - var footBone = vrik.references.leftToes != null ? vrik.references.leftToes : vrik.references.leftFoot; - var leftFootOffset = CreateTransform("LeftFootIKTarget", true, leftFootTargetTransform, footBone); - vrik.solver.leftLeg.target = leftFootOffset; - vrik.solver.leftLeg.positionWeight = 1f; - vrik.solver.leftLeg.rotationWeight = 1f; - - var bendGoal = CreateTransform("LeftFootBendGoal", true, leftFootTargetTransform); - bendGoal.position = footBone.position + currentModel.forward + currentModel.up; - vrik.solver.leftLeg.bendGoal = bendGoal; - vrik.solver.leftLeg.bendGoalWeight = 0.7f; - //vrik.solver.leftLeg.bendToTargetWeight = 1.0f; - } - else + //Do noting + }); + SetTrackingWatcher(LeftElbowTrackingPoint, weight => { - // アバターの足の位置についていくオブジェクト(FinalIKの処理順に影響を受けない) - var leftFootFollow = CreateTransform("LeftFootFollowObject", true, null, vrik.references.leftFoot); - var follower = leftFootFollow.gameObject.AddComponent(); - follower.Target = vrik.references.leftFoot; - - // 腰の子に膝のBendGoal設定用(足トラッカーが無いとき利用される) - var bendGoalTarget = CreateTransform("LeftFootBendGoalTarget", true, leftFootFollow); - bendGoalTarget.localPosition = new Vector3(0, 0.4f, 2); // 正面2m 高さ40cm - bendGoalTarget.localRotation = Quaternion.identity; - vrik.solver.leftLeg.bendGoal = bendGoalTarget; - vrik.solver.leftLeg.bendGoalWeight = 1.0f; - } - - if (rightFootTargetTransform != null) + vrik.solver.leftArm.bendGoalWeight = weight; + }); + SetTrackingWatcher(RightElbowTrackingPoint, weight => { - var footBone = vrik.references.rightToes != null ? vrik.references.rightToes : vrik.references.rightFoot; - var rightFootOffset = CreateTransform("RightFootIKTarget", true, rightFootTargetTransform, footBone); - vrik.solver.rightLeg.target = rightFootOffset; - vrik.solver.rightLeg.positionWeight = 1f; - vrik.solver.rightLeg.rotationWeight = 1f; - - var bendGoal = CreateTransform("RightFootBendGoal", true, rightFootTargetTransform); - bendGoal.position = footBone.position + currentModel.forward + currentModel.up; - vrik.solver.rightLeg.bendGoal = bendGoal; - vrik.solver.rightLeg.bendGoalWeight = 0.7f; - //vrik.solver.rightLeg.bendToTargetWeight = 1.0f; - } - else + vrik.solver.rightArm.bendGoalWeight = weight; + }); + SetTrackingWatcher(LeftKneeTrackingPoint, weight => { - // アバターの足の位置についていくオブジェクト(FinalIKの処理順に影響を受けない) - var rightFootFollow = CreateTransform("RightFootFollowObject", true, null, vrik.references.rightFoot); - var follower = rightFootFollow.gameObject.AddComponent(); - follower.Target = vrik.references.rightFoot; - - // 腰の子に膝のBendGoal設定用(足トラッカーが無いとき利用される) - var bendGoalTarget = CreateTransform("RightFootBendGoalTarget", true, rightFootFollow); - bendGoalTarget.localPosition = new Vector3(0, 0.4f, 2); // 正面2m 高さ40cm - bendGoalTarget.localRotation = Quaternion.identity; - vrik.solver.rightLeg.bendGoal = bendGoalTarget; - vrik.solver.rightLeg.bendGoalWeight = 1.0f; - } - + vrik.solver.leftLeg.bendGoalWeight = weight; + }); + SetTrackingWatcher(RightKneeTrackingPoint, weight => + { + vrik.solver.rightLeg.bendGoalWeight = weight; + }); // 腰トラッカーか両足トラッカーがある場合VRIKRootControllerを使用しないと // (特に)180度後ろを向いたときに正しい膝の方向計算ができません if (pelvisTargetTransform != null || (leftFootTargetTransform != null && rightFootTargetTransform != null)) { - var vrikRootController = vrik.references.root.gameObject.AddComponent(); + var vrikRootController = vrik.references.root.gameObject.GetComponent(); + if (vrikRootController != null) GameObject.DestroyImmediate(vrikRootController); + vrikRootController = vrik.references.root.gameObject.AddComponent(); } if (pelvisTargetTransform != null) { var pelvisWeightAdjuster = vrik.references.root.gameObject.GetComponent(); - if (pelvisWeightAdjuster == null) pelvisWeightAdjuster = vrik.references.root.gameObject.AddComponent(); + if (pelvisWeightAdjuster != null) GameObject.DestroyImmediate(pelvisWeightAdjuster); + pelvisWeightAdjuster = vrik.references.root.gameObject.AddComponent(); pelvisWeightAdjuster.vrik = vrik; } //wristRotationFix = currentModel.AddComponent(); //wristRotationFix.SetVRIK(vrik); + vrik.enabled = true; + vrik.solver.IKPositionWeight = 1.0f; + //頭の位置をかかとの影響がない程度まで上に上げる + if (vrik.solver.plantFeet == false) + { + //頭が腰に近づいたときに猫背になりすぎないように (Final IK v2.1~) + vrik.solver.spine.useAnimatedHeadHeightWeight = 1.0f; + vrik.solver.spine.useAnimatedHeadHeightRange = 0.009f; + vrik.solver.spine.animatedHeadHeightBlend = 0.18f; + + vrik.UpdateSolverExternal(); + var baseFootHeight = vrik.references.leftFoot.position.y; + var headTargetPosition = headOffset.position; + var defaultHeadTargetPosition = headTargetPosition; + var headStep = new Vector3(0, 0.0005f, 0); + while (vrik.references.leftFoot.position.y - baseFootHeight < 0.005f && headTargetPosition.y - defaultHeadTargetPosition.y < 0.4f) + { + headTargetPosition += headStep; + headOffset.position = headTargetPosition; + vrik.UpdateSolverExternal(); + } + headOffset.position -= headStep; + + //ずらした分腰を下げる + vrik.solver.spine.pelvisTarget.position -= headOffset.position - defaultHeadTargetPosition; + } + vrik.UpdateSolverExternal(); //DebugSphere(leftHandTargetTransform); @@ -804,21 +719,33 @@ public static IEnumerator CalibrateTpose(Transform handTrackerRoot, Transform fo yield return null; } - private static Transform CreateTransform(string name, bool AddDestroy, Transform parent) - => CreateTransform(name, AddDestroy, parent, null, null); - private static Transform CreateTransform(string name, bool AddDestroy, Transform parent, Transform placeTransform) - => CreateTransform(name, AddDestroy, parent, placeTransform != null ? placeTransform.position : null as Vector3?, placeTransform != null ? placeTransform.rotation : null as Quaternion?); - private static Transform CreateTransform(string name, bool AddDestroy, Transform parent, Vector3? position, Quaternion? rotation) + + private static void SetTrackingWatcher(TrackingPoint trackingPoint, Action action) { - var newGameObject = new GameObject(name); - //if (AddDestroy) GeneratedGameObjects.Add(newGameObject); - var t = newGameObject.transform; + if (trackingPoint == null) return; + trackingPoint.TargetTransform.GetComponent()?.SetActionOfSetWeight(action); + } + + private static Transform CreateTransform(string name, Transform parent) + => CreateTransform(name, parent, null, null); + private static Transform CreateTransform(string name, Transform parent, Transform placeTransform) + => CreateTransform(name, parent, placeTransform != null ? placeTransform.position : null as Vector3?, placeTransform != null ? placeTransform.rotation : null as Quaternion?); + private static Transform CreateTransform(string name, Transform parent, Vector3? position, Quaternion? rotation) + { + var t = CreateGameObject(name).transform; if (parent != null) t.SetParent(parent, false); if (position != null) t.position = position.Value; if (rotation != null) t.rotation = rotation.Value; return t; } + private static GameObject CreateGameObject(string name) + { + var newGameObject = new GameObject(name); + GeneratedGameObjects.Add(newGameObject); + return newGameObject; + } + private static GameObject DebugSphere(Transform parent) { var PositionSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); diff --git a/Assets/Scripts/Avatar/HandController.cs b/Assets/Scripts/Avatar/HandController.cs index 847378b2..455f428d 100644 --- a/Assets/Scripts/Avatar/HandController.cs +++ b/Assets/Scripts/Avatar/HandController.cs @@ -31,6 +31,12 @@ public void SetDefaultAngle(Animator animator) } } + public void SetNaturalPose() + { + var handAngles = new List { -16, -16, -17, 1, -16, -16, -20, 3, -16, -25, -10, 1, -22, -12, -21, 2, -24, -51, -9, 15 }; + SetHandEulerAngles(true, true, CalcHandEulerAngles(handAngles)); + } + private List FingerBones = new List { diff --git a/Assets/Scripts/Avatar/HandTracking/HandTracking_Skeletal.cs b/Assets/Scripts/Avatar/HandTracking/HandTracking_Skeletal.cs index 75295ad5..081098cc 100644 --- a/Assets/Scripts/Avatar/HandTracking/HandTracking_Skeletal.cs +++ b/Assets/Scripts/Avatar/HandTracking/HandTracking_Skeletal.cs @@ -38,9 +38,6 @@ public class HandTracking_Skeletal : MonoBehaviour private string skeletonLeftHandActionPath = "/actions/default/in/SkeletonLeftHand"; private string skeletonRightHandActionPath = "/actions/default/in/SkeletonRightHand"; - [SerializeField] - private HandController handController; - //Indexで手を完全に開いたとき private Vector3[] indexHandReferences_Paper = new Vector3[] { //左手 @@ -409,7 +406,7 @@ private void UpdateHandController(bool leftEnable, bool rightEnable) eulers.Add(GetVRMAngleFromIndexAngle(index++, rightBoneRotations[SteamVR_Skeleton_JointIndexes.thumbMiddle].eulerAngles)); eulers.Add(GetVRMAngleFromIndexAngle(index++, rightBoneRotations[SteamVR_Skeleton_JointIndexes.thumbProximal].eulerAngles)); - handController.SetHandEulerAngles(leftEnable, rightEnable, eulers); + IKManager.Instance.HandController.SetHandEulerAngles(leftEnable, rightEnable, eulers); } private Vector3 GetVRMAngleFromIndexAngle(int index, Vector3 angle) @@ -460,7 +457,7 @@ private Vector3 GetVRMAngleFromIndexAngle(int index, Vector3 angle) vrmangles[19] = vrmeulersideangle; retindex = 14 + onehandCount; } - var handEulerAngles = handController.CalcHandEulerAngles(vrmangles); + var handEulerAngles = IKManager.Instance.HandController.CalcHandEulerAngles(vrmangles); if (handEulerAngles == null) return Vector3.zero; return handEulerAngles[retindex]; } diff --git a/Assets/Scripts/Avatar/LipTracking.meta b/Assets/Scripts/Avatar/LipTracking.meta deleted file mode 100644 index d1d6fc2f..00000000 --- a/Assets/Scripts/Avatar/LipTracking.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: eac691aa3ea925044b7c9c57dd1d97ed -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs b/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs deleted file mode 100644 index d6ab5601..00000000 --- a/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using ViveSR.anipal.Lip; - -namespace VMC -{ - public class LipTracking_Vive : MonoBehaviour - { - public FaceController faceController; - public ControlWPFWindow controlWPFWindow; - - private Dictionary LipWeightings; - public Dictionary LipShapeToStringKeyMap = new Dictionary(); - public Dictionary LipShapeNameToEnumMap = new Dictionary(); - - - void Awake() - { - controlWPFWindow.SetLipShapeToBlendShapeStringMapAction += SetLipShapeToBlendShapeStringMap; - controlWPFWindow.GetLipShapesStringListFunc = GetLipShapesStringList; - controlWPFWindow.LipTracking_ViveComponent = this; - controlWPFWindow.SRanipal_Lip_FrameworkComponent = GetComponent(); - enabled = false; - } - - void Update() - { - if (SRanipal_Lip_Framework.Status != SRanipal_Lip_Framework.FrameworkStatus.WORKING) return; - - if (LipWeightings == null) - { - if (!SRanipal_Lip_Framework.Instance.EnableLip) - { - enabled = false; - return; - } - - //Get All Shapes - SRanipal_Lip_v2.GetLipWeightings(out LipWeightings); - foreach (var weighting in LipWeightings) - { - if (Enum.IsDefined(typeof(LipShape_v2), weighting.Key)) - { - LipShapeNameToEnumMap[weighting.Key.ToString()] = weighting.Key; - } - } - } - - SRanipal_Lip_v2.GetLipWeightings(out LipWeightings); - - var keyvalues = new Dictionary(); - foreach (var weighting in LipWeightings) - { - if (LipShapeToStringKeyMap.ContainsKey(weighting.Key)) - { - keyvalues[LipShapeToStringKeyMap[weighting.Key]] = weighting.Value; - } - } - if (keyvalues.Any()) - { - faceController.MixPresets(nameof(LipTracking_Vive), keyvalues.Keys.ToArray(), keyvalues.Values.ToArray()); - } - } - - public List GetLipShapesStringList() - { - return LipShapeNameToEnumMap.Keys.ToList(); - } - - public Dictionary GetLipShapeToBlendShapeStringMap() - { - var dict = new Dictionary(); - foreach (var map in LipShapeToStringKeyMap) - { - dict.Add(map.Key.ToString(), map.Value); - } - return dict; - } - - public void SetLipShapeToBlendShapeStringMap(Dictionary stringMap) - { - LipShapeToStringKeyMap.Clear(); - foreach (var map in stringMap) - { - if (LipShapeNameToEnumMap.ContainsKey(map.Key)) - { - LipShapeToStringKeyMap[LipShapeNameToEnumMap[map.Key]] = map.Value; - } - } - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs.meta b/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs.meta deleted file mode 100644 index 56e313a3..00000000 --- a/Assets/Scripts/Avatar/LipTracking/LipTracking_Vive.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 20b4e5c0c4847e8478b223793a2a5cd0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK.meta b/Assets/Scripts/Avatar/MotionTracking.meta similarity index 77% rename from Assets/ExternalPlugins/DVRSDK.meta rename to Assets/Scripts/Avatar/MotionTracking.meta index 76c161bb..75026245 100644 --- a/Assets/ExternalPlugins/DVRSDK.meta +++ b/Assets/Scripts/Avatar/MotionTracking.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 78a757e95e069a448a765f3d0ca76b54 +guid: bf97ddb48dee70e44baf75342aedf680 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Scripts/Avatar/MotionTracking/IKManager.cs b/Assets/Scripts/Avatar/MotionTracking/IKManager.cs new file mode 100644 index 00000000..77c60f64 --- /dev/null +++ b/Assets/Scripts/Avatar/MotionTracking/IKManager.cs @@ -0,0 +1,1247 @@ +using RootMotion.FinalIK; +using sh_akira; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; +using Valve.VR; + +namespace VMC +{ + public class IKManager : MonoBehaviour + { + private static IKManager instance; + public static IKManager Instance => instance; + + + + public CalibrationState CalibrationState = CalibrationState.Uncalibrated; + public PipeCommands.CalibrateType LastCalibrateType = PipeCommands.CalibrateType.Ipose; //最後に行ったキャリブレーションの種類 + public PipeCommands.CalibrationResult CalibrationResult = new PipeCommands.CalibrationResult { Type = PipeCommands.CalibrateType.Invalid }; //初期値は失敗 + + private PipeCommands.CalibrateType currentSelectCalibrateType = PipeCommands.CalibrateType.Ipose; + + [SerializeField] + private ControlWPFWindow controlWPFWindow; + private System.Threading.SynchronizationContext context = null; + public HandController HandController; + + public CameraLookTarget CalibrationCamera; + + public WristRotationFix wristRotationFix; + + public Transform HandTrackerRoot; + public Transform PelvisTrackerRoot; + + private VirtualAvatar virtualAvatar; + + public VRIK vrik = null; + + public Transform generatedObject; + + private Animator animator => virtualAvatar?.animator; + + private SortedDictionary> OnPostUpdateEvents = new SortedDictionary>(); + + private const float LeftLowerArmAngle = -30f; + private const float RightLowerArmAngle = -30f; + private const float LeftUpperArmAngle = -30f; + private const float RightUpperArmAngle = -30f; + private const float LeftHandAngle = -30f; + private const float RightHandAngle = -30f; + + private void Awake() + { + instance = this; + context = System.Threading.SynchronizationContext.Current; + StartCoroutine(AfterUpdateCoroutine()); + } + + private void Start() + { + virtualAvatar = new VirtualAvatar(transform, MotionSource.VRIK); + MotionManager.Instance.AddVirtualAvatar(virtualAvatar); + + VMCEvents.OnCurrentModelChanged += OnCurrentModelChanged; + VMCEvents.OnModelLoaded += OnModelLoaded; + VMCEvents.OnModelUnloading += OnModelUnloading; + controlWPFWindow.server.ReceivedEvent += Server_Received; + + + virtualAvatar.ApplyRootRotation = true; + virtualAvatar.ApplyRootPosition = true; + virtualAvatar.ApplySpine = true; + virtualAvatar.ApplyChest = true; + virtualAvatar.ApplyHead = true; + virtualAvatar.ApplyLeftArm = true; + virtualAvatar.ApplyRightArm = true; + virtualAvatar.ApplyLeftHand = true; + virtualAvatar.ApplyRightHand = true; + virtualAvatar.ApplyLeftLeg = true; + virtualAvatar.ApplyRightLeg = true; + virtualAvatar.ApplyLeftFoot = true; + virtualAvatar.ApplyRightFoot = true; + virtualAvatar.ApplyEye = false; + virtualAvatar.ApplyLeftFinger = true; + virtualAvatar.ApplyRightFinger = true; + + virtualAvatar.IgnoreDefaultBone = false; + } + private void OnCurrentModelChanged(GameObject model) + { + if (model != null) + { + CalibrationState = CalibrationState.Uncalibrated; //キャリブレーション状態を"未キャリブレーション"に設定 + } + } + private void OnModelLoaded(GameObject model) + { + if (model == null) return; + if (Settings.Current.EnableAutoCalibrationOnModelLoad == false) return; + + var snapshot = Settings.Current.LastCalibrationSnapshot; + if (snapshot == null || snapshot.Poses == null || snapshot.Poses.Count == 0) return; + + if (autoCalibrateCoroutine != null) StopCoroutine(autoCalibrateCoroutine); + autoCalibrateCoroutine = StartCoroutine(WaitAndAutoCalibrate()); + } + + private void OnModelUnloading(GameObject model) + { + FinalIKCalibrator.ClearGeneratedGameObjects(); + RemoveComponents(); + } + + #region 自動再キャリブレーション + + //自動再キャリブレーション実行中フラグ(この間はスナップショットを記録し直さない) + private bool isAutoCalibrating = false; + private Coroutine autoCalibrateCoroutine = null; + + /// + /// トラッキング機器が認識されるのを待ってから自動再キャリブレーションを実行する。 + /// (アプリ起動直後のモデル読み込みでは、まだトラッカーが認識されていないことがあるため) + /// + private IEnumerator WaitAndAutoCalibrate() + { + const float TimeoutSeconds = 30f; + var startTime = Time.realtimeSinceStartup; + + while (CanAutoCalibrate() == false) + { + //ユーザーが手動でキャリブレーションを開始した場合は自動実行しない + if (CalibrationState != CalibrationState.Uncalibrated) yield break; + if (Time.realtimeSinceStartup - startTime > TimeoutSeconds) + { + Debug.Log("[AutoCalib] Timed out waiting for trackers. Skip auto calibration."); + yield break; + } + yield return new WaitForSeconds(0.5f); + } + + if (CalibrationState != CalibrationState.Uncalibrated) yield break; + + yield return AutoCalibrateFromSnapshot(); + autoCalibrateCoroutine = null; + } + + /// + /// キャリブレーションに使用したトラッカーの姿勢を記録する。 + /// 記録するのはトラッキング機器から報告される生のローカル姿勢なので、アバターに依存しない。 + /// + private void SaveCalibrationSnapshot(PipeCommands.CalibrateType calibrateType, params TrackingPoint[] trackingPoints) + { + var snapshot = new CalibrationSnapshot + { + CalibrateType = (int)calibrateType, + Poses = new List(), + }; + + foreach (var trackingPoint in trackingPoints) + { + if (trackingPoint == null || string.IsNullOrEmpty(trackingPoint.Name)) continue; + if (snapshot.Poses.Any(d => d.Name == trackingPoint.Name)) continue; //同じデバイスを複数部位に割り当てている場合 + snapshot.Poses.Add(new CalibrationTrackerPose + { + Name = trackingPoint.Name, + Position = trackingPoint.LastLocalPosition, + Rotation = trackingPoint.LastLocalRotation, + }); + } + + Settings.Current.LastCalibrationSnapshot = snapshot.Poses.Count > 0 ? snapshot : null; + } + + /// + /// 記録済みのキャリブレーション姿勢を再現できるか(必要なトラッカーが全て接続されているか) + /// + private bool CanAutoCalibrate() + { + var snapshot = Settings.Current.LastCalibrationSnapshot; + if (snapshot == null || snapshot.Poses == null || snapshot.Poses.Count == 0) return false; + if ((PipeCommands.CalibrateType)snapshot.CalibrateType == PipeCommands.CalibrateType.Invalid) return false; + if (TrackingPointManager.Instance == null) return false; + + //記録時のトラッカーが1つでも見つからない場合は再現できない(接続待ち、または構成変更) + foreach (var pose in snapshot.Poses) + { + if (TrackingPointManager.Instance.TryGetTrackingPoint(pose.Name, out _) == false) + { + return false; + } + } + + //現在割り当てられているトラッカーが記録に含まれていない場合(記録後にトラッカーを追加した等)は、 + //一部だけ実機の姿勢が混ざった状態でキャリブレーションされてしまうため自動実行しない + return AreAssignedTrackersCoveredBy(snapshot); + } + + /// + /// 現在の設定で使用されるトラッカーが全て記録済みスナップショットに含まれているか + /// + private bool AreAssignedTrackersCoveredBy(CalibrationSnapshot snapshot) + { + var headTracker = GetTrackerTransformBySerialNumber(Settings.Current.Head, TargetType.Head); + if (headTracker == null) return false; + var headTransform = headTracker.TargetTransform; + + var assigned = new[] + { + headTracker, + GetTrackerTransformBySerialNumber(Settings.Current.LeftHand, TargetType.LeftArm, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.RightHand, TargetType.RightArm, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.Pelvis, TargetType.Pelvis, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.LeftFoot, TargetType.LeftLeg, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.RightFoot, TargetType.RightLeg, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.LeftElbow, TargetType.LeftElbow, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.RightElbow, TargetType.RightElbow, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.LeftKnee, TargetType.LeftKnee, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.RightKnee, TargetType.RightKnee, headTransform), + GetTrackerTransformBySerialNumber(Settings.Current.Chest, TargetType.Chest, headTransform), + }; + + foreach (var trackingPoint in assigned) + { + if (trackingPoint == null) continue; + if (snapshot.Poses.Any(d => d.Name == trackingPoint.Name) == false) return false; + } + return true; + } + + /// + /// 記録済みのトラッカー姿勢を再現してキャリブレーションを自動実行する。 + /// これにより別のアバターを読み込んだ時やアプリ再起動後もTポーズを取り直す必要がなくなる。 + /// + public IEnumerator AutoCalibrateFromSnapshot() + { + if (isAutoCalibrating) yield break; + if (CanAutoCalibrate() == false) yield break; + + var snapshot = Settings.Current.LastCalibrationSnapshot; + var calibrateType = (PipeCommands.CalibrateType)snapshot.CalibrateType; + var manager = TrackingPointManager.Instance; + + isAutoCalibrating = true; + try + { + //キャリブレーション中に実機の入力で姿勢が動かないように、記録時の姿勢で固定する + var overridePoses = new Dictionary(); + foreach (var pose in snapshot.Poses) + { + overridePoses[pose.Name] = (pose.Position, pose.Rotation); + } + manager.SetPoseOverride(overridePoses); + + currentSelectCalibrateType = calibrateType; + ModelCalibrationInitialize(silent: true); //自動実行なのでキャリブレーション用カメラ等は表示しない + + //トラッキングの更新(ApplyPoint)を通して固定姿勢が反映されるのを待つ。 + //トラッキング入力が無い環境でも再現できるように、直接も適用しておく。 + foreach (var pose in snapshot.Poses) + { + if (manager.TryGetTrackingPoint(pose.Name, out var trackingPoint) && trackingPoint.TargetTransform != null) + { + trackingPoint.TargetTransform.localPosition = pose.Position; + trackingPoint.TargetTransform.localRotation = pose.Rotation; + } + } + //VRIKの再生成やトラッキング更新が落ち着くまで数フレーム待つ + for (int i = 0; i < 3; i++) + { + yield return null; + } + yield return new WaitForEndOfFrame(); + + yield return Calibrate(calibrateType); + + EndCalibrate(); + + Debug.Log($"[AutoCalib] Auto calibration finished. type={calibrateType}"); + } + finally + { + manager.ClearPoseOverride(); + isAutoCalibrating = false; + } + } + + #endregion + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(async s => + { + if (e.CommandType == typeof(PipeCommands.InitializeCalibration)) + { + IKManager.Instance.ModelCalibrationInitialize(); + } + else if (e.CommandType == typeof(PipeCommands.SelectCalibrateMode)) + { + var d = (PipeCommands.SelectCalibrateMode)e.Data; + currentSelectCalibrateType = d.CalibrateType; + SetCalibratePoseToCurrentModel(); + } + else if (e.CommandType == typeof(PipeCommands.Calibrate)) + { + var d = (PipeCommands.Calibrate)e.Data; + StartCoroutine(Calibrate(d.CalibrateType)); + } + else if (e.CommandType == typeof(PipeCommands.EndCalibrate)) + { + EndCalibrate(); + } + else if (e.CommandType == typeof(PipeCommands.GetTrackerSerialNumbers)) + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.ReturnTrackerSerialNumbers { List = GetTrackerSerialNumbers(), CurrentSetting = GetCurrentTrackerSettings() }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetTrackerSerialNumbers)) + { + var d = (PipeCommands.SetTrackerSerialNumbers)e.Data; + SetTrackerSerialNumbers(d); + + } + else if (e.CommandType == typeof(PipeCommands.GetCalibrationSetting)) + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.SetCalibrationSetting + { + EnableOverrideBodyHeight = Settings.Current.EnableOverrideBodyHeight, + OverrideBodyHeight = (int)(Settings.Current.OverrideBodyHeight * 1000), + PelvisOffsetAdjustY = (int)(Settings.Current.PelvisOffsetAdjustY * 1000), + PelvisOffsetAdjustZ = (int)(Settings.Current.PelvisOffsetAdjustZ * 1000), + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetCalibrationSetting)) + { + var d = (PipeCommands.SetCalibrationSetting)e.Data; + Settings.Current.EnableOverrideBodyHeight = d.EnableOverrideBodyHeight; + Settings.Current.OverrideBodyHeight = d.OverrideBodyHeight / 1000f; + Settings.Current.PelvisOffsetAdjustY = d.PelvisOffsetAdjustY / 1000f; + Settings.Current.PelvisOffsetAdjustZ = d.PelvisOffsetAdjustZ / 1000f; + } + else if (e.CommandType == typeof(PipeCommands.SetHandFreeOffset)) + { + var d = (PipeCommands.SetHandFreeOffset)e.Data; + Settings.Current.LeftHandPositionX = d.LeftHandPositionX / 1000f; + Settings.Current.LeftHandPositionY = d.LeftHandPositionY / 1000f; + Settings.Current.LeftHandPositionZ = d.LeftHandPositionZ / 1000f; + Settings.Current.LeftHandRotationX = d.LeftHandRotationX; + Settings.Current.LeftHandRotationY = d.LeftHandRotationY; + Settings.Current.LeftHandRotationZ = d.LeftHandRotationZ; + Settings.Current.RightHandPositionX = d.RightHandPositionX / 1000f; + Settings.Current.RightHandPositionY = d.RightHandPositionY / 1000f; + Settings.Current.RightHandPositionZ = d.RightHandPositionZ / 1000f; + Settings.Current.RightHandRotationX = d.RightHandRotationX; + Settings.Current.RightHandRotationY = d.RightHandRotationY; + Settings.Current.RightHandRotationZ = d.RightHandRotationZ; + Settings.Current.SwivelOffset = d.SwivelOffset; + SetHandFreeOffset(); + } + else if (e.CommandType == typeof(PipeCommands.GetTrackerOffsets)) + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.SetTrackerOffsets + { + LeftHandTrackerOffsetToBodySide = Settings.Current.LeftHandTrackerOffsetToBodySide, + LeftHandTrackerOffsetToBottom = Settings.Current.LeftHandTrackerOffsetToBottom, + RightHandTrackerOffsetToBodySide = Settings.Current.RightHandTrackerOffsetToBodySide, + RightHandTrackerOffsetToBottom = Settings.Current.RightHandTrackerOffsetToBottom + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetTrackerOffsets)) + { + var d = (PipeCommands.SetTrackerOffsets)e.Data; + Settings.Current.LeftHandTrackerOffsetToBodySide = d.LeftHandTrackerOffsetToBodySide; + Settings.Current.LeftHandTrackerOffsetToBottom = d.LeftHandTrackerOffsetToBottom; + Settings.Current.RightHandTrackerOffsetToBodySide = d.RightHandTrackerOffsetToBodySide; + Settings.Current.RightHandTrackerOffsetToBottom = d.RightHandTrackerOffsetToBottom; + } + else if (e.CommandType == typeof(PipeCommands.SetHandAngle)) + { + var d = (PipeCommands.SetHandAngle)e.Data; + HandController.SetHandEulerAngles(d.LeftEnable, d.RightEnable, HandController.CalcHandEulerAngles(d.HandAngles)); + } + + }, null); + } + + private void RemoveComponents() + { + if (virtualAvatar != null) + { + var currentVRIKTimingManager = virtualAvatar.GetComponent(); + if (currentVRIKTimingManager != null) DestroyImmediate(currentVRIKTimingManager); + var rootController = virtualAvatar.GetComponent(); + if (rootController != null) DestroyImmediate(rootController); + var currentvrik = virtualAvatar.GetComponent(); + if (currentvrik != null) + { + currentvrik.solver.OnPostUpdate -= OnPostUpdate; + DestroyImmediate(currentvrik); + } + } + } + + public void ModelCalibrationInitialize() => ModelCalibrationInitialize(false); + + /// + /// キャリブレーションの準備を行う + /// + /// trueの場合はキャリブレーション用カメラやトラッカー位置の表示を行わない(自動再キャリブレーション用) + public void ModelCalibrationInitialize(bool silent) + { + CalibrationState = CalibrationState.WaitingForCalibrating; //キャリブレーション状態を"キャリブレーション待機中"に設定 + + if (virtualAvatar != null) + { + RemoveComponents(); + MotionManager.Instance.ResetVirtualAvatarPose(virtualAvatar); + } + + //SetVRIK(CurrentModel); + if (animator != null) + { + SetCalibratePoseToCurrentModel(); + + //wristRotationFix.SetVRIK(vrik); + + HandController.SetDefaultAngle(animator); + + HandController.SetNaturalPose(); + + //トラッカーのスケールリセット + HandTrackerRoot.localPosition = Vector3.zero; + HandTrackerRoot.localScale = Vector3.one; + PelvisTrackerRoot.localPosition = Vector3.zero; + PelvisTrackerRoot.localScale = Vector3.one; + + if (silent == false) + { + //トラッカー位置の表示 + TrackingPointManager.Instance.SetTrackingPointPositionVisible(true); + + if (CalibrationCamera != null) + { + CalibrationCamera.Target = animator.GetBoneTransform(HumanBodyBones.Head); + CalibrationCamera.gameObject.SetActive(true); + } + } + } + } + + public void ModelInitialize() + { + + SetVRIK(virtualAvatar); + + if (animator != null) + { + wristRotationFix.SetVRIK(vrik); + + animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).eulerAngles = new Vector3(LeftLowerArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightLowerArm).eulerAngles = new Vector3(RightLowerArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).eulerAngles = new Vector3(LeftUpperArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightUpperArm).eulerAngles = new Vector3(RightUpperArmAngle, 0, 0); + + HandController.SetDefaultAngle(animator); + + //初期の指を自然に閉じたポーズにする + HandController.SetNaturalPose(); + } + //SetTrackersToVRIK(); + } + + + private void SetCalibratePoseToCurrentModel() + { + if (animator != null) + { + if (currentSelectCalibrateType == PipeCommands.CalibrateType.Ipose) + { + animator.GetBoneTransform(HumanBodyBones.LeftShoulder).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightShoulder).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(0, 0, 80); + animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(0, 0, 5); + animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(0, 0, -80); + animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(0, 0, -5); + animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(0, 0, 0); + } + else + { + animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(LeftLowerArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(RightLowerArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(LeftUpperArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(RightUpperArmAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(LeftHandAngle, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(RightHandAngle, 0, 0); + } + } + } + + + public void ResetTrackerScale() + { + //jsonが正しくデコードできていなければ無視する + if (Settings.Current == null) + { + return; + } + + //トラッカーのルートスケールを初期値に戻す + HandTrackerRoot.localScale = new Vector3(1.0f, 1.0f, 1.0f); + PelvisTrackerRoot.localScale = new Vector3(1.0f, 1.0f, 1.0f); + HandTrackerRoot.position = Vector3.zero; + PelvisTrackerRoot.position = Vector3.zero; + + //スケール変更時の位置オフセット設定 + var handTrackerOffset = HandTrackerRoot.GetComponent(); + var footTrackerOffset = PelvisTrackerRoot.GetComponent(); + handTrackerOffset.ResetTargetAndPosition(); + footTrackerOffset.ResetTargetAndPosition(); + } + #region Calibration + + public void FixLegDirection(VirtualAvatar targetHumanoidModel) + { + var avatarForward = targetHumanoidModel.transform.forward; + var animator = targetHumanoidModel.GetComponent(); + + var leftUpperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); + var leftLowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg); + var leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); + var leftFootDefaultRotation = leftFoot.rotation; + var leftFootTargetPosition = new Vector3(leftFoot.position.x, leftFoot.position.y, leftFoot.position.z); + LookAtBones(leftFootTargetPosition + avatarForward * 0.03f, leftUpperLeg, leftLowerLeg); + LookAtBones(leftFootTargetPosition, leftLowerLeg, leftFoot); + leftFoot.rotation = leftFootDefaultRotation; + + var rightUpperLeg = animator.GetBoneTransform(HumanBodyBones.RightUpperLeg); + var rightLowerLeg = animator.GetBoneTransform(HumanBodyBones.RightLowerLeg); + var rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot); + var rightFootDefaultRotation = rightFoot.rotation; + var rightFootTargetPosition = new Vector3(rightFoot.position.x, rightFoot.position.y, rightFoot.position.z); + LookAtBones(rightFootTargetPosition + avatarForward * 0.03f, rightUpperLeg, rightLowerLeg); + LookAtBones(rightFootTargetPosition, rightLowerLeg, rightFoot); + rightFoot.rotation = rightFootDefaultRotation; + } + + public void FixArmDirection(VirtualAvatar targetHumanoidModel) + { + var avatarForward = targetHumanoidModel.transform.forward; + var animator = targetHumanoidModel.GetComponent(); + + var leftShoulder = animator.GetBoneTransform(HumanBodyBones.LeftShoulder); + var leftUpperArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm); + var leftLowerArm = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm); + var leftHand = animator.GetBoneTransform(HumanBodyBones.LeftHand); + var leftHandDefaultRotation = leftHand.rotation; + var leftHandTargetPosition = new Vector3(leftHand.position.x, leftHand.position.y, leftHand.position.z); + LookAtBones(leftHandTargetPosition + avatarForward * 0.01f, leftShoulder, leftUpperArm); + LookAtBones(leftHandTargetPosition - avatarForward * 0.01f, leftUpperArm, leftLowerArm); + LookAtBones(leftHandTargetPosition, leftLowerArm, leftHand); + leftHand.rotation = leftHandDefaultRotation; + + var rightShoulder = animator.GetBoneTransform(HumanBodyBones.RightShoulder); + var rightUpperArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm); + var rightLowerArm = animator.GetBoneTransform(HumanBodyBones.RightLowerArm); + var rightHand = animator.GetBoneTransform(HumanBodyBones.RightHand); + var rightHandDefaultRotation = rightHand.rotation; + var rightHandTargetPosition = new Vector3(rightHand.position.x, rightHand.position.y, rightHand.position.z); + LookAtBones(rightHandTargetPosition + avatarForward * 0.01f, rightShoulder, rightUpperArm); + LookAtBones(rightHandTargetPosition - avatarForward * 0.01f, rightUpperArm, rightLowerArm); + LookAtBones(rightHandTargetPosition, rightLowerArm, rightHand); + rightHand.rotation = rightHandDefaultRotation; + } + + private void LookAtBones(Vector3 lookTargetPosition, params Transform[] bones) + { + for (int i = 0; i < bones.Length - 1; i++) + { + bones[i].rotation = Quaternion.FromToRotation((bones[i].position - bones[i + 1].position).normalized, (bones[i].position - lookTargetPosition).normalized) * bones[i].rotation; + } + } + + private void SetVRIK(VirtualAvatar virtualAvatar) + { + //膝のボーンの曲がる方向で膝の向きが決まってしまうため、強制的に膝のボーンを少し前に曲げる + var leftOffset = Vector3.zero; + var rightOffset = Vector3.zero; + if (animator != null && Settings.Current.FixKneeRotation) + { + //leftOffset = fixKneeBone(animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg), animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftFoot)); + //rightOffset = fixKneeBone(animator.GetBoneTransform(HumanBodyBones.RightUpperLeg), animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightFoot)); + //fixPelvisBone(animator.GetBoneTransform(HumanBodyBones.Spine), animator.GetBoneTransform(HumanBodyBones.Hips)); + FixLegDirection(virtualAvatar); + } + + if (animator != null && Settings.Current.FixElbowRotation) + { + FixArmDirection(virtualAvatar); + } + + vrik = virtualAvatar.AddComponent(); + virtualAvatar.AddComponent(); + vrik.AutoDetectReferences(); + + //親指の方向の検出に失敗すると腕の回転もおかしくなる + vrik.solver.leftArm.palmToThumbAxis = new Vector3(0, 0, 1); + vrik.solver.rightArm.palmToThumbAxis = new Vector3(0, 0, 1); + + vrik.solver.FixTransforms(); + + vrik.solver.IKPositionWeight = 0f; + vrik.solver.leftArm.stretchCurve = new AnimationCurve(); + vrik.solver.rightArm.stretchCurve = new AnimationCurve(); + vrik.UpdateSolverExternal(); + + vrik.solver.OnPostUpdate += OnPostUpdate; + + //膝のボーンの曲がる方向で膝の向きが決まってしまうため、強制的に膝のボーンを少し前に曲げる + //if (animator != null) + //{ + // unfixKneeBone(leftOffset, animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftFoot)); + // unfixKneeBone(rightOffset, animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightFoot)); + //} + //if (animator != null) + //{ + // var leftWrist = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).gameObject; + // var rightWrist = animator.GetBoneTransform(HumanBodyBones.RightLowerArm).gameObject; + // var leftRelaxer = leftWrist.AddComponent(); + // var rightRelaxer = rightWrist.AddComponent(); + // leftRelaxer.ik = vrik; + // rightRelaxer.ik = vrik; + //} + } + + private List> GetTrackerSerialNumbers() + { + var list = new List>(); + foreach (var trackingPoint in TrackingPointManager.Instance.GetTrackingPoints()) + { + if (trackingPoint.DeviceClass == ETrackedDeviceClass.HMD) + { + list.Add(Tuple.Create("HMD", trackingPoint.Name)); + } + else if (trackingPoint.DeviceClass == ETrackedDeviceClass.Controller) + { + list.Add(Tuple.Create("コントローラー", trackingPoint.Name)); + } + else if (trackingPoint.DeviceClass == ETrackedDeviceClass.GenericTracker) + { + list.Add(Tuple.Create("トラッカー", trackingPoint.Name)); + } + else + { + list.Add(Tuple.Create("Unknown", trackingPoint.Name)); + } + } + return list; + } + + private PipeCommands.SetTrackerSerialNumbers GetCurrentTrackerSettings() + { + var deviceDictionary = new Dictionary + { + {ETrackedDeviceClass.HMD, "HMD"}, + {ETrackedDeviceClass.Controller, "コントローラー"}, + {ETrackedDeviceClass.GenericTracker, "トラッカー"}, + {ETrackedDeviceClass.TrackingReference, "ベースステーション"}, + {ETrackedDeviceClass.Invalid, "割り当てしない"}, + }; + return new PipeCommands.SetTrackerSerialNumbers + { + Head = Tuple.Create(deviceDictionary[Settings.Current.Head.Item1], Settings.Current.Head.Item2), + LeftHand = Tuple.Create(deviceDictionary[Settings.Current.LeftHand.Item1], Settings.Current.LeftHand.Item2), + RightHand = Tuple.Create(deviceDictionary[Settings.Current.RightHand.Item1], Settings.Current.RightHand.Item2), + Pelvis = Tuple.Create(deviceDictionary[Settings.Current.Pelvis.Item1], Settings.Current.Pelvis.Item2), + LeftFoot = Tuple.Create(deviceDictionary[Settings.Current.LeftFoot.Item1], Settings.Current.LeftFoot.Item2), + RightFoot = Tuple.Create(deviceDictionary[Settings.Current.RightFoot.Item1], Settings.Current.RightFoot.Item2), + LeftElbow = Tuple.Create(deviceDictionary[Settings.Current.LeftElbow.Item1], Settings.Current.LeftElbow.Item2), + RightElbow = Tuple.Create(deviceDictionary[Settings.Current.RightElbow.Item1], Settings.Current.RightElbow.Item2), + LeftKnee = Tuple.Create(deviceDictionary[Settings.Current.LeftKnee.Item1], Settings.Current.LeftKnee.Item2), + RightKnee = Tuple.Create(deviceDictionary[Settings.Current.RightKnee.Item1], Settings.Current.RightKnee.Item2), + Chest = Tuple.Create(deviceDictionary[Settings.Current.Chest.Item1], Settings.Current.Chest.Item2), + }; + } + + private void SetTrackerSerialNumbers(PipeCommands.SetTrackerSerialNumbers data) + { + var deviceDictionary = new Dictionary + { + {"HMD", ETrackedDeviceClass.HMD }, + {"コントローラー", ETrackedDeviceClass.Controller }, + {"トラッカー", ETrackedDeviceClass.GenericTracker }, + {"ベースステーション", ETrackedDeviceClass.TrackingReference }, + {"割り当てしない", ETrackedDeviceClass.Invalid }, + }; + + Settings.Current.Head = Tuple.Create(deviceDictionary[data.Head.Item1], data.Head.Item2); + Settings.Current.LeftHand = Tuple.Create(deviceDictionary[data.LeftHand.Item1], data.LeftHand.Item2); + Settings.Current.RightHand = Tuple.Create(deviceDictionary[data.RightHand.Item1], data.RightHand.Item2); + Settings.Current.Pelvis = Tuple.Create(deviceDictionary[data.Pelvis.Item1], data.Pelvis.Item2); + Settings.Current.LeftFoot = Tuple.Create(deviceDictionary[data.LeftFoot.Item1], data.LeftFoot.Item2); + Settings.Current.RightFoot = Tuple.Create(deviceDictionary[data.RightFoot.Item1], data.RightFoot.Item2); + Settings.Current.LeftElbow = Tuple.Create(deviceDictionary[data.LeftElbow.Item1], data.LeftElbow.Item2); + Settings.Current.RightElbow = Tuple.Create(deviceDictionary[data.RightElbow.Item1], data.RightElbow.Item2); + Settings.Current.LeftKnee = Tuple.Create(deviceDictionary[data.LeftKnee.Item1], data.LeftKnee.Item2); + Settings.Current.RightKnee = Tuple.Create(deviceDictionary[data.RightKnee.Item1], data.RightKnee.Item2); + Settings.Current.Chest = Tuple.Create(deviceDictionary[data.Chest.Item1], data.Chest.Item2); + SetVRIKTargetTrackers(); + } + + private enum TargetType + { + Head, Pelvis, LeftArm, RightArm, LeftLeg, RightLeg, LeftElbow, RightElbow, LeftKnee, RightKnee, Chest + } + + private TrackingPoint GetTrackerTransformBySerialNumber(Tuple serial, TargetType setTo, Transform headTracker = null) + { + var manager = TrackingPointManager.Instance; + if (serial.Item1 == ETrackedDeviceClass.HMD) + { + if (string.IsNullOrEmpty(serial.Item2)) + { + return manager.GetTrackingPoints(ETrackedDeviceClass.HMD).FirstOrDefault(); + } + else if (manager.TryGetTrackingPoint(serial.Item2, out var hmdTrackingPoint)) + { + return hmdTrackingPoint; + } + } + else if (serial.Item1 == ETrackedDeviceClass.Controller) + { + var controllers = manager.GetTrackingPoints(ETrackedDeviceClass.Controller).Where(d => d.Name.Contains("LIV Virtual Camera") == false); + TrackingPoint ret = null; + foreach (var controller in controllers) + { + if (controller != null && controller.Name == serial.Item2) + { + if (setTo == TargetType.LeftArm || setTo == TargetType.RightArm) + { + ret = controller; + break; + } + return controller; + } + } + if (ret == null) + { + var controllerTrackingPoints = controllers.Select((d, i) => new { index = i, pos = headTracker.InverseTransformDirection(d.TargetTransform.position - headTracker.position), trackingPoint = d }) + .OrderBy(d => d.pos.x) + .Select(d => d.trackingPoint); + if (setTo == TargetType.LeftArm) ret = controllerTrackingPoints.ElementAtOrDefault(0); + if (setTo == TargetType.RightArm) ret = controllerTrackingPoints.ElementAtOrDefault(1); + } + return ret; + } + else if (serial.Item1 == ETrackedDeviceClass.GenericTracker) + { + foreach (var tracker in manager.GetTrackingPoints(ETrackedDeviceClass.GenericTracker).Where(d => d.Name.Contains("LIV Virtual Camera") == false && !(Settings.Current.VirtualMotionTrackerEnable && d.Name.Contains($"VMT_{Settings.Current.VirtualMotionTrackerNo}")))) + { + if (tracker != null && tracker.Name == serial.Item2) + { + return tracker; + } + } + if (string.IsNullOrEmpty(serial.Item2) == false) return null; //Serialあるのに見つからなかったらnull + + var trackerIds = new List(); + + if (Settings.Current.Head.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.Head.Item2); + if (Settings.Current.LeftHand.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftHand.Item2); + if (Settings.Current.RightHand.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightHand.Item2); + if (Settings.Current.Pelvis.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.Pelvis.Item2); + if (Settings.Current.LeftFoot.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftFoot.Item2); + if (Settings.Current.RightFoot.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightFoot.Item2); + if (Settings.Current.LeftElbow.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftElbow.Item2); + if (Settings.Current.RightElbow.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightElbow.Item2); + if (Settings.Current.LeftKnee.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftKnee.Item2); + if (Settings.Current.RightKnee.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightKnee.Item2); + if (Settings.Current.Chest.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.Chest.Item2); + + //ここに来るときは腰か足のトラッカー自動認識になってるとき + //割り当てられていないトラッカーリスト + var autoTrackers = manager.GetTrackingPoints(ETrackedDeviceClass.GenericTracker).Where(d => d.TrackingWatcher.ok).Where(d => trackerIds.Contains(d.Name) == false).Select((d, i) => new { index = i, pos = headTracker.InverseTransformDirection(d.TargetTransform.position - headTracker.position), trackingPoint = d }); + if (autoTrackers.Any()) + { + var count = autoTrackers.Count(); + if (count >= 3) + { + if (setTo == TargetType.Pelvis) + { //腰は一番高い位置にあるトラッカー + return autoTrackers.OrderByDescending(d => d.pos.y).Select(d => d.trackingPoint).First(); + } + } + if (count >= 2) + { + if (setTo == TargetType.LeftLeg) + { + return autoTrackers.OrderBy(d => d.pos.y).Take(2).OrderBy(d => d.pos.x).Select(d => d.trackingPoint).First(); + } + else if (setTo == TargetType.RightLeg) + { + return autoTrackers.OrderBy(d => d.pos.y).Take(2).OrderByDescending(d => d.pos.x).Select(d => d.trackingPoint).First(); + } + } + } + } + return null; + } + + private void SetVRIKTargetTrackers() + { + if (vrik == null) { return; } //まだmodelがない + + vrik.solver.spine.headTarget = GetTrackerTransformBySerialNumber(Settings.Current.Head, TargetType.Head)?.TargetTransform; + vrik.solver.spine.headClampWeight = 0.38f; + + vrik.solver.spine.pelvisTarget = GetTrackerTransformBySerialNumber(Settings.Current.Pelvis, TargetType.Pelvis, vrik.solver.spine.headTarget)?.TargetTransform; + if (vrik.solver.spine.pelvisTarget != null) + { + vrik.solver.spine.pelvisPositionWeight = 1f; + vrik.solver.spine.pelvisRotationWeight = 1f; + vrik.solver.plantFeet = false; + vrik.solver.spine.neckStiffness = 0f; + vrik.solver.spine.maxRootAngle = 180f; + } + else + { + vrik.solver.spine.pelvisPositionWeight = 0f; + vrik.solver.spine.pelvisRotationWeight = 0f; + vrik.solver.plantFeet = true; + vrik.solver.spine.neckStiffness = 1f; + vrik.solver.spine.maxRootAngle = 0f; + } + + vrik.solver.leftArm.target = GetTrackerTransformBySerialNumber(Settings.Current.LeftHand, TargetType.LeftArm, vrik.solver.spine.headTarget)?.TargetTransform; + if (vrik.solver.leftArm.target != null) + { + vrik.solver.leftArm.positionWeight = 1f; + vrik.solver.leftArm.rotationWeight = 1f; + } + else + { + vrik.solver.leftArm.positionWeight = 0f; + vrik.solver.leftArm.rotationWeight = 0f; + } + + vrik.solver.rightArm.target = GetTrackerTransformBySerialNumber(Settings.Current.RightHand, TargetType.RightArm, vrik.solver.spine.headTarget)?.TargetTransform; + if (vrik.solver.rightArm.target != null) + { + vrik.solver.rightArm.positionWeight = 1f; + vrik.solver.rightArm.rotationWeight = 1f; + } + else + { + vrik.solver.rightArm.positionWeight = 0f; + vrik.solver.rightArm.rotationWeight = 0f; + } + + vrik.solver.leftLeg.target = GetTrackerTransformBySerialNumber(Settings.Current.LeftFoot, TargetType.LeftLeg, vrik.solver.spine.headTarget)?.TargetTransform; + if (vrik.solver.leftLeg.target != null) + { + vrik.solver.leftLeg.positionWeight = 1f; + vrik.solver.leftLeg.rotationWeight = 1f; + } + else + { + vrik.solver.leftLeg.positionWeight = 0f; + vrik.solver.leftLeg.rotationWeight = 0f; + } + + vrik.solver.rightLeg.target = GetTrackerTransformBySerialNumber(Settings.Current.RightFoot, TargetType.RightLeg, vrik.solver.spine.headTarget)?.TargetTransform; + if (vrik.solver.rightLeg.target != null) + { + vrik.solver.rightLeg.positionWeight = 1f; + vrik.solver.rightLeg.rotationWeight = 1f; + } + else + { + vrik.solver.rightLeg.positionWeight = 0f; + vrik.solver.rightLeg.rotationWeight = 0f; + } + } + + private Transform leftHandFreeOffsetRotation; + private Transform rightHandFreeOffsetRotation; + private Transform leftHandFreeOffsetPosition; + private Transform rightHandFreeOffsetPosition; + + public IEnumerator Calibrate(PipeCommands.CalibrateType calibrateType) + { + LastCalibrateType = calibrateType;//最後に実施したキャリブレーションタイプとして記録 + + + //開始状態を格納 + CalibrationResult = new PipeCommands.CalibrationResult + { + Type = calibrateType + }; + + + if (animator == null) + { + Debug.LogError("[Calib Fail] No avatar found. (animator == null)"); + yield break; + } + + animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(0, 0, 0); + animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(0, 0, 0); + + SetVRIK(virtualAvatar); + wristRotationFix.SetVRIK(vrik); + + var headTracker = GetTrackerTransformBySerialNumber(Settings.Current.Head, TargetType.Head); + var leftHandTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftHand, TargetType.LeftArm, headTracker?.TargetTransform); + var rightHandTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightHand, TargetType.RightArm, headTracker?.TargetTransform); + var bodyTracker = GetTrackerTransformBySerialNumber(Settings.Current.Pelvis, TargetType.Pelvis, headTracker?.TargetTransform); + var leftFootTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftFoot, TargetType.LeftLeg, headTracker?.TargetTransform); + var rightFootTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightFoot, TargetType.RightLeg, headTracker?.TargetTransform); + var leftElbowTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftElbow, TargetType.LeftElbow, headTracker?.TargetTransform); + var rightElbowTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightElbow, TargetType.RightElbow, headTracker?.TargetTransform); + var leftKneeTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftKnee, TargetType.LeftKnee, headTracker?.TargetTransform); + var rightKneeTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightKnee, TargetType.RightKnee, headTracker?.TargetTransform); + var chestTracker = GetTrackerTransformBySerialNumber(Settings.Current.Chest, TargetType.Chest, headTracker?.TargetTransform); + + ClearChildren(headTracker, leftHandTracker, rightHandTracker, bodyTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker); + + var settings = new RootMotion.FinalIK.VRIKCalibrator.Settings(); + + yield return new WaitForEndOfFrame(); + + var leftHandOffset = Vector3.zero; + var rightHandOffset = Vector3.zero; + + //トラッカー + //xをプラス方向に動かすとトラッカーの左(LEDを上に見たとき)に進む + //yをプラス方向に動かすとトラッカーの上(LED方向)に進む + //zをマイナス方向に動かすとトラッカーの底面に向かって進む + + if (Settings.Current.LeftHand.Item1 == ETrackedDeviceClass.GenericTracker) + { + //角度補正(左手なら右のトラッカーに向けた)後 + //xを+方向は体の正面に向かって進む + //yを+方向は体の上(天井方向)に向かって進む + //zを+方向は体中心(左手なら右手の方向)に向かって進む + leftHandOffset = new Vector3(1.0f, Settings.Current.LeftHandTrackerOffsetToBottom, Settings.Current.LeftHandTrackerOffsetToBodySide); // Vector3 (IsEnable, ToTrackerBottom, ToBodySide) + } + if (Settings.Current.RightHand.Item1 == ETrackedDeviceClass.GenericTracker) + { + //角度補正(左手なら右のトラッカーに向けた)後 + //xを-方向は体の正面に向かって進む + //yを+方向は体の上(天井方向)に向かって進む + //zを+方向は体中心(左手なら右手の方向)に向かって進む + rightHandOffset = new Vector3(1.0f, Settings.Current.RightHandTrackerOffsetToBottom, Settings.Current.RightHandTrackerOffsetToBodySide); // Vector3 (IsEnable, ToTrackerBottom, ToBodySide) + } + + TrackingPointManager.Instance.ClearTrackingWatcher(); + + foreach (Transform child in generatedObject) + { + DestroyImmediate(child.gameObject); + } + + var trackerPositions = new TrackerPositions + { + Head = new TrackerPosition(headTracker), + LeftHand = new TrackerPosition(leftHandTracker), + RightHand = new TrackerPosition(rightHandTracker), + Pelvis = new TrackerPosition(bodyTracker), + LeftFoot = new TrackerPosition(leftFootTracker), + RightFoot = new TrackerPosition(rightFootTracker), + LeftElbow = new TrackerPosition(leftElbowTracker), + RightElbow = new TrackerPosition(rightElbowTracker), + LeftKnee = new TrackerPosition(leftKneeTracker), + RightKnee = new TrackerPosition(rightKneeTracker), + Chest = new TrackerPosition(chestTracker), + }; + + try + { + var trackerPositionsJson = JsonUtility.ToJson(trackerPositions); + string path = Path.GetFullPath(Application.dataPath + "/../TrackerPositions.json"); + var directoryName = Path.GetDirectoryName(path); + if (Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); + File.WriteAllText(path, Json.Serializer.ToReadable(trackerPositionsJson)); + } + catch { } + + //別のアバターを読み込んだ時に同じ姿勢でキャリブレーションをやり直せるように、 + //このキャリブレーションで使用したトラッカーの姿勢を記録する(自動再キャリブレーション用)。 + //自動再キャリブレーション自身による実行時は、記録済みの姿勢をそのまま使うため記録し直さない。 + if (isAutoCalibrating == false) + { + SaveCalibrationSnapshot(calibrateType, headTracker, leftHandTracker, rightHandTracker, bodyTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker); + } + + // 胸だけでも良い感じに動くことが分かったので、オプション対応に変更 + if (Settings.Current.TrackerReassignmentWhenChestAvailable && bodyTracker == null && chestTracker != null) + { + Debug.LogWarning("*No waist tracker. Reassign chest tracker to waist."); + bodyTracker = chestTracker; + chestTracker = null; + } + + if (calibrateType == PipeCommands.CalibrateType.Ipose || calibrateType == PipeCommands.CalibrateType.Tpose) + { + yield return FinalIKCalibrator.Calibrate(calibrateType == PipeCommands.CalibrateType.Ipose ? FinalIKCalibrator.CalibrateMode.Ipose : FinalIKCalibrator.CalibrateMode.Tpose, HandTrackerRoot, PelvisTrackerRoot, vrik, settings, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker, generatedObject); + } + else if (calibrateType == PipeCommands.CalibrateType.FixedHand) + { + yield return Calibrator.CalibrateFixedHand(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, leftHandOffset, rightHandOffset, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker); + } + else if (calibrateType == PipeCommands.CalibrateType.FixedHandWithGround) + { + yield return Calibrator.CalibrateFixedHandWithGround(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, leftHandOffset, rightHandOffset, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker); + } + else if (calibrateType == PipeCommands.CalibrateType.Default) + { + yield return Calibrator.CalibrateScaled(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, leftHandOffset, rightHandOffset, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker, chestTracker); + } + + vrik.solver.IKPositionWeight = 1.0f; + if (leftFootTracker == null && rightFootTracker == null) + { + vrik.solver.plantFeet = true; + vrik.solver.locomotion.weight = 1.0f; + var rootController = vrik.references.root.GetComponent(); + if (rootController != null) GameObject.Destroy(rootController); + } + + vrik.solver.locomotion.footDistance = 0.08f; + vrik.solver.locomotion.stepThreshold = 0.05f; + vrik.solver.locomotion.angleThreshold = 10f; + vrik.solver.locomotion.maxVelocity = 0.04f; + vrik.solver.locomotion.velocityFactor = 0.04f; + vrik.solver.locomotion.rootSpeed = 40; + vrik.solver.locomotion.stepSpeed = 2; + vrik.solver.locomotion.offset = new Vector3(0, 0, 0.03f); + + Settings.Current.headTracker = StoreTransform.Create(headTracker?.TargetTransform); + Settings.Current.bodyTracker = StoreTransform.Create(bodyTracker?.TargetTransform); + Settings.Current.leftHandTracker = StoreTransform.Create(leftHandTracker?.TargetTransform); + Settings.Current.rightHandTracker = StoreTransform.Create(rightHandTracker?.TargetTransform); + Settings.Current.leftFootTracker = StoreTransform.Create(leftFootTracker?.TargetTransform); + Settings.Current.rightFootTracker = StoreTransform.Create(rightFootTracker?.TargetTransform); + Settings.Current.leftElbowTracker = StoreTransform.Create(leftElbowTracker?.TargetTransform); + Settings.Current.rightElbowTracker = StoreTransform.Create(rightElbowTracker?.TargetTransform); + Settings.Current.leftKneeTracker = StoreTransform.Create(leftKneeTracker?.TargetTransform); + Settings.Current.rightKneeTracker = StoreTransform.Create(rightKneeTracker?.TargetTransform); + Settings.Current.chestTracker = StoreTransform.Create(chestTracker?.TargetTransform); + + var calibratedLeftHandTransform = leftHandTracker?.TargetTransform?.OfType().FirstOrDefault(); + var calibratedRightHandTransform = rightHandTracker?.TargetTransform?.OfType().FirstOrDefault(); + + if (calibratedLeftHandTransform != null && calibratedRightHandTransform != null) + { + leftHandFreeOffsetRotation = new GameObject(nameof(leftHandFreeOffsetRotation)).transform; + rightHandFreeOffsetRotation = new GameObject(nameof(rightHandFreeOffsetRotation)).transform; + leftHandFreeOffsetRotation.SetParent(leftHandTracker?.TargetTransform); + rightHandFreeOffsetRotation.SetParent(rightHandTracker?.TargetTransform); + leftHandFreeOffsetRotation.localPosition = Vector3.zero; + leftHandFreeOffsetRotation.localRotation = Quaternion.identity; + leftHandFreeOffsetRotation.localScale = Vector3.one; + rightHandFreeOffsetRotation.localPosition = Vector3.zero; + rightHandFreeOffsetRotation.localRotation = Quaternion.identity; + rightHandFreeOffsetRotation.localScale = Vector3.one; + + leftHandFreeOffsetPosition = new GameObject(nameof(leftHandFreeOffsetPosition)).transform; + rightHandFreeOffsetPosition = new GameObject(nameof(rightHandFreeOffsetPosition)).transform; + leftHandFreeOffsetPosition.SetParent(leftHandFreeOffsetRotation); + rightHandFreeOffsetPosition.SetParent(rightHandFreeOffsetRotation); + leftHandFreeOffsetPosition.localPosition = Vector3.zero; + leftHandFreeOffsetPosition.localRotation = Quaternion.identity; + leftHandFreeOffsetPosition.localScale = Vector3.one; + rightHandFreeOffsetPosition.localPosition = Vector3.zero; + rightHandFreeOffsetPosition.localRotation = Quaternion.identity; + rightHandFreeOffsetPosition.localScale = Vector3.one; + + calibratedLeftHandTransform.parent = leftHandFreeOffsetPosition; + calibratedRightHandTransform.parent = rightHandFreeOffsetPosition; + } + + yield return null; + + if (CalibrationResult.Type == PipeCommands.CalibrateType.Invalid) + { + CalibrationState = CalibrationState.Uncalibrated; //キャリブレーションタイプがInvalidになっているときはキャリブレーション失敗 + } + else + { + CalibrationState = CalibrationState.Calibrating; //キャリブレーション状態を"キャリブレーション中"に設定 + } + } + + private void ClearChildren(params TrackingPoint[] Parents) => ClearChildren(Parents.Select(d => d?.TargetTransform).ToArray()); + + private void ClearChildren(params Transform[] Parents) + { + foreach (var parent in Parents) + { + if (parent != null) + { + foreach (Transform child in parent) + { + Destroy(child.gameObject); + } + } + } + } + + public void EndCalibrate() + { + //トラッカー位置の非表示 + TrackingPointManager.Instance.SetTrackingPointPositionVisible(false); + + if (CalibrationCamera != null) + { + CalibrationCamera.gameObject.SetActive(false); + } + SetHandFreeOffset(); + //SetTrackersToVRIK(); + + //直前がキャリブレーション実行中なら + if (CalibrationState == CalibrationState.Calibrating) + { + CalibrationState = CalibrationState.Calibrated; //キャリブレーション状態を"キャリブレーション完了"に設定 + + context.Post(async (_) => + { + //最終結果を送信 + await controlWPFWindow.server.SendCommandAsync(CalibrationResult); + }, null); + } + else + { + //キャンセルされたなど + CalibrationState = CalibrationState.Uncalibrated; //キャリブレーション状態を"未キャリブレーション"に設定 + + RemoveComponents(); + MotionManager.Instance.ResetVirtualAvatarPose(virtualAvatar); + ModelInitialize(); + } + } + + public void SetHandFreeOffset() + { + if (vrik == null) return; + if (leftHandFreeOffsetRotation == null) return; + if (rightHandFreeOffsetRotation == null) return; + if (leftHandFreeOffsetPosition == null) return; + if (rightHandFreeOffsetPosition == null) return; + + // Beat Saber compatible + + leftHandFreeOffsetRotation.localRotation = Quaternion.Euler( + Settings.Current.LeftHandRotationX, + -Settings.Current.LeftHandRotationY, + Settings.Current.LeftHandRotationZ + ); + leftHandFreeOffsetPosition.localPosition = new Vector3( + -Settings.Current.LeftHandPositionX, + Settings.Current.LeftHandPositionY, + Settings.Current.LeftHandPositionZ + ); + + rightHandFreeOffsetRotation.localRotation = Quaternion.Euler( + Settings.Current.RightHandRotationX, + Settings.Current.RightHandRotationY, + Settings.Current.RightHandRotationZ + ); + rightHandFreeOffsetPosition.localPosition = new Vector3( + Settings.Current.RightHandPositionX, + Settings.Current.RightHandPositionY, + Settings.Current.RightHandPositionZ + ); + + vrik.solver.leftArm.swivelOffset = Settings.Current.SwivelOffset; + vrik.solver.rightArm.swivelOffset = -Settings.Current.SwivelOffset; + } + + #endregion + + public Guid AddOnPostUpdate(int priority, Action action) + { + if (OnPostUpdateEvents.ContainsKey(priority) == false) OnPostUpdateEvents.Add(priority, new List<(Guid eventId, Action action)>()); + var eventId = Guid.NewGuid(); + OnPostUpdateEvents[priority].Add((eventId, action)); + return eventId; + } + + public void RemoveOnPostUpdate(Guid eventId) + { + foreach(var list in OnPostUpdateEvents.Values) + { + foreach(var value in list) + { + if (value.eventId == eventId) + { + list.Remove(value); + return; + } + } + } + } + + private void OnPostUpdate() + { + foreach (var list in OnPostUpdateEvents.Values) + { + foreach (var value in list) + { + if (value.action != null) + { + value.action.Invoke(); + } + } + } + } + + private IEnumerator AfterUpdateCoroutine() + { + while (true) + { + yield return null; + // run after Update() + + if (vrik != null) continue; + //VRIKが無い時に他のモーションソースを動かすために手動で実行する + OnPostUpdate(); + } + } + + } + public enum CalibrationState + { + Uncalibrated = 0, + WaitingForCalibrating = 1, + Calibrating = 2, + Calibrated = 3, + } +} diff --git a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs.meta b/Assets/Scripts/Avatar/MotionTracking/IKManager.cs.meta similarity index 83% rename from Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs.meta rename to Assets/Scripts/Avatar/MotionTracking/IKManager.cs.meta index 545764aa..9f54e834 100644 --- a/Assets/ExternalPlugins/DVRSDK/Examples/DVRAuth/Scripts/SdkSettings.cs.meta +++ b/Assets/Scripts/Avatar/MotionTracking/IKManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9afc7f897dabe70439a5340779d41c65 +guid: 047ad6f77be3a2e4f9e9b4133f1fd63c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Avatar/MotionTracking/MotionManager.cs b/Assets/Scripts/Avatar/MotionTracking/MotionManager.cs new file mode 100644 index 00000000..dfb32da0 --- /dev/null +++ b/Assets/Scripts/Avatar/MotionTracking/MotionManager.cs @@ -0,0 +1,355 @@ +using RootMotion.FinalIK; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace VMC +{ + public class MotionManager : MonoBehaviour + { + [SerializeField] + private ControlWPFWindow controlWPFWindow; + + private GameObject currentModel; + + [SerializeField] + private List VirtualAvatars = new List(); + + private static MotionManager instance; + public static MotionManager Instance => instance; + + private Dictionary defaultPoses; + + private Guid eventId; + + private void Awake() + { + instance = this; + if (controlWPFWindow == null) controlWPFWindow = GameObject.Find("ControlWPFWindow").GetComponent(); + VMCEvents.OnCurrentModelChanged += OnCurrentModelChanged; + VMCEvents.OnModelUnloading += OnModelUnloading; + VirtualAvatar.EnableChanged += OnVirtualAvatarEnableChanged; + } + + private void Start() + { + eventId = IKManager.Instance.AddOnPostUpdate(100, ApplyMotion); + } + + private void OnDestroy() + { + IKManager.Instance.RemoveOnPostUpdate(eventId); + VirtualAvatar.EnableChanged -= OnVirtualAvatarEnableChanged; + } + + /// + /// 外部デバイスプラグイン(mocopi等)のモーションが今アバターへ適用されているか。 + /// 全身が動くかどうかで挙動を変えたい箇所(カメラの注視点など)から参照する。 + /// + public bool IsExternalDeviceMotionActive + => VirtualAvatars.Any(d => d.MotionSource == MotionSource.ExternalDevice && d.Enable); + + /// IsExternalDeviceMotionActive が変わったときに呼ばれる + public event Action ExternalDeviceMotionActiveChanged; + + private void OnVirtualAvatarEnableChanged(VirtualAvatar virtualAvatar) + { + if (virtualAvatar.MotionSource != MotionSource.ExternalDevice) return; + ExternalDeviceMotionActiveChanged?.Invoke(); + } + + public void AddVirtualAvatar(VirtualAvatar virtualAvatar) + { + if (VirtualAvatars.Contains(virtualAvatar) == false) + { + if (currentModel != null) + { + var currentPose = GetModelPoses(currentModel); + SetModelPoses(currentModel, defaultPoses); + virtualAvatar.ImportAvatar(currentModel); + SetModelPoses(currentModel, currentPose); + + } + VirtualAvatars.Add(virtualAvatar); + + VirtualAvatars = VirtualAvatars.OrderBy(d => (int)d.MotionSource).ToList(); + } + } + + public void RemoveVirtualAvatar(VirtualAvatar virtualAvatar) + { + if (VirtualAvatars.Contains(virtualAvatar) == true) + { + VirtualAvatars.Remove(virtualAvatar); + } + } + + + private void OnCurrentModelChanged(GameObject model) + { + if (model != null) + { + currentModel = model; + + defaultPoses = GetModelPoses(model); + + foreach (var virtualAvatar in VirtualAvatars) + { + virtualAvatar.ImportAvatar(model); + } + } + } + + public Dictionary GetModelPoses(GameObject model) + { + var animator = model.GetComponent(); + if (animator == null) return null; + + var poses = new Dictionary(); + var rootPose = new Pose(model.transform.localPosition, model.transform.localRotation); + poses.Add(VirtualAvatar.HumanBodyBonesRoot, rootPose); + + foreach(var bone in VirtualAvatar.ReverseBodyBones) + { + var boneTransform = animator.GetBoneTransform(bone); + if (boneTransform == null) continue; + var pose = new Pose(boneTransform.localPosition, boneTransform.localRotation); + poses.Add(bone, pose); + } + + return poses; + } + + public void SetModelPoses(GameObject model, Dictionary poses) + { + var animator = model.GetComponent(); + if (animator == null) return; + + foreach(var kv in poses) + { + var bone = kv.Key; + var pose = kv.Value; + + var boneTransform = bone == VirtualAvatar.HumanBodyBonesRoot ? model.transform : animator.GetBoneTransform(bone); + if (boneTransform == null) continue; + boneTransform.localPosition = pose.position; + boneTransform.localRotation = pose.rotation; + } + } + + public void ResetVirtualAvatarPose(VirtualAvatar virtualAvatar) => SetModelPoses(virtualAvatar.animator.gameObject, defaultPoses); + + private void OnModelUnloading(GameObject model) + { + //前回の生成物の削除 + if (currentModel != null) + { + currentModel = null; + } + } + + private void ApplyMotion() + { + if (currentModel == null) return; + + //無効になってる時は適用しない + if (enabled == false) return; + + VMCEvents.BeforeApplyMotion?.Invoke(currentModel); + + Transform ikHeadBone = null; + + foreach (var virtualAvatar in VirtualAvatars) + { + if (virtualAvatar.BoneTransformCache == null) continue; + if (virtualAvatar.MotionSource == MotionSource.VRIK) ikHeadBone = virtualAvatar.BoneTransformCache[HumanBodyBones.Head].cloneBone; + if (virtualAvatar.Enable == false) continue; + + //キャリブレーション中は適用しない + if (virtualAvatar.MotionSource != MotionSource.VRIK && + (IKManager.Instance.CalibrationState == CalibrationState.WaitingForCalibrating || + IKManager.Instance.CalibrationState == CalibrationState.Calibrating)) return; + + if (ikHeadBone == null) continue; + + Transform headBone = virtualAvatar.BoneTransformCache[HumanBodyBones.Head].modelBone; + Transform hipBone = null; + Transform spineBone = null; + Vector3 defaultHeadPosition = ikHeadBone.position; + Quaternion defaultHeadRotation = ikHeadBone.rotation; + + foreach (var (bone, (cloneBone, modelBone)) in virtualAvatar.BoneTransformCache) + { + bool apply = false; + switch (bone) + { + case VirtualAvatar.HumanBodyBonesRoot: + case HumanBodyBones.Hips: + if (virtualAvatar.IgnoreDefaultBone && IsDefaultPose(virtualAvatar, bone, cloneBone)) + { + apply = false; + } + else + { + if ((virtualAvatar.MotionSource == MotionSource.VRIK && bone == VirtualAvatar.HumanBodyBonesRoot) || + (virtualAvatar.MotionSource != MotionSource.VRIK && bone == HumanBodyBones.Hips)) + { + hipBone = modelBone; + if (virtualAvatar.ApplyRootRotation) + { + modelBone.localRotation = cloneBone.localRotation; + modelBone.Rotate(new Vector3(0, virtualAvatar.CenterOffsetRotationY, 0), Space.World); + } + if (virtualAvatar.ApplyRootPosition) + { + modelBone.localPosition = cloneBone.localPosition + virtualAvatar.CenterOffsetPosition; //Root位置だけは同期 + } + } + else if ((virtualAvatar.MotionSource == MotionSource.VRIK && bone == HumanBodyBones.Hips) || + (virtualAvatar.MotionSource == MotionSource.VMCProtocol && bone == VirtualAvatar.HumanBodyBonesRoot)) + { + if (virtualAvatar.ApplyRootRotation) + { + modelBone.localRotation = cloneBone.localRotation; + } + if (virtualAvatar.ApplyRootPosition) + { + modelBone.localPosition = cloneBone.localPosition; + } + } + } + break; + case HumanBodyBones.Spine: + spineBone = modelBone; + apply = virtualAvatar.ApplySpine; + break; + case HumanBodyBones.Chest: + case HumanBodyBones.UpperChest: + apply = virtualAvatar.ApplyChest; + break; + case HumanBodyBones.Neck: + case HumanBodyBones.Head: + case HumanBodyBones.Jaw: + apply = virtualAvatar.ApplyHead; + break; + case HumanBodyBones.LeftShoulder: + case HumanBodyBones.LeftUpperArm: + case HumanBodyBones.LeftLowerArm: + apply = virtualAvatar.ApplyLeftArm; + break; + case HumanBodyBones.RightShoulder: + case HumanBodyBones.RightUpperArm: + case HumanBodyBones.RightLowerArm: + apply = virtualAvatar.ApplyRightArm; + break; + case HumanBodyBones.LeftHand: + apply = virtualAvatar.ApplyLeftHand; + break; + case HumanBodyBones.RightHand: + apply = virtualAvatar.ApplyRightHand; + break; + case HumanBodyBones.LeftUpperLeg: + case HumanBodyBones.LeftLowerLeg: + apply = virtualAvatar.ApplyLeftLeg; + break; + case HumanBodyBones.RightUpperLeg: + case HumanBodyBones.RightLowerLeg: + apply = virtualAvatar.ApplyRightLeg; + break; + case HumanBodyBones.LeftFoot: + case HumanBodyBones.LeftToes: + apply = virtualAvatar.ApplyLeftFoot; + break; + case HumanBodyBones.RightFoot: + case HumanBodyBones.RightToes: + apply = virtualAvatar.ApplyRightFoot; + break; + case HumanBodyBones.LeftEye: + case HumanBodyBones.RightEye: + apply = virtualAvatar.ApplyEye; + break; + case HumanBodyBones.LeftThumbProximal: + case HumanBodyBones.LeftThumbIntermediate: + case HumanBodyBones.LeftThumbDistal: + case HumanBodyBones.LeftIndexProximal: + case HumanBodyBones.LeftIndexIntermediate: + case HumanBodyBones.LeftIndexDistal: + case HumanBodyBones.LeftMiddleProximal: + case HumanBodyBones.LeftMiddleIntermediate: + case HumanBodyBones.LeftMiddleDistal: + case HumanBodyBones.LeftRingProximal: + case HumanBodyBones.LeftRingIntermediate: + case HumanBodyBones.LeftRingDistal: + case HumanBodyBones.LeftLittleProximal: + case HumanBodyBones.LeftLittleIntermediate: + case HumanBodyBones.LeftLittleDistal: + apply = virtualAvatar.ApplyLeftFinger; + break; + case HumanBodyBones.RightThumbProximal: + case HumanBodyBones.RightThumbIntermediate: + case HumanBodyBones.RightThumbDistal: + case HumanBodyBones.RightIndexProximal: + case HumanBodyBones.RightIndexIntermediate: + case HumanBodyBones.RightIndexDistal: + case HumanBodyBones.RightMiddleProximal: + case HumanBodyBones.RightMiddleIntermediate: + case HumanBodyBones.RightMiddleDistal: + case HumanBodyBones.RightRingProximal: + case HumanBodyBones.RightRingIntermediate: + case HumanBodyBones.RightRingDistal: + case HumanBodyBones.RightLittleProximal: + case HumanBodyBones.RightLittleIntermediate: + case HumanBodyBones.RightLittleDistal: + apply = virtualAvatar.ApplyRightFinger; + break; + case HumanBodyBones.LastBone: + default: + break; + } + + if (apply) + { + if (virtualAvatar.IgnoreDefaultBone && IsDefaultPose(virtualAvatar, bone, cloneBone)) + { + continue; + } + modelBone.localPosition = cloneBone.localPosition; + modelBone.localRotation = cloneBone.localRotation; + } + } + + if (virtualAvatar.CorrectHipBone && virtualAvatar.ApplyHead == false && hipBone != null && spineBone != null) + { + //頭の回転無効の時、VR機器優先するために最後に元の位置に戻るように腰を動かす + var rotdiff = defaultHeadRotation * Quaternion.Inverse(headBone.rotation); + spineBone.rotation = rotdiff * spineBone.rotation; + var posdiff = defaultHeadPosition - headBone.position; + hipBone.position = posdiff + hipBone.position; + } + } + + VMCEvents.AfterApplyMotion?.Invoke(currentModel); + } + + private bool IsDefaultPose(VirtualAvatar virtualAvatar, HumanBodyBones bone, Transform cloneBone) + { + if (cloneBone == null) return true; + if (virtualAvatar.GetPoseChanged(bone) == true) + { + // 過去に変動していたら現在の値に関わらずデフォルトじゃない扱い + return false; + } + var pose = defaultPoses[bone]; + bool isDefault = ((cloneBone.localRotation == pose.rotation && cloneBone.localPosition == pose.position) || + (cloneBone.localRotation == Quaternion.identity && cloneBone.localPosition == Vector3.zero)); + if (isDefault == false) + { + // ボーンの変動を見つけたとき、デフォルトじゃない扱いする + virtualAvatar.SetPoseChanged(bone); + } + return isDefault; + } + } +} diff --git a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs.meta b/Assets/Scripts/Avatar/MotionTracking/MotionManager.cs.meta similarity index 83% rename from Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs.meta rename to Assets/Scripts/Avatar/MotionTracking/MotionManager.cs.meta index 29c6bbf0..8bfb6f5d 100644 --- a/Assets/Scripts/Avatar/EyeTracking/EyeTracking_Tobii.cs.meta +++ b/Assets/Scripts/Avatar/MotionTracking/MotionManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c6fd25003140c0e47b39261ea2e0f470 +guid: 3fe96d37d49f9a24680cbb921ebbb8aa MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs b/Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs new file mode 100644 index 00000000..4b1f05eb --- /dev/null +++ b/Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs @@ -0,0 +1,35 @@ +using RootMotion.FinalIK; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC +{ + [RequireComponent(typeof(VRIK))] + public class VRIKTimingManager : MonoBehaviour + { + private VRIK vrik; + + private void Awake() + { + StartCoroutine(AfterUpdateCoroutine()); + } + + private IEnumerator AfterUpdateCoroutine() + { + while (true) + { + yield return null; + // run after Update() + + if (vrik == null) vrik = GetComponent(); + if (vrik == null) continue; + if (vrik.enabled == false) continue; + + vrik.solver.FixTransforms(); + vrik.UpdateSolverExternal(); + + } + } + } +} diff --git a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs.meta b/Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs.meta similarity index 83% rename from Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs.meta rename to Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs.meta index 296cbaa9..88af16b7 100644 --- a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts/VRMLoader.cs.meta +++ b/Assets/Scripts/Avatar/MotionTracking/VRIKTimingManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 62ca30c0d40f188468e6358e24d123ef +guid: d910e233171d13647b8560eeb3c6d366 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Avatar/PelvisWeightAdjuster.cs b/Assets/Scripts/Avatar/PelvisWeightAdjuster.cs index 3ce60f14..81ce0853 100644 --- a/Assets/Scripts/Avatar/PelvisWeightAdjuster.cs +++ b/Assets/Scripts/Avatar/PelvisWeightAdjuster.cs @@ -34,7 +34,7 @@ private void Update() } else { //前に曲がった時 - PelvisWeight = Mathf.Lerp(1.0f, 0.7f, (signedAngle) / 120f); + PelvisWeight = Mathf.Lerp(1.0f, 0.70f, (signedAngle) / 120f); } vrik.solver.spine.pelvisPositionWeight = PelvisWeight; diff --git a/Assets/Scripts/Avatar/TransformAdjustFollower.cs b/Assets/Scripts/Avatar/TransformAdjustFollower.cs index 9d29abd8..7989d99c 100644 --- a/Assets/Scripts/Avatar/TransformAdjustFollower.cs +++ b/Assets/Scripts/Avatar/TransformAdjustFollower.cs @@ -7,8 +7,8 @@ namespace VMC { public class TransformAdjustFollower : MonoBehaviour { - private const float AdjustLimit = 0.032f; - private const float LerpDistance = 0.023f; + private const float AdjustLimit = 0.018f; + private const float LerpDistance = 0.009f; private Transform adjustTargetPoint; private float defaultHeight; diff --git a/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs b/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs deleted file mode 100644 index 12f486ce..00000000 --- a/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs +++ /dev/null @@ -1,82 +0,0 @@ -#pragma warning disable 0414, 0649 -using UnityEngine; -using VRM; - -namespace VMC -{ - public class VMC_VRMLookAtBlendShapeApplyer : MonoBehaviour, IVRMComponent - { - public bool DrawGizmo = true; - - [SerializeField, Header("Degree Mapping")] - public CurveMapper Horizontal = new CurveMapper(90.0f, 1.0f); - - [SerializeField] - public CurveMapper VerticalDown = new CurveMapper(90.0f, 1.0f); - - [SerializeField] - public CurveMapper VerticalUp = new CurveMapper(90.0f, 1.0f); - - [SerializeField] - public bool m_notSetValueApply; - - public FaceController faceController; - - public void OnImported(VRMImporterContext context) - { - var gltfFirstPerson = context.GLTF.extensions.VRM.firstPerson; - Horizontal.Apply(gltfFirstPerson.lookAtHorizontalOuter); - VerticalDown.Apply(gltfFirstPerson.lookAtVerticalDown); - VerticalUp.Apply(gltfFirstPerson.lookAtVerticalUp); - } - - VRMLookAtHead m_head; - - private void Start() - { - m_head = GetComponent(); - if (faceController == null) faceController = GameObject.Find("AnimationController").GetComponent(); - if (m_head == null) - { - enabled = false; - return; - } - m_head.YawPitchChanged += ApplyRotations; - } - - private BlendShapeKey[] presets = new[] { BlendShapeKey.CreateFromPreset(BlendShapePreset.LookLeft), BlendShapeKey.CreateFromPreset(BlendShapePreset.LookRight), BlendShapeKey.CreateFromPreset(BlendShapePreset.LookUp), BlendShapeKey.CreateFromPreset(BlendShapePreset.LookDown) }; - private float[] blendShapeValues = new float[4]; - - void ApplyRotations(float yaw, float pitch) - { -#pragma warning disable 0618 - if (yaw < 0) - { - // Left - blendShapeValues[1] = 0; - blendShapeValues[0] = Mathf.Clamp(Horizontal.Map(-yaw), 0, 1.0f); - } - else - { - // Right - blendShapeValues[0] = 0; - blendShapeValues[1] = Mathf.Clamp(Horizontal.Map(yaw), 0, 1.0f); - } - - if (pitch < 0) - { - // Down - blendShapeValues[2] = 0; - blendShapeValues[3] = Mathf.Clamp(VerticalDown.Map(-pitch), 0, 1.0f); - } - else - { - // Up - blendShapeValues[3] = 0; - blendShapeValues[2] = Mathf.Clamp(VerticalUp.Map(pitch), 0, 1.0f); - } - faceController.OverwritePresets(nameof(VMC_VRMLookAtBlendShapeApplyer), presets, blendShapeValues); -#pragma warning restore 0618 - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs.meta b/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs.meta deleted file mode 100644 index 99d81abe..00000000 --- a/Assets/Scripts/Avatar/VMC_VRMLookAtBlendShapeApplyer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 64887fc21d0196741951e25406abcaa2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Avatar/VRM10CompatibleNames.cs b/Assets/Scripts/Avatar/VRM10CompatibleNames.cs new file mode 100644 index 00000000..d2264ce6 --- /dev/null +++ b/Assets/Scripts/Avatar/VRM10CompatibleNames.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using UniVRM10; + +namespace VMC +{ + /// + /// VRM0.xとVRM1.0のプリセット表情名の相互変換 + /// VMCProtocolの仕様上、VRM1.0使用時もVRM0形式での送信を必ず実装する必要がある + /// 対応表: https://protocol.vmc.info/marionette-spec (VRM0系とVRM1系の非互換性に関する警告) + /// + public static class VRM10CompatibleNames + { + private static readonly Dictionary PresetToVRM0 = new Dictionary + { + { ExpressionPreset.happy, "Joy" }, + { ExpressionPreset.angry, "Angry" }, + { ExpressionPreset.sad, "Sorrow" }, + { ExpressionPreset.relaxed, "Fun" }, + { ExpressionPreset.aa, "A" }, + { ExpressionPreset.ih, "I" }, + { ExpressionPreset.ou, "U" }, + { ExpressionPreset.ee, "E" }, + { ExpressionPreset.oh, "O" }, + { ExpressionPreset.blink, "Blink" }, + { ExpressionPreset.blinkLeft, "Blink_L" }, + { ExpressionPreset.blinkRight, "Blink_R" }, + { ExpressionPreset.lookUp, "LookUp" }, + { ExpressionPreset.lookDown, "LookDown" }, + { ExpressionPreset.lookLeft, "LookLeft" }, + { ExpressionPreset.lookRight, "LookRight" }, + { ExpressionPreset.neutral, "Neutral" }, + // surprisedはVRM0.xにプリセットが無いためVRM1.0名のまま扱う + }; + + /// + /// VRM1.0プリセット→VRM0.x名称の対応表(受信側の互換キー登録用) + /// + public static IReadOnlyDictionary PresetToVrm0Names => PresetToVRM0; + + /// + /// プリセット表情はVRM0.xの名称(Joy, A, Blink_L等)、カスタム表情は元の名称を返す + /// + public static string GetVRM0CompatibleName(ExpressionKey key) + { + if (PresetToVRM0.TryGetValue(key.Preset, out var name)) + { + return name; + } + return key.Name; + } + } +} diff --git a/Assets/Scripts/Avatar/VRM10CompatibleNames.cs.meta b/Assets/Scripts/Avatar/VRM10CompatibleNames.cs.meta new file mode 100644 index 00000000..72f1d7bd --- /dev/null +++ b/Assets/Scripts/Avatar/VRM10CompatibleNames.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b619f48de095f040a3b53a53255a90d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Avatar/VRMMetaImporter.cs b/Assets/Scripts/Avatar/VRMMetaImporter.cs deleted file mode 100644 index a131618c..00000000 --- a/Assets/Scripts/Avatar/VRMMetaImporter.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UniGLTF; -using UnityEngine; -using VRM; - -namespace VMC -{ - public class VRMMetaImporter - { - public static async Task ImportVRMMeta(string path, bool createThumbnail = false) - { - byte[] buffer; - using (FileStream SourceStream = File.Open(path, FileMode.Open)) - { - var length = SourceStream.Length; - - if (length == 0) - { - throw new Exception("empty bytes"); - } - - buffer = new byte[4]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - if (Encoding.ASCII.GetString(buffer, 0, 4) != glbImporter.GLB_MAGIC) - { - throw new Exception("invalid magic"); - } - - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - var version = BitConverter.ToUInt32(buffer, 0); - if (version != glbImporter.GLB_VERSION) - { - Debug.LogWarningFormat("unknown version: {0}", version); - return null; - } - - SourceStream.Seek(4, SeekOrigin.Current); - - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - var chunkDataSize = BitConverter.ToInt32(buffer, 0); - - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - var chunkTypeBytes = buffer.Where(x => x != 0).ToArray(); - var chunkTypeStr = Encoding.ASCII.GetString(chunkTypeBytes); - var type = glbImporter.ToChunkType(chunkTypeStr); - - if (type != GlbChunkType.JSON) - { - throw new Exception("chunk 0 is not JSON"); - } - - buffer = new byte[chunkDataSize]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - - var context = new VRMImporterContext(); - context.Json = Encoding.UTF8.GetString(buffer); - context.GLTF = JsonUtility.FromJson(context.Json); - - if (context.GLTF.asset.version != "2.0") - { - throw new UniGLTFException("unknown gltf version {0}", context.GLTF.asset.version); - } - - /* Cannot call because private */ - //context.RestoreOlderVersionValues(); - - //TODO: Is it necessary for the current VRM version? - RestoreOlderVersionValues(context.Json, context.GLTF); - - if (createThumbnail) - { - buffer = new byte[4]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - chunkDataSize = BitConverter.ToInt32(buffer, 0); - - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - chunkTypeBytes = buffer.Where(x => x != 0).ToArray(); - chunkTypeStr = Encoding.ASCII.GetString(chunkTypeBytes); - type = glbImporter.ToChunkType(chunkTypeStr); - - if (type != GlbChunkType.BIN) - { - throw new Exception("chunk 1 is not BIN"); - } - - buffer = new byte[chunkDataSize]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length); - - var storage = new SimpleStorage(new ArraySegment(buffer)); - foreach (var gltfbuffer in context.GLTF.buffers) - { - gltfbuffer.OpenStorage(storage); - } - } - - return context.ReadMeta(createThumbnail); - } - } - - //from ImporterContext.cs(UniGLTF) - static void RestoreOlderVersionValues(string Json, glTF GLTF) - { - var parsed = UniJSON.JsonParser.Parse(Json); - for (int i = 0; i < GLTF.images.Count; ++i) - { - if (string.IsNullOrEmpty(GLTF.images[i].name)) - { - try - { - var extraName = parsed["images"][i]["extra"]["name"].Value.GetString(); - if (!string.IsNullOrEmpty(extraName)) - { - //Debug.LogFormat("restore texturename: {0}", extraName); - GLTF.images[i].name = extraName; - } - } - catch (Exception) - { - // do nothing - } - } - } - for (int i = 0; i < GLTF.meshes.Count; ++i) - { - var mesh = GLTF.meshes[i]; - try - { - for (int j = 0; j < mesh.primitives.Count; ++j) - { - var primitive = mesh.primitives[j]; - for (int k = 0; k < primitive.targets.Count; ++k) - { - var extraName = parsed["meshes"][i]["primitives"][j]["targets"][k]["extra"]["name"].Value.GetString(); - //Debug.LogFormat("restore morphName: {0}", extraName); - primitive.extras.targetNames.Add(extraName); - } - } - } - catch (Exception) - { - // do nothing - } - } -#if false - for (int i = 0; i < GLTF.nodes.Count; ++i) - { - var node = GLTF.nodes[i]; - try - { - var extra = parsed["nodes"][i]["extra"]["skinRootBone"].AsInt; - //Debug.LogFormat("restore extra: {0}", extra); - //node.extras.skinRootBone = extra; - } - catch (Exception) - { - // do nothing - } - } -#endif - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/Avatar/VRMMetaImporter.cs.meta b/Assets/Scripts/Avatar/VRMMetaImporter.cs.meta deleted file mode 100644 index 698b5bc8..00000000 --- a/Assets/Scripts/Avatar/VRMMetaImporter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 10420eb4ed0206d4fac01882ed615a88 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Avatar/WristRotationFix.cs b/Assets/Scripts/Avatar/WristRotationFix.cs index 04d68e3e..b22fed19 100644 --- a/Assets/Scripts/Avatar/WristRotationFix.cs +++ b/Assets/Scripts/Avatar/WristRotationFix.cs @@ -1,6 +1,7 @@ using RootMotion.FinalIK; using System; using UnityEngine; +using UnityMemoryMappedFile; namespace VMC { @@ -9,103 +10,390 @@ public class WristRotationFix : MonoBehaviour public VRIK ik; - private FixItem LeftElbowFixItem; - private FixItem LeftUpperArmFixItem; - private FixItem RightElbowFixItem; - private FixItem RightUpperArmFixItem; + // ControlWPFWindowの参照を保持 + [SerializeField] + private ControlWPFWindow controlWPFWindow; - public float ElbowFixWeight = 0.5f; - public float UpperArmFixWeight = 0.2f; //0.5では強すぎて肩がねじれる場合がある + private ArmFixItem LeftArmFixItem; + private ArmFixItem RightArmFixItem; + // VRIKで元から回ってる分はキャンセルされるのでこの割合の通りに回転される + public float UpperArmWeight = 0.2f; // 20% + public float ForearmWeight = 0.57f; // 57% + + private Guid? eventId = null; + + [Header("Twist Limits")] + [Tooltip("累積回転がこの角度を超えたら連続性をリセット")] + [Range(180f, 720f)] + public float maxAccumulatedTwist = 300f; + + private System.Threading.SynchronizationContext context = null; + + void Start() + { + context = System.Threading.SynchronizationContext.Current; + + // ControlWPFWindowの参照取得(SerializeFieldで設定されていない場合のみ) + if (controlWPFWindow == null) + { + controlWPFWindow = GameObject.Find("ControlWPFWindow")?.GetComponent(); + } + + // 設定からパラメータを読み込み + LoadSettingsValues(); + + // UIとの通信を設定 + if (controlWPFWindow != null) + { + controlWPFWindow.server.ReceivedEvent += Server_Received; + // AdditionalSettingActionに登録して設定読み込み時に自動実行されるようにする + controlWPFWindow.AdditionalSettingAction += ApplySettings; + } + } + + void OnDestroy() + { + if (eventId != null) IKManager.Instance.RemoveOnPostUpdate(eventId.Value); + + // 通信ハンドラの登録解除 + if (controlWPFWindow != null) + { + controlWPFWindow.server.ReceivedEvent -= Server_Received; + controlWPFWindow.AdditionalSettingAction -= ApplySettings; + } + } + + // 設定読み込み時に呼ばれるメソッド(IKManager.SetHandFreeOffsetと同様) + private void ApplySettings(GameObject gameObject) + { + LoadSettingsValues(); + } + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(async s => + { + if (e.CommandType == typeof(PipeCommands.GetWristRotationFixSetting)) + { + if (controlWPFWindow != null) + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.SetWristRotationFixSetting + { + UpperArmWeight = Settings.Current.WristRotationFix_UpperArmWeight, + ForearmWeight = Settings.Current.WristRotationFix_ForearmWeight, + MaxAccumulatedTwist = Settings.Current.WristRotationFix_MaxAccumulatedTwist, + }, e.RequestId); + } + } + else if (e.CommandType == typeof(PipeCommands.SetWristRotationFixSetting)) + { + var d = (PipeCommands.SetWristRotationFixSetting)e.Data; + + // 設定を保存 + SaveSettingsValues(d); + } + }, null); + } + + private void LoadSettingsValues() + { + if (Settings.Current != null) + { + UpperArmWeight = Settings.Current.WristRotationFix_UpperArmWeight / 1000f; + ForearmWeight = Settings.Current.WristRotationFix_ForearmWeight / 1000f; + maxAccumulatedTwist = Settings.Current.WristRotationFix_MaxAccumulatedTwist; + } + } + + private void SaveSettingsValues(PipeCommands.SetWristRotationFixSetting d) + { + UpperArmWeight = d.UpperArmWeight / 1000f; + ForearmWeight = d.ForearmWeight / 1000f; + maxAccumulatedTwist = d.MaxAccumulatedTwist; + if (Settings.Current != null) + { + Settings.Current.WristRotationFix_UpperArmWeight = d.UpperArmWeight; + Settings.Current.WristRotationFix_ForearmWeight = d.ForearmWeight; + Settings.Current.WristRotationFix_MaxAccumulatedTwist = d.MaxAccumulatedTwist; + } + } public void SetVRIK(VRIK setIK) { - if (ik != null) ik.GetIKSolver().OnPostUpdate -= OnPostUpdate; + if (eventId != null) IKManager.Instance.RemoveOnPostUpdate(eventId.Value); ik = setIK; - LeftElbowFixItem = new FixItem(ik.references.leftUpperArm, ik.references.leftForearm, ik.references.leftHand, () => ElbowFixWeight); - LeftUpperArmFixItem = new FixItem(ik.references.leftShoulder, ik.references.leftUpperArm, ik.references.leftForearm, () => UpperArmFixWeight); - RightElbowFixItem = new FixItem(ik.references.rightUpperArm, ik.references.rightForearm, ik.references.rightHand, () => ElbowFixWeight); - RightUpperArmFixItem = new FixItem(ik.references.rightShoulder, ik.references.rightUpperArm, ik.references.rightForearm, () => UpperArmFixWeight); + // 基準となるTransformを取得(胸が優先、なければルート) + Transform referenceTransform = ik.references.chest ?? ik.references.root; + + LeftArmFixItem = new ArmFixItem(ik.references.leftShoulder, ik.references.leftUpperArm, ik.references.leftForearm, ik.references.leftHand, referenceTransform); + RightArmFixItem = new ArmFixItem(ik.references.rightShoulder, ik.references.rightUpperArm, ik.references.rightForearm, ik.references.rightHand, referenceTransform); + + // 設定を再読み込み + LoadSettingsValues(); - if (ik != null) ik.GetIKSolver().OnPostUpdate += OnPostUpdate; + eventId = IKManager.Instance.AddOnPostUpdate(10, OnPostUpdate); } - void OnDestroy() + private void OnPostUpdate() { - if (ik != null) ik.GetIKSolver().OnPostUpdate -= OnPostUpdate; + if (IKManager.Instance.vrik == null) return; + if (enabled == false) return; + + ApplyArmTwistFix(LeftArmFixItem); + ApplyArmTwistFix(RightArmFixItem); } + + private void ApplyArmTwistFix(ArmFixItem item) + { + Quaternion originalHandRotation = item.Hand.rotation; + Quaternion currentUpperArmRotation = item.UpperArm.rotation; + Quaternion currentForearmRotation = item.Forearm.rotation; - private void OnPostUpdate() + // 1. UpperArmとForearmの現在の回転からTwist成分のみを除去し、Swing成分のみを残す + Quaternion upperArmSwingOnly = RemoveTwistFromRotation(currentUpperArmRotation, item.InitialUpperArmRotation, item.UpperArmTwistAxis, item); + Quaternion forearmSwingOnly = RemoveTwistFromRotation(currentForearmRotation, item.InitialForearmRotation, item.ForearmTwistAxis, item); + + // 2. Swing成分のみの回転を適用(IKの結果を保持) + item.UpperArm.rotation = upperArmSwingOnly; + item.Forearm.rotation = forearmSwingOnly; + + // 3. HandのForearmに対するローカル回転からTwist角度を計算 + float twistAngle = CalculateHandTwistAngle(item, forearmSwingOnly, originalHandRotation); + + // 4. 連続性を保つための補正 + float lastAngle = item.LastTwistAngle; + float delta = Mathf.DeltaAngle(lastAngle, twistAngle); + + // 通常の連続性補正 + twistAngle = lastAngle + delta; + + // 5. 初期角度からの累積角度を更新 + item.AccumulatedTwistFromInitial += delta; + + // 初期角度から±maxAccumulatedTwist度を超えたらリセット + if (Mathf.Abs(item.AccumulatedTwistFromInitial) > maxAccumulatedTwist) + { + // リセット時の処理:360度逆回転させる + float resetDirection = Mathf.Sign(item.AccumulatedTwistFromInitial); + float resetAdjustment = -resetDirection * 360f; + + // Twist角度と累積角度を調整 + twistAngle += resetAdjustment; + item.AccumulatedTwistFromInitial += resetAdjustment; + + // twistAngleがmaxAccumulatedTwist範囲を超えた場合に正規化 + if (twistAngle > maxAccumulatedTwist) + { + // 正の範囲を超えた場合:360度引く + twistAngle -= 360f; + item.AccumulatedTwistFromInitial -= 360f; + } + else if (twistAngle < -maxAccumulatedTwist) + { + // 負の範囲を超えた場合:360度足す + twistAngle += 360f; + item.AccumulatedTwistFromInitial += 360f; + } + + // デバッグ用 + Debug.Log($"Twist angle reset ({(item.IsLeftArm ? "Left" : "Right")} Arm): direction = {resetDirection}, adjustment = {resetAdjustment} degrees, normalized twistAngle = {twistAngle}, new accumulated = {item.AccumulatedTwistFromInitial}"); + } + + item.LastTwistAngle = twistAngle; + + // 6. Twist角度をUpperArmとForearmに分配 + float upperArmTwist = twistAngle * UpperArmWeight; + float forearmTwist = twistAngle * ForearmWeight; + + // 7. Twist軸を使用してTwist回転を適用 + Vector3 upperArmTwistAxis = item.UpperArm.TransformDirection(item.UpperArmTwistAxis); + Vector3 forearmTwistAxis = item.Forearm.TransformDirection(item.ForearmTwistAxis); + + // 8. Swing成分にTwist回転を加算 + item.UpperArm.rotation = Quaternion.AngleAxis(upperArmTwist, upperArmTwistAxis) * upperArmSwingOnly; + item.Forearm.rotation = Quaternion.AngleAxis(forearmTwist, forearmTwistAxis) * forearmSwingOnly; + item.Hand.rotation = originalHandRotation; + } + + // 回転からTwist成分を除去し、Swing成分のみを残す + private Quaternion RemoveTwistFromRotation(Quaternion currentRotation, Quaternion initialRotation, Vector3 twistAxis, ArmFixItem item) { - FixAxis(LeftElbowFixItem); - FixAxis(LeftUpperArmFixItem); - FixAxis(RightElbowFixItem); - FixAxis(RightUpperArmFixItem); + Quaternion relativeRotation; + + // 基準Transform(胸またはルート)を取得 + Transform referenceTransform = ik.references.chest ?? ik.references.root; + + if (referenceTransform != null) + { + // 基準Transformを使った相対回転計算 + Quaternion currentReferenceRotation = referenceTransform.rotation; + + // 現在の腕の回転を基準Transformからの相対回転として計算 + Quaternion currentArmRelativeToReference = Quaternion.Inverse(currentReferenceRotation) * currentRotation; + + // 初期状態との相対回転を計算(基準Transformの回転変化の影響を除去) + Quaternion initialArmRelativeToReference = (initialRotation == item.InitialUpperArmRotation) + ? item.InitialUpperArmRelativeToReference + : item.InitialForearmRelativeToReference; + + relativeRotation = currentArmRelativeToReference * Quaternion.Inverse(initialArmRelativeToReference); + + // Swing-Twist分解でSwing成分のみを抽出 + Quaternion swing, twist; + SwingTwistDecomposition(relativeRotation, twistAxis, out swing, out twist); + + // 結果を基準Transformを基準としたワールド座標に戻す + return currentReferenceRotation * (swing * initialArmRelativeToReference); + } + else + { + // 基準Transformがない場合は従来の方法 + relativeRotation = currentRotation * Quaternion.Inverse(initialRotation); + + Quaternion swing, twist; + SwingTwistDecomposition(relativeRotation, twistAxis, out swing, out twist); + + return swing * initialRotation; + } } - private void FixAxis(FixItem fix) + // HandのTwist角度を計算(Swing-Twist分解を使用) + private float CalculateHandTwistAngle(ArmFixItem item, Quaternion forearmRotation, Quaternion handRotation) { - //Quaternion.AngleAxis:軸を決めて回転させる - //Quaternion * Vector3: 指定方向に回転させたVector3が返ってくる - Quaternion targetRotation = fix.Target.rotation; - Quaternion twistOffset = Quaternion.AngleAxis(0, targetRotation * fix.TwistAxis); - targetRotation = twistOffset * targetRotation; + // HandのForearmに対するローカル回転 + Quaternion handLocal = Quaternion.Inverse(forearmRotation) * handRotation; - // 親(肩)と子(手首)のワールド座標の緩和軸を求める - Vector3 relaxedAxisParent = twistOffset * fix.Parent.rotation * fix.AxisRelativeToParentDefault; - Vector3 relaxedAxisChild = twistOffset * fix.Child.rotation * fix.AxisRelativeToChildDefault; + // 初期状態との差分 + Quaternion deltaRotation = handLocal * Quaternion.Inverse(item.NeutralHandRotation); - // 親(肩)と子(手首)の中間の回転角度を計算する - Vector3 relaxedAxis = Vector3.Slerp(relaxedAxisParent, relaxedAxisChild, fix.GetFixWeight()); + // Swing-Twist分解でTwist成分のみを抽出 + Vector3 twistAxis = Vector3.right; // ForearmのローカルX軸(Twist軸) + + // 左腕の場合はTwist軸を反転 + if (item.IsLeftArm) + { + twistAxis = -twistAxis; // Vector3.leftと同等 + } + + Quaternion swing, twist; + SwingTwistDecomposition(deltaRotation, twistAxis, out swing, out twist); - // relaxedAxisを(axis、twistAxis)空間で変換して、ねじれ角を計算できます - Quaternion r = Quaternion.LookRotation(targetRotation * fix.Axis, targetRotation * fix.TwistAxis); - relaxedAxis = Quaternion.Inverse(r) * relaxedAxis; + // Twist角度を取得 + float angle; + Vector3 axis; + twist.ToAngleAxis(out angle, out axis); - // ねじれ軸を中心にこのTransformを回転させるために必要な角度を計算します - float angle = Mathf.Atan2(relaxedAxis.x, relaxedAxis.z) * Mathf.Rad2Deg; - //Debug.Log($"Angle{angle}"); + // 符号を正しく設定 + if (Vector3.Dot(axis, twistAxis) < 0) + angle = -angle; + + // -180~180度の範囲に正規化 + return Mathf.DeltaAngle(0, angle); + } + + // Swing-Twist分解(改善版) + private void SwingTwistDecomposition(Quaternion rotation, Vector3 twistAxis, out Quaternion swing, out Quaternion twist) + { + twistAxis.Normalize(); - // 子(手首)の回転を取っておいて、対象(ひじ)を回転させた後戻せるようにしておく - Quaternion childRotation = fix.Child.rotation; + // 回転軸を取得 + Vector3 r = new Vector3(rotation.x, rotation.y, rotation.z); - // 対象(ひじ)を回転させる - fix.Target.rotation = Quaternion.AngleAxis(angle, targetRotation * fix.TwistAxis) * targetRotation; + // Twist軸への投影 + float dot = Vector3.Dot(r, twistAxis); + Vector3 twistPart = dot * twistAxis; - // 対象(ひじ)で動いてしまった子(手首)の回転を元に戻す - fix.Child.rotation = childRotation; + // Twist成分のクォータニオン + twist = new Quaternion(twistPart.x, twistPart.y, twistPart.z, rotation.w); + + // 長さが0に近い場合は単位クォータニオンに + if (twist.x * twist.x + twist.y * twist.y + twist.z * twist.z + twist.w * twist.w < 0.01f) + { + twist = Quaternion.identity; + } + else + { + twist = Quaternion.Normalize(twist); + } + + // Swing成分 + swing = rotation * Quaternion.Inverse(twist); } - private class FixItem + public class ArmFixItem { - public Vector3 TwistAxis = Vector3.right; - public Vector3 Axis = Vector3.forward; - public Vector3 AxisRelativeToParentDefault; - public Vector3 AxisRelativeToChildDefault; + public Transform Shoulder; + public Transform UpperArm; + public Transform Forearm; + public Transform Hand; + + // 左腕かどうかの判定(初期化時に決定) + public bool IsLeftArm; + + // Twist軸(ローカル空間) + public Vector3 UpperArmTwistAxis; + public Vector3 ForearmTwistAxis; - public Transform Parent; - public Transform Target; - public Transform Child; + // 前回のTwist角度 + public float LastTwistAngle = 0f; + + // 初期角度からの累積Twist角度 + public float AccumulatedTwistFromInitial = 0f; - public Func GetFixWeight; + // 初期状態のHandの回転(T-Pose時など) + public Quaternion NeutralHandRotation; + + // T-ポーズ時の初期回転を保存 + public Quaternion InitialUpperArmRotation; + public Quaternion InitialForearmRotation; + + // 基準Transform(胸またはルート)の初期回転を保存 + public Quaternion InitialReferenceRotation; + + // 腕の初期回転を基準Transformからの相対回転として保存 + public Quaternion InitialUpperArmRelativeToReference; + public Quaternion InitialForearmRelativeToReference; - public FixItem(Transform parent, Transform target, Transform child, Func getFixWeight) + public ArmFixItem(Transform shoulder, Transform upperArm, Transform forearm, Transform hand, Transform referenceTransform) { - Parent = parent; - Target = target; - Child = child; - GetFixWeight = getFixWeight; + Shoulder = shoulder; + UpperArm = upperArm; + Forearm = forearm; + Hand = hand; - //InverseTransformDirection:特定のワールド座標が自身のローカル座標だといくつになるか - TwistAxis = target.InverseTransformDirection(child.position - target.position); - Axis = new Vector3(TwistAxis.y, TwistAxis.z, TwistAxis.x); + // 初期化時に肩から手への方向で左右を判定 + Vector3 shoulderToHand = hand.position - shoulder.position; + IsLeftArm = shoulderToHand.x < 0; - // ワールド座標での軸 - Vector3 elbowAxisWorld = target.rotation * Axis; + // UpperArmのTwist軸(Forearm→UpperArm方向をUpperArmローカル空間で) + UpperArmTwistAxis = upperArm.InverseTransformDirection(forearm.position - upperArm.position).normalized; + // ForearmのTwist軸(Hand→Forearm方向をForearmローカル空間で) + ForearmTwistAxis = forearm.InverseTransformDirection(hand.position - forearm.position).normalized; - // 肩と手首のワールド座標での軸 - AxisRelativeToParentDefault = Quaternion.Inverse(parent.rotation) * elbowAxisWorld; - AxisRelativeToChildDefault = Quaternion.Inverse(child.rotation) * elbowAxisWorld; + // 初期状態のHandの回転を保存 + NeutralHandRotation = Quaternion.Inverse(forearm.rotation) * hand.rotation; + + // T-ポーズ時の絶対回転を保存 + InitialUpperArmRotation = upperArm.rotation; + InitialForearmRotation = forearm.rotation; + + // 基準Transform(胸またはルート)の初期回転を保存 + if (referenceTransform != null) + { + InitialReferenceRotation = referenceTransform.rotation; + + // 腕の回転を基準Transformからの相対回転として保存 + InitialUpperArmRelativeToReference = Quaternion.Inverse(referenceTransform.rotation) * upperArm.rotation; + InitialForearmRelativeToReference = Quaternion.Inverse(referenceTransform.rotation) * forearm.rotation; + } + else + { + // 基準Transformがない場合は従来の方法を使用 + InitialReferenceRotation = Quaternion.identity; + InitialUpperArmRelativeToReference = upperArm.rotation; + InitialForearmRelativeToReference = forearm.rotation; + } } } } diff --git a/Assets/Scripts/Camera/CameraManager.cs b/Assets/Scripts/Camera/CameraManager.cs index 227567a6..a24ffd75 100644 --- a/Assets/Scripts/Camera/CameraManager.cs +++ b/Assets/Scripts/Camera/CameraManager.cs @@ -30,6 +30,7 @@ public class CameraManager : MonoBehaviour private GameObject CurrentModel; private Animator animator; + public GameObject CurrentLookTarget = null; private void Awake() { @@ -44,6 +45,13 @@ private void Start() context = System.Threading.SynchronizationContext.Current; controlWPFWindow.server.ReceivedEvent += Server_Received; + //外部デバイス(mocopi等)で全身が動く時は注視点を腰に付ける必要があるので、 + //切り替わったら付け直す + if (MotionManager.Instance != null) + { + MotionManager.Instance.ExternalDeviceMotionActiveChanged += () => SetCameraLookTarget(); + } + VMCEvents.OnCameraChanged?.Invoke(ControlCamera); } private void Server_Received(object sender, DataReceivedEventArgs e) @@ -149,11 +157,16 @@ await controlWPFWindow.server.SendCommandAsync(new PipeCommands.SetVirtualWebCam var d = (PipeCommands.TakePhoto)e.Data; TakePhoto(d.Width, d.TransparentBackground, d.Directory); } + else if (e.CommandType == typeof(PipeCommands.ResetCamera)) + { + CurrentCameraControl?.FrontReset(); + } }, null); } private void ModelLoaded(GameObject currentModel) { + if (currentModel == null) return; CurrentModel = currentModel; animator = currentModel.GetComponent(); @@ -230,6 +243,13 @@ public void ChangeCamera(CameraTypes type) SetCameraEnable(PositionFixedCamera); } Settings.Current.CameraType = type; + + // UI反映のために送り返す + context.Post(async s => + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.ChangeCamera { type = type }); + }, null); + } private void SetCameraEnable(CameraMouseControl camera) @@ -265,28 +285,33 @@ private void SetCameraMirrorEnable(bool mirrorEnable) private void SetCameraLookTarget() { + //全身が外部デバイスで動く時は、注視点をモデルのルートではなく腰に追従させる + var followHips = MotionManager.Instance != null && MotionManager.Instance.IsExternalDeviceMotionActive; + if (animator != null) { var spineTransform = animator.GetBoneTransform(HumanBodyBones.Spine); var calcPosition = Vector3.Lerp(animator.GetBoneTransform(HumanBodyBones.Head).position, spineTransform.position, 0.5f); - var gameObject = new GameObject("CameraLook"); - gameObject.transform.position = calcPosition; - gameObject.transform.rotation = spineTransform.rotation; - gameObject.transform.parent =/* bodyTracker == null ? animator.GetBoneTransform(HumanBodyBones.Spine) :*/ CurrentModel.transform; + if (CurrentLookTarget != null) DestroyImmediate(CurrentLookTarget); + CurrentLookTarget = new GameObject("CameraLook"); + CurrentLookTarget.transform.position = calcPosition; + CurrentLookTarget.transform.rotation = spineTransform.rotation; + CurrentLookTarget.transform.parent = followHips ? animator.GetBoneTransform(HumanBodyBones.Hips) : CurrentModel.transform; + var lookTarget = FrontCamera.GetComponent(); if (lookTarget != null) { - lookTarget.LookTarget = gameObject.transform; + lookTarget.LookTarget = CurrentLookTarget.transform; } lookTarget = BackCamera.GetComponent(); if (lookTarget != null) { - lookTarget.LookTarget = gameObject.transform; + lookTarget.LookTarget = CurrentLookTarget.transform; } var positionFixedCamera = PositionFixedCamera.GetComponent(); if (positionFixedCamera != null) { - positionFixedCamera.PositionFixedTarget = gameObject.transform; + positionFixedCamera.PositionFixedTarget = CurrentLookTarget.transform; } } } @@ -411,5 +436,11 @@ private void ApplySettings(GameObject gameObject) SetCameraMirrorEnable(Settings.Current.CameraMirrorEnable); } + + #region 自動テスト用フック + + internal void Test_SetCameraFOV(float fov) => SetCameraFOV(fov); + + #endregion } } \ No newline at end of file diff --git a/Assets/Scripts/Camera/CameraMouseControl.cs b/Assets/Scripts/Camera/CameraMouseControl.cs index fdb7a562..e9e136e3 100644 --- a/Assets/Scripts/Camera/CameraMouseControl.cs +++ b/Assets/Scripts/Camera/CameraMouseControl.cs @@ -1,9 +1,12 @@ -using UnityEngine; +using System.Collections; +using UnityEngine; using UnityMemoryMappedFile; namespace VMC { + //ExecutionOrder after VRIK + [DefaultExecutionOrder(20000)] public class CameraMouseControl : MonoBehaviour { public static CameraMouseControl Current; @@ -44,7 +47,7 @@ void Start() private bool isTargetRotate = false; - void Update() + private void LateUpdate() { CheckUpdate(); } @@ -175,6 +178,8 @@ public void UpdateRelativePosition() doUpdateRelativePosition = true; } + private Vector3 oldLookAt; + void UpdateCamera() { if (doUpdateRelativePosition && PositionFixedTarget != null) @@ -187,11 +192,17 @@ void UpdateCamera() { var lookAt = LookTarget.position + LookOffset; + if (oldLookAt == Vector3.zero) oldLookAt = lookAt; + lookAt = Vector3.Lerp(oldLookAt, lookAt, Time.deltaTime * 10f); + oldLookAt = lookAt; // カメラとプレイヤーとの間の距離を調整 - setPosition = lookAt - (LookTarget.transform.forward) * (Settings.Current.CameraType == CameraTypes.Front ? -CameraDistance : CameraDistance); - + var oldPosition = transform.position; + setPosition = lookAt - (Quaternion.Euler(0, LookTarget.transform.rotation.eulerAngles.y, LookTarget.transform.rotation.eulerAngles.z) * Vector3.forward) * (Settings.Current.CameraType == CameraTypes.Front ? -CameraDistance : CameraDistance); + setPosition = Vector3.Lerp(oldPosition, setPosition, Time.deltaTime * 5f); + setPosition = lookAt - (Quaternion.LookRotation(setPosition - lookAt) * Vector3.forward) * -CameraDistance; transform.position = setPosition; + // 注視点の設定 transform.LookAt(lookAt); } @@ -206,13 +217,58 @@ void UpdateCamera() setPosition = CameraTarget + transform.rotation * Vector3.back * CameraDistance; } currentNoScaledPosition = setPosition; - if (parentTransform != null) + if (LookTarget == null && parentTransform != null) { setPosition = new Vector3(setPosition.x * parentTransform.localScale.x + parentTransform.position.x, setPosition.y * parentTransform.localScale.y + parentTransform.position.y, setPosition.z * parentTransform.localScale.z + parentTransform.position.z); } transform.position = setPosition; } + + public void FrontReset() + { + StartCoroutine(FrontResetCoroutine()); + } + + private IEnumerator FrontResetCoroutine() + { + yield return new WaitForEndOfFrame(); + + CameraDistance = 1.5f; //default + + if (LookTarget != null) + { + SaveLookTarget(); + }else{ + // free or position fixed + var currentLookTarget = CameraManager.Current.CurrentLookTarget.transform; + var lookAt = currentLookTarget.position + LookOffset; + + // カメラとプレイヤーとの間の距離を調整 + transform.position = lookAt - (currentLookTarget.transform.forward) * -CameraDistance; + + // 注視点の設定 + transform.LookAt(lookAt); + + CameraTarget = lookAt; + CameraAngle = -transform.eulerAngles; + + UpdateRelativePosition(); + + yield return new WaitForEndOfFrame(); + + if (Settings.Current.CameraType == CameraTypes.Free) + { + Settings.Current.FreeCameraTransform.SetPosition(currentNoScaledPosition); + Settings.Current.FreeCameraTransform.SetRotation(transform); + } + else if (Settings.Current.CameraType == CameraTypes.PositionFixed) + { + Settings.Current.PositionFixedCameraTransform.SetPositionAndRotation(transform); + } + } + } + private void SaveLookTarget() { if (Settings.Current.CameraType == CameraTypes.Front) diff --git a/Assets/Scripts/ControlWPFWindow.cs b/Assets/Scripts/ControlWPFWindow.cs index 76e845af..7f531af2 100644 --- a/Assets/Scripts/ControlWPFWindow.cs +++ b/Assets/Scripts/ControlWPFWindow.cs @@ -1,2885 +1,2327 @@ -using RootMotion.FinalIK; -using sh_akira; -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading.Tasks; -using UnityEngine; -using UnityMemoryMappedFile; -using Valve.VR; -using VMCMod; -using VRM; -using static VMC.NativeMethods; -#if UNITY_EDITOR // エディタ上でしか動きません。 -using UnityEditor; -#endif - -namespace VMC -{ - public class ControlWPFWindow : MonoBehaviour - { - public bool IsBeta = false; - public bool IsPreRelease = false; - - public string VersionString; - private string baseVersionString; - - public Transform LeftWristTransform = null; - public Transform RightWristTransform = null; - - public CameraLookTarget CalibrationCamera; - - public Renderer BackgroundRenderer; - - public GameObject GridCanvas; - - public DynamicOVRLipSync LipSync; - - public FaceController faceController; - public HandController handController; - - public WristRotationFix wristRotationFix; - - public Transform HandTrackerRoot; - public Transform PelvisTrackerRoot; - - public GameObject ExternalMotionSenderObject; - private ExternalSender externalMotionSender; - - public GameObject ExternalMotionReceiverObject; - public ExternalReceiverForVMC[] externalMotionReceivers; - - public MemoryMappedFileServer server; - private string pipeName = Guid.NewGuid().ToString(); - - private GameObject CurrentModel = null; - - private RootMotion.FinalIK.VRIK vrik = null; - - private Animator animator = null; - - private int CurrentWindowNum = 1; - - public int CriticalErrorCount = 0; - - public VMTClient vmtClient; - - public PostProcessingManager postProcessingManager; - - private uint defaultWindowStyle; - private uint defaultExWindowStyle; - - private System.Threading.SynchronizationContext context = null; - - public Action AdditionalSettingAction = null; - public Action VRMmetaLodedAction = null; - public Action VRMRemoteLoadedAction = null; - - public Action EyeTracking_TobiiCalibrationAction = null; - public Action SetEyeTracking_TobiiOffsetsAction = null; - public Action SetEyeTracking_ViveProEyeOffsetsAction = null; - public Action SetEyeTracking_ViveProEyeUseEyelidMovementsAction = null; - public Action> SetLipShapeToBlendShapeStringMapAction = null; - public Func> GetLipShapesStringListFunc = null; - - public Behaviour EyeTracking_ViveProEyeComponent = null; - public Behaviour SRanipal_Eye_FrameworkComponent = null; - public Behaviour LipTracking_ViveComponent = null; - public Behaviour SRanipal_Lip_FrameworkComponent = null; - - public MIDICCBlendShape midiCCBlendShape; - - public enum CalibrationState - { - Uncalibrated = 0, - WaitingForCalibrating = 1, - Calibrating = 2, - Calibrated = 3, - } - - public CalibrationState calibrationState = CalibrationState.Uncalibrated; - public PipeCommands.CalibrateType lastCalibrateType = PipeCommands.CalibrateType.Default; //最後に行ったキャリブレーションの種類 - - public string lastLoadedConfigPath = ""; - - public EasyDeviceDiscoveryProtocolManager easyDeviceDiscoveryProtocolManager; - - public ModManager modManager; - - private void Awake() - { - Application.targetFrameRate = 60; - -#if UNITY_EDITOR // エディタ上でしか動きません。 - pipeName = "VMCTest"; -#else - //Debug.unityLogger.logEnabled = false; - pipeName = "VMCpipe" + Guid.NewGuid().ToString(); -#endif - -#if !UNITY_EDITOR - //start control panel - ExecuteControlPanel(); -#endif - - context = System.Threading.SynchronizationContext.Current; - - baseVersionString = VersionString.Split('f').First(); - defaultWindowStyle = GetWindowLong(GetUnityWindowHandle(), GWL_STYLE); - defaultExWindowStyle = GetWindowLong(GetUnityWindowHandle(), GWL_EXSTYLE); - - server = new MemoryMappedFileServer(); - server.ReceivedEvent += Server_Received; - server.Start(pipeName); - - externalMotionSender = ExternalMotionSenderObject.GetComponent(); - externalMotionReceivers = ExternalMotionReceiverObject.GetComponentsInChildren(true); - } - - void Start() - { - Settings.Current.BackgroundColor = BackgroundRenderer.material.color; - Settings.Current.CustomBackgroundColor = BackgroundRenderer.material.color; - } - - private int SetWindowTitle() - { - int setWindowNum = 1; - var allWindowList = GetAllWindowHandle(); - var numlist = allWindowList.Where(p => p.Value.StartsWith(Application.productName + " ") && p.Value.EndsWith(")") && p.Value.Contains('(')).Select(t => int.Parse(t.Value.Split('(').Last().Replace(")", ""))).OrderBy(d => d); - while (numlist.Contains(setWindowNum)) - { - setWindowNum++; - } - var buildString = ""; - if (IsBeta) - { - buildString = "b" + VersionString.Split('b').Last(); - } - else if (IsPreRelease) - { - buildString = "r" + VersionString.Split('r').Last().Split('b').First(); - } - else - { - buildString = "f" + VersionString.Split('f').Last().Split('r').First(); - } - NativeMethods.SetUnityWindowTitle($"{Application.productName} {baseVersionString + buildString} ({setWindowNum})"); - return setWindowNum; - } - - private int doSendTrackerMoved = 0; - private Dictionary trackerMovedLastSendTime = new Dictionary(); - private async void TransformExtensions_TrackerMovedEvent(object sender, string e) - { - if (doSendTrackerMoved > 0) - { - if (trackerMovedLastSendTime.ContainsKey(e) == false) - { - trackerMovedLastSendTime.Add(e, DateTime.Now); - } - else if (DateTime.Now - trackerMovedLastSendTime[e] < TimeSpan.FromSeconds(1)) - { - return; - } - await server.SendCommandAsync(new PipeCommands.TrackerMoved { SerialNumber = e }); - trackerMovedLastSendTime[e] = DateTime.Now; - } - } - - private bool doStatusStringUpdated = false; - private async void StatusStringUpdatedEvent(string e) - { - if (doStatusStringUpdated) - { - await server.SendCommandAsync(new PipeCommands.StatusStringChanged { StatusString = e }); - } - } - - private bool ControlPanelExecuted = false; - private System.Diagnostics.Process controlPanelProcess = null; - private void ExecuteControlPanel() - { - if (ControlPanelExecuted == false) - { - var path = Application.dataPath + "/../ControlPanel/VirtualMotionCaptureControlPanel.exe"; - controlPanelProcess = new System.Diagnostics.Process(); - controlPanelProcess.StartInfo.FileName = path; - controlPanelProcess.StartInfo.Arguments = "/pipeName " + pipeName; - controlPanelProcess.EnableRaisingEvents = true; - controlPanelProcess.Exited += ControlPanelProcess_Exited; - controlPanelProcess.Start(); - ControlPanelExecuted = true; - } - } - - private void ControlPanelProcess_Exited(object sender, EventArgs e) - { - ControlPanelExecuted = false; - controlPanelProcess.Dispose(); - } - - private void OnApplicationQuit() - { - // アプリが終了したらコントロールパネルも終了する。 - server?.SendCommandAsync(new PipeCommands.QuitApplication { }); - - server.ReceivedEvent -= Server_Received; - server?.Dispose(); - - Application.logMessageReceived -= LogMessageHandler; - } - - private void Server_Received(object sender, DataReceivedEventArgs e) - { - context.Post(async s => - { - if (e.CommandType == typeof(PipeCommands.SetIsBeta)) - { - var d = (PipeCommands.SetIsBeta)e.Data; - IsBeta = d.IsBeta; - IsPreRelease = d.IsPreRelease; - - //エラー情報をWPFに飛ばす - Application.logMessageReceived += LogMessageHandler; - - if (IsPreRelease) - { - modManager.ImportMods(); - } - } - else if (e.CommandType == typeof(PipeCommands.LoadVRM)) - { - var d = (PipeCommands.LoadVRM)e.Data; - await server.SendCommandAsync(new PipeCommands.ReturnLoadVRM { Data = LoadVRM(d.Path) }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.LoadRemoteVRM)) - { - var d = (PipeCommands.LoadRemoteVRM)e.Data; - VRMRemoteLoadedAction?.Invoke(d.Path); - } - else if (e.CommandType == typeof(PipeCommands.ImportVRM)) - { - var d = (PipeCommands.ImportVRM)e.Data; - var t = ImportVRM(d.Path, d.ImportForCalibration, d.UseCurrentFixSetting ? Settings.Current.EnableNormalMapFix : d.EnableNormalMapFix, d.UseCurrentFixSetting ? Settings.Current.DeleteHairNormalMap : d.DeleteHairNormalMap); - - //メタ情報をOSC送信する - VRMmetaLodedAction?.Invoke(LoadVRM(d.Path)); - } - - else if (e.CommandType == typeof(PipeCommands.Calibrate)) - { - var d = (PipeCommands.Calibrate)e.Data; - StartCoroutine(Calibrate(d.CalibrateType)); - } - else if (e.CommandType == typeof(PipeCommands.EndCalibrate)) - { - EndCalibrate(); - } - - else if (e.CommandType == typeof(PipeCommands.SetLipSyncEnable)) - { - var d = (PipeCommands.SetLipSyncEnable)e.Data; - SetLipSyncEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.GetLipSyncDevices)) - { - var d = (PipeCommands.GetLipSyncDevices)e.Data; - await server.SendCommandAsync(new PipeCommands.ReturnGetLipSyncDevices { Devices = GetLipSyncDevices() }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetLipSyncDevice)) - { - var d = (PipeCommands.SetLipSyncDevice)e.Data; - SetLipSyncDevice(d.device); - } - else if (e.CommandType == typeof(PipeCommands.SetLipSyncGain)) - { - var d = (PipeCommands.SetLipSyncGain)e.Data; - SetLipSyncGain(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetLipSyncMaxWeightEnable)) - { - var d = (PipeCommands.SetLipSyncMaxWeightEnable)e.Data; - SetLipSyncMaxWeightEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetLipSyncWeightThreashold)) - { - var d = (PipeCommands.SetLipSyncWeightThreashold)e.Data; - SetLipSyncWeightThreashold(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetLipSyncMaxWeightEmphasis)) - { - var d = (PipeCommands.SetLipSyncMaxWeightEmphasis)e.Data; - SetLipSyncMaxWeightEmphasis(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.ChangeBackgroundColor)) - { - var d = (PipeCommands.ChangeBackgroundColor)e.Data; - ChangeBackgroundColor(d.r, d.g, d.b, d.isCustom); - } - else if (e.CommandType == typeof(PipeCommands.SetBackgroundTransparent)) - { - SetBackgroundTransparent(); - } - else if (e.CommandType == typeof(PipeCommands.SetWindowBorder)) - { - var d = (PipeCommands.SetWindowBorder)e.Data; - HideWindowBorder(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetWindowTopMost)) - { - var d = (PipeCommands.SetWindowTopMost)e.Data; - SetWindowTopMost(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetWindowClickThrough)) - { - var d = (PipeCommands.SetWindowClickThrough)e.Data; - SetWindowClickThrough(d.enable); - } - - else if (e.CommandType == typeof(PipeCommands.SetGridVisible)) - { - var d = (PipeCommands.SetGridVisible)e.Data; - SetGridVisible(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetAutoBlinkEnable)) - { - var d = (PipeCommands.SetAutoBlinkEnable)e.Data; - SetAutoBlinkEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetBlinkTimeMin)) - { - var d = (PipeCommands.SetBlinkTimeMin)e.Data; - SetBlinkTimeMin(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetBlinkTimeMax)) - { - var d = (PipeCommands.SetBlinkTimeMax)e.Data; - SetBlinkTimeMax(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetCloseAnimationTime)) - { - var d = (PipeCommands.SetCloseAnimationTime)e.Data; - SetCloseAnimationTime(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetOpenAnimationTime)) - { - var d = (PipeCommands.SetOpenAnimationTime)e.Data; - SetOpenAnimationTime(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetClosingTime)) - { - var d = (PipeCommands.SetClosingTime)e.Data; - SetClosingTime(d.value); - } - else if (e.CommandType == typeof(PipeCommands.SetDefaultFace)) - { - var d = (PipeCommands.SetDefaultFace)e.Data; - SetDefaultFace(d.face); - } - else if (e.CommandType == typeof(PipeCommands.LoadSettings)) - { - var d = (PipeCommands.LoadSettings)e.Data; - LoadSettings(d.Path); - //イベントを登録(何度呼び出しても1回のみ) - RegisterEventCallBack(); - } - else if (e.CommandType == typeof(PipeCommands.SaveSettings)) - { - var d = (PipeCommands.SaveSettings)e.Data; - SaveSettings(d.Path); - } - else if (e.CommandType == typeof(PipeCommands.SetControllerTouchPadPoints)) - { - var d = (PipeCommands.SetControllerTouchPadPoints)e.Data; - if (d.isStick) - { - Settings.Current.LeftThumbStickPoints = d.LeftPoints; - Settings.Current.RightThumbStickPoints = d.RightPoints; - } - else - { - Settings.Current.LeftCenterEnable = d.LeftCenterEnable; - Settings.Current.RightCenterEnable = d.RightCenterEnable; - Settings.Current.LeftTouchPadPoints = d.LeftPoints; - Settings.Current.RightTouchPadPoints = d.RightPoints; - } - } - else if (e.CommandType == typeof(PipeCommands.SetHandAngle)) - { - var d = (PipeCommands.SetHandAngle)e.Data; - handController.SetHandEulerAngles(d.LeftEnable, d.RightEnable, handController.CalcHandEulerAngles(d.HandAngles)); - } - else if (e.CommandType == typeof(PipeCommands.GetFaceKeys)) - { - await server.SendCommandAsync(new PipeCommands.ReturnFaceKeys { Keys = faceController.BlendShapeClips.Select(d => d.BlendShapeName).ToList() }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetFace)) - { - var d = (PipeCommands.SetFace)e.Data; - faceController.SetFace(d.Keys, d.Strength, true); - } - else if (e.CommandType == typeof(PipeCommands.ExitControlPanel)) - { - ControlPanelExecuted = false; - } - else if (e.CommandType == typeof(PipeCommands.SetHandFreeOffset)) - { - var d = (PipeCommands.SetHandFreeOffset)e.Data; - Settings.Current.LeftHandPositionX = d.LeftHandPositionX / 1000f; - Settings.Current.LeftHandPositionY = d.LeftHandPositionY / 1000f; - Settings.Current.LeftHandPositionZ = d.LeftHandPositionZ / 1000f; - Settings.Current.LeftHandRotationX = d.LeftHandRotationX; - Settings.Current.LeftHandRotationY = d.LeftHandRotationY; - Settings.Current.LeftHandRotationZ = d.LeftHandRotationZ; - Settings.Current.RightHandPositionX = d.RightHandPositionX / 1000f; - Settings.Current.RightHandPositionY = d.RightHandPositionY / 1000f; - Settings.Current.RightHandPositionZ = d.RightHandPositionZ / 1000f; - Settings.Current.RightHandRotationX = d.RightHandRotationX; - Settings.Current.RightHandRotationY = d.RightHandRotationY; - Settings.Current.RightHandRotationZ = d.RightHandRotationZ; - Settings.Current.SwivelOffset = d.SwivelOffset; - SetHandFreeOffset(); - } - else if (e.CommandType == typeof(PipeCommands.GetTrackerSerialNumbers)) - { - await server.SendCommandAsync(new PipeCommands.ReturnTrackerSerialNumbers { List = GetTrackerSerialNumbers(), CurrentSetting = GetCurrentTrackerSettings() }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetTrackerSerialNumbers)) - { - var d = (PipeCommands.SetTrackerSerialNumbers)e.Data; - SetTrackerSerialNumbers(d); - - } - else if (e.CommandType == typeof(PipeCommands.GetTrackerOffsets)) - { - await server.SendCommandAsync(new PipeCommands.SetTrackerOffsets - { - LeftHandTrackerOffsetToBodySide = Settings.Current.LeftHandTrackerOffsetToBodySide, - LeftHandTrackerOffsetToBottom = Settings.Current.LeftHandTrackerOffsetToBottom, - RightHandTrackerOffsetToBodySide = Settings.Current.RightHandTrackerOffsetToBodySide, - RightHandTrackerOffsetToBottom = Settings.Current.RightHandTrackerOffsetToBottom - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetTrackerOffsets)) - { - var d = (PipeCommands.SetTrackerOffsets)e.Data; - Settings.Current.LeftHandTrackerOffsetToBodySide = d.LeftHandTrackerOffsetToBodySide; - Settings.Current.LeftHandTrackerOffsetToBottom = d.LeftHandTrackerOffsetToBottom; - Settings.Current.RightHandTrackerOffsetToBodySide = d.RightHandTrackerOffsetToBodySide; - Settings.Current.RightHandTrackerOffsetToBottom = d.RightHandTrackerOffsetToBottom; - - } - else if (e.CommandType == typeof(PipeCommands.GetResolutions)) - { - await server.SendCommandAsync(new PipeCommands.ReturnResolutions - { - List = new List>(Screen.resolutions.Select(r => Tuple.Create(r.width, r.height, r.refreshRate))), - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetResolution)) - { - var d = (PipeCommands.SetResolution)e.Data; - Settings.Current.ScreenWidth = d.Width; - Settings.Current.ScreenHeight = d.Height; - Settings.Current.ScreenRefreshRate = d.RefreshRate; - Screen.SetResolution(d.Width, d.Height, false, d.RefreshRate); - } - else if (e.CommandType == typeof(PipeCommands.SetLightAngle)) - { - var d = (PipeCommands.SetLightAngle)e.Data; - SetLightAngle(d.X, d.Y); - } - else if (e.CommandType == typeof(PipeCommands.ChangeLightColor)) - { - var d = (PipeCommands.ChangeLightColor)e.Data; - ChangeLightColor(d.a, d.r, d.g, d.b); - } - else if (e.CommandType == typeof(PipeCommands.TrackerMovedRequest)) - { - //イベントを登録(何度呼び出しても1回のみ) - RegisterEventCallBack(); - - var d = (PipeCommands.TrackerMovedRequest)e.Data; - if (d.doSend) - { - doSendTrackerMoved++; - } - else - { - doSendTrackerMoved--; - } - } - else if (e.CommandType == typeof(PipeCommands.SetEyeTracking_TobiiOffsets)) - { - var d = (PipeCommands.SetEyeTracking_TobiiOffsets)e.Data; - SetEyeTracking_TobiiOffsets(d); - } - else if (e.CommandType == typeof(PipeCommands.GetEyeTracking_TobiiOffsets)) - { - await server.SendCommandAsync(new PipeCommands.SetEyeTracking_TobiiOffsets - { - OffsetHorizontal = Settings.Current.EyeTracking_TobiiOffsetHorizontal, - OffsetVertical = Settings.Current.EyeTracking_TobiiOffsetVertical, - ScaleHorizontal = Settings.Current.EyeTracking_TobiiScaleHorizontal, - ScaleVertical = Settings.Current.EyeTracking_TobiiScaleVertical - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.EyeTracking_TobiiCalibration)) - { - EyeTracking_TobiiCalibrationAction?.Invoke(CurrentModel); - } - else if (e.CommandType == typeof(PipeCommands.SetEyeTracking_ViveProEyeOffsets)) - { - var d = (PipeCommands.SetEyeTracking_ViveProEyeOffsets)e.Data; - SetEyeTracking_ViveProEyeOffsets(d); - } - else if (e.CommandType == typeof(PipeCommands.GetEyeTracking_ViveProEyeOffsets)) - { - await server.SendCommandAsync(new PipeCommands.SetEyeTracking_ViveProEyeOffsets - { - OffsetHorizontal = Settings.Current.EyeTracking_ViveProEyeOffsetHorizontal, - OffsetVertical = Settings.Current.EyeTracking_ViveProEyeOffsetVertical, - ScaleHorizontal = Settings.Current.EyeTracking_ViveProEyeScaleHorizontal, - ScaleVertical = Settings.Current.EyeTracking_ViveProEyeScaleVertical - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements)) - { - var d = (PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements)e.Data; - SetEyeTracking_ViveProEyeUseEyelidMovements(d); - } - else if (e.CommandType == typeof(PipeCommands.SetEyeTracking_ViveProEyeEnable)) - { - var d = (PipeCommands.SetEyeTracking_ViveProEyeEnable)e.Data; - Settings.Current.EyeTracking_ViveProEyeEnable = d.enable; - SetEyeTracking_ViveProEyeEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.GetEyeTracking_ViveProEyeUseEyelidMovements)) - { - await server.SendCommandAsync(new PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements - { - Use = Settings.Current.EyeTracking_ViveProEyeUseEyelidMovements, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetEyeTracking_ViveProEyeEnable)) - { - await server.SendCommandAsync(new PipeCommands.SetEyeTracking_ViveProEyeEnable - { - enable = Settings.Current.EyeTracking_ViveProEyeEnable, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.LoadCurrentSettings)) - { - if (isFirstTimeExecute) - { - isFirstTimeExecute = false; - CurrentWindowNum = SetWindowTitle(); - //起動時は初期設定ロード - LoadSettings(null); - //イベントを登録(何度呼び出しても1回のみ) - RegisterEventCallBack(); - } - else - { - //現在の設定を再適用する - ApplySettings(); - } - } - else if (e.CommandType == typeof(PipeCommands.EnableExternalMotionSender)) - { - var d = (PipeCommands.EnableExternalMotionSender)e.Data; - SetExternalMotionSenderEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.GetEnableExternalMotionSender)) - { - await server.SendCommandAsync(new PipeCommands.EnableExternalMotionSender - { - enable = Settings.Current.ExternalMotionSenderEnable - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.ChangeExternalMotionSenderAddress)) - { - var d = (PipeCommands.ChangeExternalMotionSenderAddress)e.Data; - ChangeExternalMotionSenderAddress(d.address, d.port, d.PeriodStatus, d.PeriodRoot, d.PeriodBone, d.PeriodBlendShape, d.PeriodCamera, d.PeriodDevices, d.OptionString, d.ResponderEnable); - - } - else if (e.CommandType == typeof(PipeCommands.GetExternalMotionSenderAddress)) - { - await server.SendCommandAsync(new PipeCommands.ChangeExternalMotionSenderAddress - { - address = Settings.Current.ExternalMotionSenderAddress, - port = Settings.Current.ExternalMotionSenderPort, - PeriodStatus = Settings.Current.ExternalMotionSenderPeriodStatus, - PeriodRoot = Settings.Current.ExternalMotionSenderPeriodRoot, - PeriodBone = Settings.Current.ExternalMotionSenderPeriodBone, - PeriodBlendShape = Settings.Current.ExternalMotionSenderPeriodBlendShape, - PeriodCamera = Settings.Current.ExternalMotionSenderPeriodCamera, - PeriodDevices = Settings.Current.ExternalMotionSenderPeriodDevices, - OptionString = Settings.Current.ExternalMotionSenderOptionString, - ResponderEnable = Settings.Current.ExternalMotionSenderResponderEnable - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.EnableExternalMotionReceiver)) - { - var d = (PipeCommands.EnableExternalMotionReceiver)e.Data; - SetExternalMotionReceiverEnable(d.enable, d.index); - } - else if (e.CommandType == typeof(PipeCommands.GetEnableExternalMotionReceiver)) - { - var d = (PipeCommands.GetEnableExternalMotionReceiver)e.Data; - await server.SendCommandAsync(new PipeCommands.EnableExternalMotionReceiver - { - enable = Settings.Current.ExternalMotionReceiverEnableList[d.index], - index = d.index - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.ChangeExternalMotionReceiverPort)) - { - var d = (PipeCommands.ChangeExternalMotionReceiverPort)e.Data; - ChangeExternalMotionReceiverPort(d.ports, d.RequesterEnable); - - } - else if (e.CommandType == typeof(PipeCommands.GetExternalMotionReceiverPort)) - { - await server.SendCommandAsync(new PipeCommands.ChangeExternalMotionReceiverPort - { - ports = Settings.Current.ExternalMotionReceiverPortList.ToArray(), - RequesterEnable = Settings.Current.ExternalMotionReceiverRequesterEnable - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetMidiCCBlendShape)) - { - var bs = Settings.Current.MidiCCBlendShape; - await server.SendCommandAsync(new PipeCommands.SetMidiCCBlendShape - { - BlendShapes = Settings.Current.MidiCCBlendShape, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetMidiCCBlendShape)) - { - var d = (PipeCommands.SetMidiCCBlendShape)e.Data; - SetMidiCCBlendShape(d.BlendShapes); - } - else if (e.CommandType == typeof(PipeCommands.GetMidiEnable)) - { - await server.SendCommandAsync(new PipeCommands.MidiEnable - { - enable = Settings.Current.MidiEnable, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.MidiEnable)) - { - var d = (PipeCommands.MidiEnable)e.Data; - SetMidiEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.EnableTrackingFilter)) - { - var d = (PipeCommands.EnableTrackingFilter)e.Data; - SetTrackingFilterEnable(d.globalEnable, d.hmdEnable, d.controllerEnable, d.trackerEnable); - } - else if (e.CommandType == typeof(PipeCommands.GetPauseTracking)) - { - await server.SendCommandAsync(new PipeCommands.PauseTracking - { - enable = DeviceInfo.pauseTracking - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.PauseTracking)) - { - var d = (PipeCommands.PauseTracking)e.Data; - DeviceInfo.pauseTracking = d.enable; - } - else if (e.CommandType == typeof(PipeCommands.GetEnableTrackingFilter)) - { - await server.SendCommandAsync(new PipeCommands.EnableTrackingFilter - { - globalEnable = Settings.Current.TrackingFilterEnable, - hmdEnable = Settings.Current.TrackingFilterHmdEnable, - controllerEnable = Settings.Current.TrackingFilterControllerEnable, - trackerEnable = Settings.Current.TrackingFilterTrackerEnable, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.EnableModelModifier)) - { - var d = (PipeCommands.EnableModelModifier)e.Data; - SetModelModifierEnable(d.fixKneeRotation, d.fixElbowRotation); - } - else if (e.CommandType == typeof(PipeCommands.GetEnableModelModifier)) - { - await server.SendCommandAsync(new PipeCommands.EnableModelModifier - { - fixKneeRotation = Settings.Current.FixKneeRotation, - fixElbowRotation = Settings.Current.FixElbowRotation, - }, e.RequestId); - } - //------------------------ - else if (e.CommandType == typeof(PipeCommands.GetStatusString)) - { - string statusStringBuf = ""; - //有効な場合だけ送る - if (externalMotionReceivers[0].isActiveAndEnabled) - { - statusStringBuf = externalMotionReceivers?[0]?.statusString; - } - await server.SendCommandAsync(new PipeCommands.SetStatusString - { - StatusString = statusStringBuf, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.StatusStringChangedRequest)) - { - var d = (PipeCommands.StatusStringChangedRequest)e.Data; - doStatusStringUpdated = d.doSend; - } - else if (e.CommandType == typeof(PipeCommands.EnableHandleControllerAsTracker)) - { - var d = (PipeCommands.EnableHandleControllerAsTracker)e.Data; - SetHandleControllerAsTracker(d.HandleControllerAsTracker); - } - else if (e.CommandType == typeof(PipeCommands.GetHandleControllerAsTracker)) - { - await server.SendCommandAsync(new PipeCommands.EnableHandleControllerAsTracker - { - HandleControllerAsTracker = Settings.Current.HandleControllerAsTracker - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetQualitySettings)) - { - await server.SendCommandAsync(new PipeCommands.SetQualitySettings - { - antiAliasing = Settings.Current.AntiAliasing, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetQualitySettings)) - { - var d = (PipeCommands.SetQualitySettings)e.Data; - SetQualitySettings(d); - } - else if (e.CommandType == typeof(PipeCommands.GetVirtualMotionTracker)) - { - await server.SendCommandAsync(new PipeCommands.SetVirtualMotionTracker - { - enable = vmtClient.GetEnable(), - no = vmtClient.GetNo() - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetVirtualMotionTracker)) - { - var d = (PipeCommands.SetVirtualMotionTracker)e.Data; - SetVMT(d.enable, d.no); - } - else if (e.CommandType == typeof(PipeCommands.SetupVirtualMotionTracker)) - { - var d = (PipeCommands.SetupVirtualMotionTracker)e.Data; - var ret = d.install ? await VMTServer.InstallVMT() : await VMTServer.UninstallVMT(); - await server.SendCommandAsync(new PipeCommands.ResultSetupVirtualMotionTracker - { - result = ret, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetViveLipTrackingBlendShape)) - { - if (GetLipShapesStringListFunc != null) - { - await server.SendCommandAsync(new PipeCommands.SetViveLipTrackingBlendShape - { - LipShapes = GetLipShapesStringListFunc(), - LipShapesToBlendShapeMap = Settings.Current.LipShapesToBlendShapeMap, - }, e.RequestId); - } - } - else if (e.CommandType == typeof(PipeCommands.GetViveLipTrackingEnable)) - { - await server.SendCommandAsync(new PipeCommands.SetViveLipTrackingEnable - { - enable = Settings.Current.LipTracking_ViveEnable, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.SetViveLipTrackingEnable)) - { - var d = (PipeCommands.SetViveLipTrackingEnable)e.Data; - Settings.Current.LipTracking_ViveEnable = d.enable; - SetLipTracking_ViveEnable(d.enable); - } - else if (e.CommandType == typeof(PipeCommands.SetViveLipTrackingBlendShape)) - { - var d = (PipeCommands.SetViveLipTrackingBlendShape)e.Data; - Settings.Current.LipShapesToBlendShapeMap = d.LipShapesToBlendShapeMap; - SetLipShapeToBlendShapeStringMapAction?.Invoke(d.LipShapesToBlendShapeMap); - } - else if (e.CommandType == typeof(PipeCommands.GetAdvancedGraphicsOption)) - { - LoadAdvancedGraphicsOption(); - } - else if (e.CommandType == typeof(PipeCommands.SetAdvancedGraphicsOption)) - { - var d = (PipeCommands.SetAdvancedGraphicsOption)e.Data; - - Settings.Current.PPS_Enable = d.PPS_Enable; - - Settings.Current.PPS_Bloom_Enable = d.Bloom_Enable; - Settings.Current.PPS_Bloom_Intensity = d.Bloom_Intensity; - Settings.Current.PPS_Bloom_Threshold = d.Bloom_Threshold; - - Settings.Current.PPS_DoF_Enable = d.DoF_Enable; - Settings.Current.PPS_DoF_FocusDistance = d.DoF_FocusDistance; - Settings.Current.PPS_DoF_Aperture = d.DoF_Aperture; - Settings.Current.PPS_DoF_FocusLength = d.DoF_FocusLength; - Settings.Current.PPS_DoF_MaxBlurSize = d.DoF_MaxBlurSize; - - Settings.Current.PPS_CG_Enable = d.CG_Enable; - Settings.Current.PPS_CG_Temperature = d.CG_Temperature; - Settings.Current.PPS_CG_Saturation = d.CG_Saturation; - Settings.Current.PPS_CG_Contrast = d.CG_Contrast; - Settings.Current.PPS_CG_Gamma = d.CG_Gamma; - - Settings.Current.PPS_Vignette_Enable = d.Vignette_Enable; - Settings.Current.PPS_Vignette_Intensity = d.Vignette_Intensity; - Settings.Current.PPS_Vignette_Smoothness = d.Vignette_Smoothness; - Settings.Current.PPS_Vignette_Roundness = d.Vignette_Roundness; - - Settings.Current.PPS_CA_Enable = d.CA_Enable; - Settings.Current.PPS_CA_Intensity = d.CA_Intensity; - Settings.Current.PPS_CA_FastMode = d.CA_FastMode; - - Settings.Current.PPS_Bloom_Color_a = d.Bloom_Color_a; - Settings.Current.PPS_Bloom_Color_r = d.Bloom_Color_r; - Settings.Current.PPS_Bloom_Color_g = d.Bloom_Color_g; - Settings.Current.PPS_Bloom_Color_b = d.Bloom_Color_b; - - Settings.Current.PPS_CG_ColorFilter_a = d.CG_ColorFilter_a; - Settings.Current.PPS_CG_ColorFilter_r = d.CG_ColorFilter_r; - Settings.Current.PPS_CG_ColorFilter_g = d.CG_ColorFilter_g; - Settings.Current.PPS_CG_ColorFilter_b = d.CG_ColorFilter_b; - - Settings.Current.PPS_Vignette_Color_a = d.Vignette_Color_a; - Settings.Current.PPS_Vignette_Color_r = d.Vignette_Color_r; - Settings.Current.PPS_Vignette_Color_g = d.Vignette_Color_g; - Settings.Current.PPS_Vignette_Color_b = d.Vignette_Color_b; - - Settings.Current.TurnOffAmbientLight = d.TurnOffAmbientLight; - - SetAdvancedGraphicsOption(); - } - else if (e.CommandType == typeof(PipeCommands.ExternalReceiveBones)) - { - var d = (PipeCommands.ExternalReceiveBones)e.Data; - SetExternalBonesReceiverEnable(d.ReceiveBonesEnable); - } - - else if (e.CommandType == typeof(PipeCommands.GetExternalReceiveBones)) - { - await server.SendCommandAsync(new PipeCommands.ExternalReceiveBones - { - ReceiveBonesEnable = Settings.Current.ExternalBonesReceiverEnable - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetModIsLoaded)) - { - await server.SendCommandAsync(new PipeCommands.ReturnModIsLoaded - { - IsLoaded = modManager.IsModLoaded, - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.GetModList)) - { - await server.SendCommandAsync(new PipeCommands.ReturnModList - { - ModList = GetModList(), - }, e.RequestId); - } - else if (e.CommandType == typeof(PipeCommands.ModSettingEvent)) - { - var d = (PipeCommands.ModSettingEvent)e.Data; - modManager.InvokeSetting(d.InstanceId); - } - else if (e.CommandType == typeof(PipeCommands.SetLogNotifyLevel)) - { - var d = (PipeCommands.SetLogNotifyLevel)e.Data; - notifyLogLevel = d.type; - } - - else if (e.CommandType == typeof(PipeCommands.Alive)) - { - await server.SendCommandAsync(new PipeCommands.Alive { }); - } - }, null); - } - - private List GetModList() - { - var modList = new List(); - - foreach (var attribute in modManager.GetModsList()) - { - var item = new ModItem - { - Name = attribute.Name, - Version = attribute.Version, - Author = attribute.Author, - AuthorURL = attribute.AuthorURL, - Description = attribute.Description, - PluginURL = attribute.PluginURL, - InstanceId = attribute.InstanceId, - AssemblyPath = attribute.AssemblyPath, - }; - modList.Add(item); - } - - return modList; - } - - public Transform MainDirectionalLightTransform; - public Light MainDirectionalLight; - - private void SetLightAngle(float x, float y) - { - if (MainDirectionalLightTransform != null) - { - MainDirectionalLightTransform.eulerAngles = new Vector3(x, y, MainDirectionalLightTransform.eulerAngles.z); - Settings.Current.LightRotationX = x; - Settings.Current.LightRotationY = y; - - VMCEvents.OnLightChanged?.Invoke(); - } - } - - private void ChangeLightColor(float a, float r, float g, float b) - { - if (MainDirectionalLight != null) - { - Settings.Current.LightColor = new Color(r, g, b, a); - MainDirectionalLight.color = Settings.Current.LightColor; - - VMCEvents.OnLightChanged?.Invoke(); - } - } - - private void SetQualitySettings(PipeCommands.SetQualitySettings setting) - { - Settings.Current.AntiAliasing = setting.antiAliasing; - QualitySettings.antiAliasing = setting.antiAliasing; - } - - private void SetVMT(bool enable, int no) - { - vmtClient.SetNo(no); - vmtClient.SetEnable(enable); - vmtClient.SendRoomMatrixTemporary(); - - Settings.Current.VirtualMotionTrackerNo = no; - Settings.Current.VirtualMotionTrackerEnable = enable; - } - - private async void LoadAdvancedGraphicsOption() - { - SetAdvancedGraphicsOption(); - await server.SendCommandAsync(new PipeCommands.SetAdvancedGraphicsOption - { - PPS_Enable = Settings.Current.PPS_Enable, - - Bloom_Enable = Settings.Current.PPS_Bloom_Enable, - Bloom_Intensity = Settings.Current.PPS_Bloom_Intensity, - Bloom_Threshold = Settings.Current.PPS_Bloom_Threshold, - - DoF_Enable = Settings.Current.PPS_DoF_Enable, - DoF_FocusDistance = Settings.Current.PPS_DoF_FocusDistance, - DoF_Aperture = Settings.Current.PPS_DoF_Aperture, - DoF_FocusLength = Settings.Current.PPS_DoF_FocusLength, - DoF_MaxBlurSize = Settings.Current.PPS_DoF_MaxBlurSize, - - CG_Enable = Settings.Current.PPS_CG_Enable, - CG_Temperature = Settings.Current.PPS_CG_Temperature, - CG_Saturation = Settings.Current.PPS_CG_Saturation, - CG_Contrast = Settings.Current.PPS_CG_Contrast, - CG_Gamma = Settings.Current.PPS_CG_Gamma, - - Vignette_Enable = Settings.Current.PPS_Vignette_Enable, - Vignette_Intensity = Settings.Current.PPS_Vignette_Intensity, - Vignette_Smoothness = Settings.Current.PPS_Vignette_Smoothness, - Vignette_Roundness = Settings.Current.PPS_Vignette_Roundness, - - CA_Enable = Settings.Current.PPS_CA_Enable, - CA_Intensity = Settings.Current.PPS_CA_Intensity, - CA_FastMode = Settings.Current.PPS_CA_FastMode, - - Bloom_Color_a = Settings.Current.PPS_Bloom_Color_a, - Bloom_Color_r = Settings.Current.PPS_Bloom_Color_r, - Bloom_Color_g = Settings.Current.PPS_Bloom_Color_g, - Bloom_Color_b = Settings.Current.PPS_Bloom_Color_b, - - CG_ColorFilter_a = Settings.Current.PPS_CG_ColorFilter_a, - CG_ColorFilter_r = Settings.Current.PPS_CG_ColorFilter_r, - CG_ColorFilter_g = Settings.Current.PPS_CG_ColorFilter_g, - CG_ColorFilter_b = Settings.Current.PPS_CG_ColorFilter_b, - - Vignette_Color_a = Settings.Current.PPS_Vignette_Color_a, - Vignette_Color_r = Settings.Current.PPS_Vignette_Color_r, - Vignette_Color_g = Settings.Current.PPS_Vignette_Color_g, - Vignette_Color_b = Settings.Current.PPS_Vignette_Color_b, - - TurnOffAmbientLight = Settings.Current.TurnOffAmbientLight - }); - } - - private void SetAdvancedGraphicsOption() - { - postProcessingManager.Apply(Settings.Current); - } - - private bool isFirstTimeExecute = true; - - #region VRM - - public VRMData LoadVRM(string path) - { - if (string.IsNullOrEmpty(path) || File.Exists(path) == false) - { - return null; - } - var vrmdata = new VRMData(); - vrmdata.FilePath = path; - var context = new VRMImporterContext(); - - var bytes = File.ReadAllBytes(path); - - // GLB形式でJSONを取得しParseします - context.ParseGlb(bytes); - - // metaを取得 - var meta = context.ReadMeta(true); - //サムネイル - if (meta.Thumbnail != null) - { - vrmdata.ThumbnailPNGBytes = meta.Thumbnail.EncodeToPNG(); //Or SaveAsPng( memoryStream, texture.Width, texture.Height ) - } - //Info - vrmdata.Title = meta.Title; - vrmdata.Version = meta.Version; - vrmdata.Author = meta.Author; - vrmdata.ContactInformation = meta.ContactInformation; - vrmdata.Reference = meta.Reference; - - // Permission - vrmdata.AllowedUser = (UnityMemoryMappedFile.AllowedUser)meta.AllowedUser; - vrmdata.ViolentUssage = (UnityMemoryMappedFile.UssageLicense)meta.ViolentUssage; - vrmdata.SexualUssage = (UnityMemoryMappedFile.UssageLicense)meta.SexualUssage; - vrmdata.CommercialUssage = (UnityMemoryMappedFile.UssageLicense)meta.CommercialUssage; - vrmdata.OtherPermissionUrl = meta.OtherPermissionUrl; - - // Distribution License - vrmdata.LicenseType = (UnityMemoryMappedFile.LicenseType)meta.LicenseType; - vrmdata.OtherLicenseUrl = meta.OtherLicenseUrl; - /* - // ParseしたJSONをシーンオブジェクトに変換していく - var now = Time.time; - var go = await VRMImporter.LoadVrmAsync(context); - - var delta = Time.time - now; - Debug.LogFormat("LoadVrmAsync {0:0.0} seconds", delta); - //OnLoaded(go); - */ - return vrmdata; - } - private const float LeftLowerArmAngle = -30f; - private const float RightLowerArmAngle = -30f; - private const float LeftUpperArmAngle = -30f; - private const float RightUpperArmAngle = -30f; - private const float LeftHandAngle = -30f; - private const float RightHandAngle = -30f; - - public async Task ImportVRM(string path, bool ImportForCalibration, bool EnableNormalMapFix, bool DeleteHairNormalMap) - { - if (ImportForCalibration == false) - { - calibrationState = CalibrationState.Uncalibrated; //キャリブレーション状態を"未キャリブレーション"に設定 - Settings.Current.VRMPath = path; - var context = new VRMImporterContext(); - - var bytes = File.ReadAllBytes(path); - - // GLB形式でJSONを取得しParseします - context.ParseGlb(bytes); - - // ParseしたJSONをシーンオブジェクトに変換していく - //CurrentModel = await VRMImporter.LoadVrmAsync(context); - await context.LoadAsyncTask(); - context.ShowMeshes(); - - //BlendShape目線制御時の表情とのぶつかりを防ぐ - if (context.GLTF.extensions.VRM.firstPerson.lookAtType == LookAtType.BlendShape) - { - var applyer = context.Root.GetComponent(); - applyer.enabled = false; - - var vmcapplyer = context.Root.AddComponent(); - vmcapplyer.OnImported(context); - vmcapplyer.faceController = faceController; - } - - LoadNewModel(context.Root); - } - else - { - calibrationState = CalibrationState.WaitingForCalibrating; //キャリブレーション状態を"キャリブレーション待機中"に設定 - - if (CurrentModel != null) - { - var currentvrik = CurrentModel.GetComponent(); - if (currentvrik != null) Destroy(currentvrik); - var rootController = CurrentModel.GetComponent(); - if (rootController != null) Destroy(rootController); - } - LoadDefaultCurrentModelTransforms(); - //SetVRIK(CurrentModel); - if (animator != null) - { - //animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(LeftLowerArmAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(RightLowerArmAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(LeftUpperArmAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(RightUpperArmAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(LeftHandAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(RightHandAngle, 0, 0); - - animator.GetBoneTransform(HumanBodyBones.LeftShoulder).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightShoulder).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(0, 0, 80); - animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(0, 0, 5); - animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(0, 0, -80); - animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(0, 0, -5); - animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(0, 0, 0); - - //wristRotationFix.SetVRIK(vrik); - - handController.SetDefaultAngle(animator); - - //トラッカーのスケールリセット - HandTrackerRoot.localPosition = Vector3.zero; - HandTrackerRoot.localScale = Vector3.one; - PelvisTrackerRoot.localPosition = Vector3.zero; - PelvisTrackerRoot.localScale = Vector3.one; - - //トラッカー位置の表示 - TrackingPointManager.Instance.SetTrackingPointPositionVisible(true); - - if (CalibrationCamera != null) - { - CalibrationCamera.Target = animator.GetBoneTransform(HumanBodyBones.Head); - CalibrationCamera.gameObject.SetActive(true); - } - } - } - } - - public void LoadNewModel(GameObject model) - { - if (CurrentModel != null) - { - VMCEvents.OnModelUnloading?.Invoke(CurrentModel); - CurrentModel.transform.SetParent(null); - CurrentModel.SetActive(false); - Destroy(CurrentModel); - CurrentModel = null; - } - CurrentModel = model; - - ModelInitialize(); - } - - public void ModelInitialize() - { - - SaveDefaultCurrentModelTransforms(); - - //Settings.Current.EnableNormalMapFix = EnableNormalMapFix; - //Settings.Current.DeleteHairNormalMap = DeleteHairNormalMap; - //if (EnableNormalMapFix) - //{ - // //VRoidモデルのNormalMapテカテカを修正する - // Yashinut.VRoid.CorrectNormalMapImport.CorrectNormalMap(CurrentModel, DeleteHairNormalMap); - //} - - //モデルのSkinnedMeshRendererがカリングされないように、すべてのオプション変更 - foreach (var renderer in CurrentModel.GetComponentsInChildren(true)) - { - renderer.updateWhenOffscreen = true; - } - - //LipSync - LipSync.ImportVRMmodel(CurrentModel); - //まばたき - faceController.ImportVRMmodel(CurrentModel); - - //CurrentModel.transform.SetParent(transform, false); - - animator = CurrentModel.GetComponent(); - - SetVRIK(CurrentModel); - - if (animator != null) - { - wristRotationFix.SetVRIK(vrik); - - animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).eulerAngles = new Vector3(LeftLowerArmAngle, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightLowerArm).eulerAngles = new Vector3(RightLowerArmAngle, 0, 0); - animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).eulerAngles = new Vector3(LeftUpperArmAngle, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightUpperArm).eulerAngles = new Vector3(RightUpperArmAngle, 0, 0); - - handController.SetDefaultAngle(animator); - } - //SetTrackersToVRIK(); - - VMCEvents.OnModelLoaded?.Invoke(CurrentModel); - } - /* - private Vector3 DefaultModelPosition; - private Quaternion DefaultModelRotation; - private Vector3 DefaultModelScale; - private Dictionary DefaultRotations; - - public void SaveDefaultCurrentModelTransforms() - { - DefaultModelPosition = CurrentModel.transform.position; - DefaultModelRotation = CurrentModel.transform.rotation; - DefaultModelScale = CurrentModel.transform.localScale; - DefaultRotations = new Dictionary(); - var animator = CurrentModel.GetComponent(); - for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) - { - var t = animator.GetBoneTransform((HumanBodyBones)i); - if (t != null) - { - DefaultRotations.Add((HumanBodyBones)i, t.rotation); - } - } - } - - public void LoadDefaultCurrentModelTransforms() - { - if (DefaultRotations == null) return; - CurrentModel.transform.position = DefaultModelPosition; - CurrentModel.transform.rotation = DefaultModelRotation; - CurrentModel.transform.localScale = DefaultModelScale; - foreach (var pair in DefaultRotations) - { - var t = animator.GetBoneTransform(pair.Key); - if (t != null) - { - t.rotation = pair.Value; - } - } - } - */ - - private GameObject PositionSavedModel; - private Vector3 DefaultModelPosition; - private Quaternion DefaultModelRotation; - private Vector3 DefaultModelScale; - private Dictionary DefaultPositions; - private Dictionary DefaultRotations; - private Dictionary DefaultScales; - private Dictionary DefaultColliders; - - public void SaveDefaultCurrentModelTransforms() - { - PositionSavedModel = CurrentModel; - DefaultModelPosition = CurrentModel.transform.position; - DefaultModelRotation = CurrentModel.transform.rotation; - DefaultModelScale = CurrentModel.transform.localScale; - DefaultPositions = new Dictionary(); - DefaultRotations = new Dictionary(); - DefaultScales = new Dictionary(); - DefaultColliders = new Dictionary(); - var allTransforms = CurrentModel.transform.GetComponentsInChildren(true); - foreach (var t in allTransforms) - { - DefaultPositions.Add(t, t.position); - DefaultRotations.Add(t, t.rotation); - DefaultScales.Add(t, t.localScale); - } - - //VRMモデルのコライダー - var springBoneColiderGroups = CurrentModel.GetComponentsInChildren(); - foreach (var springBoneColiderGroup in springBoneColiderGroups) - { - foreach (var collider in springBoneColiderGroup.Colliders) - { - DefaultColliders.Add(collider, new Vector4(collider.Offset.x, collider.Offset.y, collider.Offset.z, collider.Radius)); - } - } - } - - public void LoadDefaultCurrentModelTransforms() - { - if (PositionSavedModel != CurrentModel || CurrentModel == null) return; - CurrentModel.transform.localScale = DefaultModelScale; - CurrentModel.transform.rotation = DefaultModelRotation; - CurrentModel.transform.position = DefaultModelPosition; - var animator = CurrentModel.GetComponent(); - foreach (var pair in DefaultScales) - { - var t = pair.Key; - if (t != null) - { - t.localScale = pair.Value; - } - } - foreach (var pair in DefaultRotations) - { - var t = pair.Key; - if (t != null) - { - t.rotation = pair.Value; - } - } - foreach (var pair in DefaultPositions) - { - var t = pair.Key; - if (t != null) - { - t.position = pair.Value; - } - } - - //VRMモデルのコライダー - var springBoneColiderGroups = CurrentModel.GetComponentsInChildren(); - foreach (var springBoneColiderGroup in springBoneColiderGroups) - { - foreach (var collider in springBoneColiderGroup.Colliders) - { - if (DefaultColliders.ContainsKey(collider)) - { - var col = DefaultColliders[collider]; - collider.Offset = new Vector3(col.x, col.y, col.z); - collider.Radius = col.w; - } - } - } - } - - #endregion - - #region Calibration - - public void FixLegDirection(GameObject targetHumanoidModel) - { - var avatarForward = targetHumanoidModel.transform.forward; - var animator = targetHumanoidModel.GetComponent(); - - var leftUpperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); - var leftLowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg); - var leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); - var leftFootDefaultRotation = leftFoot.rotation; - var leftFootTargetPosition = new Vector3(leftFoot.position.x, leftFoot.position.y, leftFoot.position.z); - LookAtBones(leftFootTargetPosition + avatarForward * 0.03f, leftUpperLeg, leftLowerLeg); - LookAtBones(leftFootTargetPosition, leftLowerLeg, leftFoot); - leftFoot.rotation = leftFootDefaultRotation; - - var rightUpperLeg = animator.GetBoneTransform(HumanBodyBones.RightUpperLeg); - var rightLowerLeg = animator.GetBoneTransform(HumanBodyBones.RightLowerLeg); - var rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot); - var rightFootDefaultRotation = rightFoot.rotation; - var rightFootTargetPosition = new Vector3(rightFoot.position.x, rightFoot.position.y, rightFoot.position.z); - LookAtBones(rightFootTargetPosition + avatarForward * 0.03f, rightUpperLeg, rightLowerLeg); - LookAtBones(rightFootTargetPosition, rightLowerLeg, rightFoot); - rightFoot.rotation = rightFootDefaultRotation; - } - - public void FixArmDirection(GameObject targetHumanoidModel) - { - var avatarForward = targetHumanoidModel.transform.forward; - var animator = targetHumanoidModel.GetComponent(); - - var leftShoulder = animator.GetBoneTransform(HumanBodyBones.LeftShoulder); - var leftUpperArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm); - var leftLowerArm = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm); - var leftHand = animator.GetBoneTransform(HumanBodyBones.LeftHand); - var leftHandDefaultRotation = leftHand.rotation; - var leftHandTargetPosition = new Vector3(leftHand.position.x, leftHand.position.y, leftHand.position.z); - LookAtBones(leftHandTargetPosition + avatarForward * 0.01f, leftShoulder, leftUpperArm); - LookAtBones(leftHandTargetPosition - avatarForward * 0.01f, leftUpperArm, leftLowerArm); - LookAtBones(leftHandTargetPosition, leftLowerArm, leftHand); - leftHand.rotation = leftHandDefaultRotation; - - var rightShoulder = animator.GetBoneTransform(HumanBodyBones.RightShoulder); - var rightUpperArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm); - var rightLowerArm = animator.GetBoneTransform(HumanBodyBones.RightLowerArm); - var rightHand = animator.GetBoneTransform(HumanBodyBones.RightHand); - var rightHandDefaultRotation = rightHand.rotation; - var rightHandTargetPosition = new Vector3(rightHand.position.x, rightHand.position.y, rightHand.position.z); - LookAtBones(rightHandTargetPosition + avatarForward * 0.01f, rightShoulder, rightUpperArm); - LookAtBones(rightHandTargetPosition - avatarForward * 0.01f, rightUpperArm, rightLowerArm); - LookAtBones(rightHandTargetPosition, rightLowerArm, rightHand); - rightHand.rotation = rightHandDefaultRotation; - } - - private void LookAtBones(Vector3 lookTargetPosition, params Transform[] bones) - { - for (int i = 0; i < bones.Length - 1; i++) - { - bones[i].rotation = Quaternion.FromToRotation((bones[i].position - bones[i + 1].position).normalized, (bones[i].position - lookTargetPosition).normalized) * bones[i].rotation; - } - } - - private Vector3 fixKneeBone(Transform UpperLeg, Transform Knee, Transform Ankle) - { - var a = UpperLeg.position; - var b = Ankle.position; - var z = Mathf.Max(a.z, b.z) + 0.001f; - var x = Mathf.Lerp(a.x, b.x, 0.5f); - var offset = Knee.position - new Vector3(x, Knee.position.y, z); - Knee.position -= offset; - Ankle.position += offset; - return offset; - } - - private Vector3 fixPelvisBone(Transform Spine, Transform Pelvis) - { - if (Spine.position.z < Pelvis.position.z) - { - return Vector3.zero; - } - - var offset = new Vector3(0, 0, Pelvis.position.z - Spine.position.z + 0.1f); - Pelvis.position -= offset; - foreach (var child in Pelvis.GetComponentsInChildren(true)) - { - //child.position += offset; - } - return offset; - } - - - private void unfixKneeBone(Vector3 offset, Transform Knee, Transform Ankle) - { - //return; - Knee.position += offset; - Ankle.position -= offset; - } - - private void SetVRIK(GameObject model) - { - //膝のボーンの曲がる方向で膝の向きが決まってしまうため、強制的に膝のボーンを少し前に曲げる - var leftOffset = Vector3.zero; - var rightOffset = Vector3.zero; - if (animator != null && Settings.Current.FixKneeRotation) - { - //leftOffset = fixKneeBone(animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg), animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftFoot)); - //rightOffset = fixKneeBone(animator.GetBoneTransform(HumanBodyBones.RightUpperLeg), animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightFoot)); - //fixPelvisBone(animator.GetBoneTransform(HumanBodyBones.Spine), animator.GetBoneTransform(HumanBodyBones.Hips)); - FixLegDirection(model); - } - - if (animator != null && Settings.Current.FixElbowRotation) - { - FixArmDirection(model); - } - - vrik = model.AddComponent(); - vrik.AutoDetectReferences(); - - //親指の方向の検出に失敗すると腕の回転もおかしくなる - vrik.solver.leftArm.palmToThumbAxis = new Vector3(0, 0, 1); - vrik.solver.rightArm.palmToThumbAxis = new Vector3(0, 0, 1); - - vrik.solver.FixTransforms(); - - vrik.solver.IKPositionWeight = 0f; - vrik.solver.leftArm.stretchCurve = new AnimationCurve(); - vrik.solver.rightArm.stretchCurve = new AnimationCurve(); - vrik.UpdateSolverExternal(); - - //膝のボーンの曲がる方向で膝の向きが決まってしまうため、強制的に膝のボーンを少し前に曲げる - //if (animator != null) - //{ - // unfixKneeBone(leftOffset, animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftFoot)); - // unfixKneeBone(rightOffset, animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightFoot)); - //} - //if (animator != null) - //{ - // var leftWrist = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).gameObject; - // var rightWrist = animator.GetBoneTransform(HumanBodyBones.RightLowerArm).gameObject; - // var leftRelaxer = leftWrist.AddComponent(); - // var rightRelaxer = rightWrist.AddComponent(); - // leftRelaxer.ik = vrik; - // rightRelaxer.ik = vrik; - //} - } - - private List> GetTrackerSerialNumbers() - { - var list = new List>(); - foreach (var trackingPoint in TrackingPointManager.Instance.GetTrackingPoints()) - { - if (trackingPoint.DeviceClass == ETrackedDeviceClass.HMD) - { - list.Add(Tuple.Create("HMD", trackingPoint.Name)); - } - else if (trackingPoint.DeviceClass == ETrackedDeviceClass.Controller) - { - list.Add(Tuple.Create("コントローラー", trackingPoint.Name)); - } - else if (trackingPoint.DeviceClass == ETrackedDeviceClass.GenericTracker) - { - list.Add(Tuple.Create("トラッカー", trackingPoint.Name)); - } - else - { - list.Add(Tuple.Create("Unknown", trackingPoint.Name)); - } - } - return list; - } - - private PipeCommands.SetTrackerSerialNumbers GetCurrentTrackerSettings() - { - var deviceDictionary = new Dictionary - { - {ETrackedDeviceClass.HMD, "HMD"}, - {ETrackedDeviceClass.Controller, "コントローラー"}, - {ETrackedDeviceClass.GenericTracker, "トラッカー"}, - {ETrackedDeviceClass.TrackingReference, "ベースステーション"}, - {ETrackedDeviceClass.Invalid, "割り当てしない"}, - }; - return new PipeCommands.SetTrackerSerialNumbers - { - Head = Tuple.Create(deviceDictionary[Settings.Current.Head.Item1], Settings.Current.Head.Item2), - LeftHand = Tuple.Create(deviceDictionary[Settings.Current.LeftHand.Item1], Settings.Current.LeftHand.Item2), - RightHand = Tuple.Create(deviceDictionary[Settings.Current.RightHand.Item1], Settings.Current.RightHand.Item2), - Pelvis = Tuple.Create(deviceDictionary[Settings.Current.Pelvis.Item1], Settings.Current.Pelvis.Item2), - LeftFoot = Tuple.Create(deviceDictionary[Settings.Current.LeftFoot.Item1], Settings.Current.LeftFoot.Item2), - RightFoot = Tuple.Create(deviceDictionary[Settings.Current.RightFoot.Item1], Settings.Current.RightFoot.Item2), - LeftElbow = Tuple.Create(deviceDictionary[Settings.Current.LeftElbow.Item1], Settings.Current.LeftElbow.Item2), - RightElbow = Tuple.Create(deviceDictionary[Settings.Current.RightElbow.Item1], Settings.Current.RightElbow.Item2), - LeftKnee = Tuple.Create(deviceDictionary[Settings.Current.LeftKnee.Item1], Settings.Current.LeftKnee.Item2), - RightKnee = Tuple.Create(deviceDictionary[Settings.Current.RightKnee.Item1], Settings.Current.RightKnee.Item2), - }; - } - - private void SetTrackerSerialNumbers(PipeCommands.SetTrackerSerialNumbers data) - { - var deviceDictionary = new Dictionary - { - {"HMD", ETrackedDeviceClass.HMD }, - {"コントローラー", ETrackedDeviceClass.Controller }, - {"トラッカー", ETrackedDeviceClass.GenericTracker }, - {"ベースステーション", ETrackedDeviceClass.TrackingReference }, - {"割り当てしない", ETrackedDeviceClass.Invalid }, - }; - - Settings.Current.Head = Tuple.Create(deviceDictionary[data.Head.Item1], data.Head.Item2); - Settings.Current.LeftHand = Tuple.Create(deviceDictionary[data.LeftHand.Item1], data.LeftHand.Item2); - Settings.Current.RightHand = Tuple.Create(deviceDictionary[data.RightHand.Item1], data.RightHand.Item2); - Settings.Current.Pelvis = Tuple.Create(deviceDictionary[data.Pelvis.Item1], data.Pelvis.Item2); - Settings.Current.LeftFoot = Tuple.Create(deviceDictionary[data.LeftFoot.Item1], data.LeftFoot.Item2); - Settings.Current.RightFoot = Tuple.Create(deviceDictionary[data.RightFoot.Item1], data.RightFoot.Item2); - Settings.Current.LeftElbow = Tuple.Create(deviceDictionary[data.LeftElbow.Item1], data.LeftElbow.Item2); - Settings.Current.RightElbow = Tuple.Create(deviceDictionary[data.RightElbow.Item1], data.RightElbow.Item2); - Settings.Current.LeftKnee = Tuple.Create(deviceDictionary[data.LeftKnee.Item1], data.LeftKnee.Item2); - Settings.Current.RightKnee = Tuple.Create(deviceDictionary[data.RightKnee.Item1], data.RightKnee.Item2); - SetVRIKTargetTrackers(); - } - - private enum TargetType - { - Head, Pelvis, LeftArm, RightArm, LeftLeg, RightLeg, LeftElbow, RightElbow, LeftKnee, RightKnee - } - - private TrackingPoint GetTrackerTransformBySerialNumber(Tuple serial, TargetType setTo, Transform headTracker = null) - { - var manager = TrackingPointManager.Instance; - if (serial.Item1 == ETrackedDeviceClass.HMD) - { - if (string.IsNullOrEmpty(serial.Item2)) - { - return manager.GetTrackingPoints(ETrackedDeviceClass.HMD).FirstOrDefault(); - } - else if (manager.TryGetTrackingPoint(serial.Item2, out var hmdTrackingPoint)) - { - return hmdTrackingPoint; - } - } - else if (serial.Item1 == ETrackedDeviceClass.Controller) - { - var controllers = manager.GetTrackingPoints(ETrackedDeviceClass.Controller).Where(d => d.Name.Contains("LIV Virtual Camera") == false); - TrackingPoint ret = null; - foreach (var controller in controllers) - { - if (controller != null && controller.Name == serial.Item2) - { - if (setTo == TargetType.LeftArm || setTo == TargetType.RightArm) - { - ret = controller; - break; - } - return controller; - } - } - if (ret == null) - { - var controllerTrackingPoints = controllers.Select((d, i) => new { index = i, pos = headTracker.InverseTransformDirection(d.TargetTransform.position - headTracker.position), trackingPoint = d }) - .OrderBy(d => d.pos.x) - .Select(d => d.trackingPoint); - if (setTo == TargetType.LeftArm) ret = controllerTrackingPoints.ElementAtOrDefault(0); - if (setTo == TargetType.RightArm) ret = controllerTrackingPoints.ElementAtOrDefault(1); - } - return ret; - } - else if (serial.Item1 == ETrackedDeviceClass.GenericTracker) - { - foreach (var tracker in manager.GetTrackingPoints(ETrackedDeviceClass.GenericTracker).Where(d => d.Name.Contains("LIV Virtual Camera") == false && !(Settings.Current.VirtualMotionTrackerEnable && d.Name.Contains($"VMT_{Settings.Current.VirtualMotionTrackerNo}")))) - { - if (tracker != null && tracker.Name == serial.Item2) - { - return tracker; - } - } - if (string.IsNullOrEmpty(serial.Item2) == false) return null; //Serialあるのに見つからなかったらnull - - var trackerIds = new List(); - - if (Settings.Current.Head.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.Head.Item2); - if (Settings.Current.LeftHand.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftHand.Item2); - if (Settings.Current.RightHand.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightHand.Item2); - if (Settings.Current.Pelvis.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.Pelvis.Item2); - if (Settings.Current.LeftFoot.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftFoot.Item2); - if (Settings.Current.RightFoot.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightFoot.Item2); - if (Settings.Current.LeftElbow.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftElbow.Item2); - if (Settings.Current.RightElbow.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightElbow.Item2); - if (Settings.Current.LeftKnee.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.LeftKnee.Item2); - if (Settings.Current.RightKnee.Item1 == ETrackedDeviceClass.GenericTracker) trackerIds.Add(Settings.Current.RightKnee.Item2); - - //ここに来るときは腰か足のトラッカー自動認識になってるとき - //割り当てられていないトラッカーリスト - var autoTrackers = manager.GetTrackingPoints(ETrackedDeviceClass.GenericTracker).Where(d => trackerIds.Contains(d.Name) == false).Select((d, i) => new { index = i, pos = headTracker.InverseTransformDirection(d.TargetTransform.position - headTracker.position), trackingPoint = d }); - if (autoTrackers.Any()) - { - var count = autoTrackers.Count(); - if (count >= 3) - { - if (setTo == TargetType.Pelvis) - { //腰は一番高い位置にあるトラッカー - return autoTrackers.OrderByDescending(d => d.pos.y).Select(d => d.trackingPoint).First(); - } - } - if (count >= 2) - { - if (setTo == TargetType.LeftLeg) - { - return autoTrackers.OrderBy(d => d.pos.y).Take(2).OrderBy(d => d.pos.x).Select(d => d.trackingPoint).First(); - } - else if (setTo == TargetType.RightLeg) - { - return autoTrackers.OrderBy(d => d.pos.y).Take(2).OrderByDescending(d => d.pos.x).Select(d => d.trackingPoint).First(); - } - } - } - } - return null; - } - - private void SetVRIKTargetTrackers() - { - if (vrik == null) { return; } //まだmodelがない - - vrik.solver.spine.headTarget = GetTrackerTransformBySerialNumber(Settings.Current.Head, TargetType.Head)?.TargetTransform; - vrik.solver.spine.headClampWeight = 0.38f; - - vrik.solver.spine.pelvisTarget = GetTrackerTransformBySerialNumber(Settings.Current.Pelvis, TargetType.Pelvis, vrik.solver.spine.headTarget)?.TargetTransform; - if (vrik.solver.spine.pelvisTarget != null) - { - vrik.solver.spine.pelvisPositionWeight = 1f; - vrik.solver.spine.pelvisRotationWeight = 1f; - vrik.solver.plantFeet = false; - vrik.solver.spine.neckStiffness = 0f; - vrik.solver.spine.maxRootAngle = 180f; - } - else - { - vrik.solver.spine.pelvisPositionWeight = 0f; - vrik.solver.spine.pelvisRotationWeight = 0f; - vrik.solver.plantFeet = true; - vrik.solver.spine.neckStiffness = 1f; - vrik.solver.spine.maxRootAngle = 0f; - } - - vrik.solver.leftArm.target = GetTrackerTransformBySerialNumber(Settings.Current.LeftHand, TargetType.LeftArm, vrik.solver.spine.headTarget)?.TargetTransform; - if (vrik.solver.leftArm.target != null) - { - vrik.solver.leftArm.positionWeight = 1f; - vrik.solver.leftArm.rotationWeight = 1f; - } - else - { - vrik.solver.leftArm.positionWeight = 0f; - vrik.solver.leftArm.rotationWeight = 0f; - } - - vrik.solver.rightArm.target = GetTrackerTransformBySerialNumber(Settings.Current.RightHand, TargetType.RightArm, vrik.solver.spine.headTarget)?.TargetTransform; - if (vrik.solver.rightArm.target != null) - { - vrik.solver.rightArm.positionWeight = 1f; - vrik.solver.rightArm.rotationWeight = 1f; - } - else - { - vrik.solver.rightArm.positionWeight = 0f; - vrik.solver.rightArm.rotationWeight = 0f; - } - - vrik.solver.leftLeg.target = GetTrackerTransformBySerialNumber(Settings.Current.LeftFoot, TargetType.LeftLeg, vrik.solver.spine.headTarget)?.TargetTransform; - if (vrik.solver.leftLeg.target != null) - { - vrik.solver.leftLeg.positionWeight = 1f; - vrik.solver.leftLeg.rotationWeight = 1f; - } - else - { - vrik.solver.leftLeg.positionWeight = 0f; - vrik.solver.leftLeg.rotationWeight = 0f; - } - - vrik.solver.rightLeg.target = GetTrackerTransformBySerialNumber(Settings.Current.RightFoot, TargetType.RightLeg, vrik.solver.spine.headTarget)?.TargetTransform; - if (vrik.solver.rightLeg.target != null) - { - vrik.solver.rightLeg.positionWeight = 1f; - vrik.solver.rightLeg.rotationWeight = 1f; - } - else - { - vrik.solver.rightLeg.positionWeight = 0f; - vrik.solver.rightLeg.rotationWeight = 0f; - } - } - - private Transform leftHandFreeOffsetRotation; - private Transform rightHandFreeOffsetRotation; - private Transform leftHandFreeOffsetPosition; - private Transform rightHandFreeOffsetPosition; - - public IEnumerator Calibrate(PipeCommands.CalibrateType calibrateType) - { - lastCalibrateType = calibrateType;//最後に実施したキャリブレーションタイプとして記録 - - animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(0, 0, 0); - var lefthand = animator.GetBoneTransform(HumanBodyBones.LeftHand); lefthand.localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(0, 0, 0); - - SetVRIK(CurrentModel); - wristRotationFix.SetVRIK(vrik); - - //animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(LeftHandAngle, 0, 0); - //animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(RightHandAngle, 0, 0); - - //var leftLowerArm = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm); - //var leftRelaxer = leftLowerArm.gameObject.AddComponent(); - //leftRelaxer.ik = vrik; - //leftRelaxer.twistSolvers = new TwistSolver[] { new TwistSolver { transform = leftLowerArm } }; - //var rightLowerArm = animator.GetBoneTransform(HumanBodyBones.RightLowerArm); - //var rightRelaxer = rightLowerArm.gameObject.AddComponent(); - //rightRelaxer.ik = vrik; - //rightRelaxer.twistSolvers = new TwistSolver[] { new TwistSolver { transform = rightLowerArm } }; - - var headTracker = GetTrackerTransformBySerialNumber(Settings.Current.Head, TargetType.Head); - var leftHandTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftHand, TargetType.LeftArm, headTracker?.TargetTransform); - var rightHandTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightHand, TargetType.RightArm, headTracker?.TargetTransform); - var bodyTracker = GetTrackerTransformBySerialNumber(Settings.Current.Pelvis, TargetType.Pelvis, headTracker?.TargetTransform); - var leftFootTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftFoot, TargetType.LeftLeg, headTracker?.TargetTransform); - var rightFootTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightFoot, TargetType.RightLeg, headTracker?.TargetTransform); - var leftElbowTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftElbow, TargetType.LeftElbow, headTracker?.TargetTransform); - var rightElbowTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightElbow, TargetType.RightElbow, headTracker?.TargetTransform); - var leftKneeTracker = GetTrackerTransformBySerialNumber(Settings.Current.LeftKnee, TargetType.LeftKnee, headTracker?.TargetTransform); - var rightKneeTracker = GetTrackerTransformBySerialNumber(Settings.Current.RightKnee, TargetType.RightKnee, headTracker?.TargetTransform); - - ClearChildren(headTracker, leftHandTracker, rightHandTracker, bodyTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker); - - var settings = new RootMotion.FinalIK.VRIKCalibrator.Settings(); - - yield return new WaitForEndOfFrame(); - - var leftHandOffset = Vector3.zero; - var rightHandOffset = Vector3.zero; - - //トラッカー - //xをプラス方向に動かすとトラッカーの左(LEDを上に見たとき)に進む - //yをプラス方向に動かすとトラッカーの上(LED方向)に進む - //zをマイナス方向に動かすとトラッカーの底面に向かって進む - - if (Settings.Current.LeftHand.Item1 == ETrackedDeviceClass.GenericTracker) - { - //角度補正(左手なら右のトラッカーに向けた)後 - //xを+方向は体の正面に向かって進む - //yを+方向は体の上(天井方向)に向かって進む - //zを+方向は体中心(左手なら右手の方向)に向かって進む - leftHandOffset = new Vector3(1.0f, Settings.Current.LeftHandTrackerOffsetToBottom, Settings.Current.LeftHandTrackerOffsetToBodySide); // Vector3 (IsEnable, ToTrackerBottom, ToBodySide) - } - if (Settings.Current.RightHand.Item1 == ETrackedDeviceClass.GenericTracker) - { - //角度補正(左手なら右のトラッカーに向けた)後 - //xを-方向は体の正面に向かって進む - //yを+方向は体の上(天井方向)に向かって進む - //zを+方向は体中心(左手なら右手の方向)に向かって進む - rightHandOffset = new Vector3(1.0f, Settings.Current.RightHandTrackerOffsetToBottom, Settings.Current.RightHandTrackerOffsetToBodySide); // Vector3 (IsEnable, ToTrackerBottom, ToBodySide) - } - - TrackingPointManager.Instance.ClearTrackingWatcher(); - - if (calibrateType == PipeCommands.CalibrateType.Default) - { - yield return FinalIKCalibrator.CalibrateIpose(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker); - } - else if (calibrateType == PipeCommands.CalibrateType.FixedHand) - { - yield return Calibrator.CalibrateFixedHand(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, leftHandOffset, rightHandOffset, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker); - } - else if (calibrateType == PipeCommands.CalibrateType.FixedHandWithGround) - { - yield return Calibrator.CalibrateFixedHandWithGround(HandTrackerRoot, PelvisTrackerRoot, vrik, settings, leftHandOffset, rightHandOffset, headTracker, bodyTracker, leftHandTracker, rightHandTracker, leftFootTracker, rightFootTracker, leftElbowTracker, rightElbowTracker, leftKneeTracker, rightKneeTracker); - } - - vrik.solver.IKPositionWeight = 1.0f; - if (leftFootTracker == null && rightFootTracker == null) - { - vrik.solver.plantFeet = true; - vrik.solver.locomotion.weight = 1.0f; - var rootController = vrik.references.root.GetComponent(); - if (rootController != null) GameObject.Destroy(rootController); - } - - vrik.solver.locomotion.footDistance = 0.06f; - vrik.solver.locomotion.stepThreshold = 0.2f; - vrik.solver.locomotion.angleThreshold = 45f; - vrik.solver.locomotion.maxVelocity = 0.04f; - vrik.solver.locomotion.velocityFactor = 0.04f; - vrik.solver.locomotion.rootSpeed = 40; - vrik.solver.locomotion.stepSpeed = 2; - - Settings.Current.headTracker = StoreTransform.Create(headTracker?.TargetTransform); - Settings.Current.bodyTracker = StoreTransform.Create(bodyTracker?.TargetTransform); - Settings.Current.leftHandTracker = StoreTransform.Create(leftHandTracker?.TargetTransform); - Settings.Current.rightHandTracker = StoreTransform.Create(rightHandTracker?.TargetTransform); - Settings.Current.leftFootTracker = StoreTransform.Create(leftFootTracker?.TargetTransform); - Settings.Current.rightFootTracker = StoreTransform.Create(rightFootTracker?.TargetTransform); - Settings.Current.leftElbowTracker = StoreTransform.Create(leftElbowTracker?.TargetTransform); - Settings.Current.rightElbowTracker = StoreTransform.Create(rightElbowTracker?.TargetTransform); - Settings.Current.leftKneeTracker = StoreTransform.Create(leftKneeTracker?.TargetTransform); - Settings.Current.rightKneeTracker = StoreTransform.Create(rightKneeTracker?.TargetTransform); - - - var calibratedLeftHandTransform = leftHandTracker.TargetTransform?.GetChild(0); - var calibratedRightHandTransform = rightHandTracker.TargetTransform?.GetChild(0); - - leftHandFreeOffsetRotation = new GameObject(nameof(leftHandFreeOffsetRotation)).transform; - rightHandFreeOffsetRotation = new GameObject(nameof(rightHandFreeOffsetRotation)).transform; - leftHandFreeOffsetRotation.SetParent(leftHandTracker?.TargetTransform); - rightHandFreeOffsetRotation.SetParent(rightHandTracker?.TargetTransform); - leftHandFreeOffsetRotation.localPosition = Vector3.zero; - leftHandFreeOffsetRotation.localRotation = Quaternion.identity; - leftHandFreeOffsetRotation.localScale = Vector3.one; - rightHandFreeOffsetRotation.localPosition = Vector3.zero; - rightHandFreeOffsetRotation.localRotation = Quaternion.identity; - rightHandFreeOffsetRotation.localScale = Vector3.one; - - leftHandFreeOffsetPosition = new GameObject(nameof(leftHandFreeOffsetPosition)).transform; - rightHandFreeOffsetPosition = new GameObject(nameof(rightHandFreeOffsetPosition)).transform; - leftHandFreeOffsetPosition.SetParent(leftHandFreeOffsetRotation); - rightHandFreeOffsetPosition.SetParent(rightHandFreeOffsetRotation); - leftHandFreeOffsetPosition.localPosition = Vector3.zero; - leftHandFreeOffsetPosition.localRotation = Quaternion.identity; - leftHandFreeOffsetPosition.localScale = Vector3.one; - rightHandFreeOffsetPosition.localPosition = Vector3.zero; - rightHandFreeOffsetPosition.localRotation = Quaternion.identity; - rightHandFreeOffsetPosition.localScale = Vector3.one; - - calibratedLeftHandTransform.parent = leftHandFreeOffsetPosition; - calibratedRightHandTransform.parent = rightHandFreeOffsetPosition; - - SetHandFreeOffset(); - - calibrationState = CalibrationState.Calibrating; //キャリブレーション状態を"キャリブレーション中"に設定(ここまで来なければ失敗している) - } - - private void ClearChildren(params TrackingPoint[] Parents) => ClearChildren(Parents.Select(d => d?.TargetTransform).ToArray()); - - private void ClearChildren(params Transform[] Parents) - { - foreach (var parent in Parents) - { - if (parent != null) - { - foreach (Transform child in parent) - { - Destroy(child.gameObject); - } - } - } - } - - public void EndCalibrate() - { - //トラッカー位置の非表示 - TrackingPointManager.Instance.SetTrackingPointPositionVisible(false); - - if (CalibrationCamera != null) - { - CalibrationCamera.gameObject.SetActive(false); - } - SetHandFreeOffset(); - //SetTrackersToVRIK(); - - //直前がキャリブレーション実行中なら - if (calibrationState == CalibrationState.Calibrating) - { - calibrationState = CalibrationState.Calibrated; //キャリブレーション状態を"キャリブレーション完了"に設定 - } - else - { - //キャンセルされたなど - calibrationState = CalibrationState.Uncalibrated; //キャリブレーション状態を"未キャリブレーション"に設定 - - //IKを初期化 - animator.GetBoneTransform(HumanBodyBones.LeftLowerArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightLowerArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightUpperArm).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(0, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(0, 0, 0); - - SetVRIK(CurrentModel); - wristRotationFix.SetVRIK(vrik); - - animator.GetBoneTransform(HumanBodyBones.LeftHand).localEulerAngles = new Vector3(LeftHandAngle, 0, 0); - animator.GetBoneTransform(HumanBodyBones.RightHand).localEulerAngles = new Vector3(RightHandAngle, 0, 0); - } - } - - #endregion - - #region LipSync - - private void SetLipSyncEnable(bool enable) - { - LipSync.EnableLipSync = enable; - Settings.Current.LipSyncEnable = enable; - } - - private string[] GetLipSyncDevices() - { - return LipSync.GetMicrophoneDevices(); - } - - private void SetLipSyncDevice(string device) - { - LipSync.SetMicrophoneDevice(device); - Settings.Current.LipSyncDevice = device; - } - - private void SetLipSyncGain(float gain) - { - if (gain < 1.0f) gain = 1.0f; - if (gain > 256.0f) gain = 256.0f; - LipSync.Gain = gain; - Settings.Current.LipSyncGain = gain; - } - - private void SetLipSyncMaxWeightEnable(bool enable) - { - LipSync.MaxWeightEnable = enable; - Settings.Current.LipSyncMaxWeightEnable = enable; - } - - private void SetLipSyncWeightThreashold(float threashold) - { - LipSync.WeightThreashold = threashold; - Settings.Current.LipSyncWeightThreashold = threashold; - } - - private void SetLipSyncMaxWeightEmphasis(bool enable) - { - LipSync.MaxWeightEmphasis = enable; - Settings.Current.LipSyncMaxWeightEmphasis = enable; - } - - #endregion - - #region Color - - private void ChangeBackgroundColor(float r, float g, float b, bool isCustom) - { - BackgroundRenderer.material.color = new Color(r, g, b, 1.0f); - Settings.Current.BackgroundColor = BackgroundRenderer.material.color; - if (isCustom) Settings.Current.CustomBackgroundColor = BackgroundRenderer.material.color; - Settings.Current.IsTransparent = false; - SetDwmTransparent(false); - } - - private void SetBackgroundTransparent() - { - Settings.Current.IsTransparent = true; -#if !UNITY_EDITOR // エディタ上では動きません。 - BackgroundRenderer.material.color = new Color(0.0f, 0.0f, 0.0f, 0.0f); - SetDwmTransparent(true); -#endif - } - - private bool lastHideWindowBorder = false; - void HideWindowBorder(bool enable) - { - if (lastHideWindowBorder == enable) return; - lastHideWindowBorder = enable; - Settings.Current.HideBorder = enable; -#if !UNITY_EDITOR // エディタ上では動きません。 - var hwnd = GetUnityWindowHandle(); - //var hwnd = GetActiveWindow(); - if (enable) - { - var clientrect = GetUnityWindowClientPosition(); - SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE); //ウインドウ枠の削除 - SetUnityWindowSize(clientrect.right - clientrect.left, clientrect.bottom - clientrect.top); - } - else - { - var windowrect = GetUnityWindowPosition(); - SetWindowLong(hwnd, GWL_STYLE, defaultWindowStyle); - Screen.SetResolution(windowrect.right - windowrect.left, windowrect.bottom - windowrect.top, false); - } -#endif - } - void SetWindowTopMost(bool enable) - { - Settings.Current.IsTopMost = enable; -#if !UNITY_EDITOR // エディタ上では動きません。 - SetUnityWindowTopMost(enable); -#endif - } - - void SetWindowClickThrough(bool enable) - { - Settings.Current.WindowClickThrough = enable; -#if !UNITY_EDITOR // エディタ上では動きません。 - var hwnd = GetUnityWindowHandle(); - //var hwnd = GetActiveWindow(); - if (enable) - { - SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT); //クリックを透過する - } - else - { - SetWindowLong(hwnd, GWL_EXSTYLE, defaultExWindowStyle); - } -#endif - } - - void OnRenderImage(RenderTexture from, RenderTexture to) - { - Graphics.Blit(from, to, BackgroundRenderer.material); - } - - #endregion - - #region CameraControl - - - - private void SetGridVisible(bool enable) - { - GridCanvas?.SetActive(enable); - Settings.Current.ShowCameraGrid = enable; - } - - #endregion - - #region BlinkControl - void SetAutoBlinkEnable(bool enable) - { - faceController.EnableBlink = enable; - Settings.Current.AutoBlinkEnable = enable; - } - void SetBlinkTimeMin(float time) - { - faceController.BlinkTimeMin = time; - Settings.Current.BlinkTimeMin = time; - } - void SetBlinkTimeMax(float time) - { - faceController.BlinkTimeMax = time; - Settings.Current.BlinkTimeMax = time; - } - void SetCloseAnimationTime(float time) - { - faceController.CloseAnimationTime = time; - Settings.Current.CloseAnimationTime = time; - } - void SetOpenAnimationTime(float time) - { - faceController.OpenAnimationTime = time; - Settings.Current.OpenAnimationTime = time; - } - void SetClosingTime(float time) - { - faceController.ClosingTime = time; - Settings.Current.ClosingTime = time; - } - - private Dictionary BlendShapeNameDictionary = new Dictionary - { - { "通常(NEUTRAL)", BlendShapePreset.Neutral }, - { "喜(JOY)", BlendShapePreset.Joy }, - { "怒(ANGRY)", BlendShapePreset.Angry }, - { "哀(SORROW)", BlendShapePreset.Sorrow }, - { "楽(FUN)", BlendShapePreset.Fun }, - { "上見(LOOKUP)", BlendShapePreset.LookUp }, - { "下見(LOOKDOWN)", BlendShapePreset.LookDown }, - { "左見(LOOKLEFT)", BlendShapePreset.LookLeft }, - { "右見(LOOKRIGHT)", BlendShapePreset.LookRight }, - }; - - void SetDefaultFace(string face) - { - faceController.StopBlink = false; - if (string.IsNullOrEmpty(face)) - { - } - else if (BlendShapeNameDictionary.ContainsKey(face)) - { - faceController.DefaultFace = BlendShapeNameDictionary[face]; - faceController.FacePresetName = null; - } - else - { - faceController.DefaultFace = BlendShapePreset.Unknown; - faceController.FacePresetName = face; - } - } - #endregion - - #region HandFaceControll - - - - public void DoKeyAction(KeyAction action) - { - if (action.HandAction) - { - handController.SetHandAngle(action.Hand == Hands.Left || action.Hand == Hands.Both, action.Hand == Hands.Right || action.Hand == Hands.Both, action.HandAngles, action.HandChangeTime); - } - else if (action.FaceAction) - { - foreach (var externalMotionReceiver in externalMotionReceivers) - { - externalMotionReceiver.DisableBlendShapeReception = action.DisableBlendShapeReception; - } - LipSync.MaxLevel = action.LipSyncMaxLevel; - faceController.SetFace(action.FaceNames, action.FaceStrength, action.StopBlink); - } - else if (action.FunctionAction) - { - switch (action.Function) - { - case Functions.ShowControlPanel: - ExecuteControlPanel(); - break; - case Functions.ColorGreen: - ChangeBackgroundColor(0.0f, 1.0f, 0.0f, false); - break; - case Functions.ColorBlue: - ChangeBackgroundColor(0.0f, 0.0f, 1.0f, false); - break; - case Functions.ColorWhite: - ChangeBackgroundColor(0.9375f, 0.9375f, 0.9375f, false); - break; - case Functions.ColorCustom: - ChangeBackgroundColor(Settings.Current.CustomBackgroundColor.r, Settings.Current.CustomBackgroundColor.g, Settings.Current.CustomBackgroundColor.b, true); - break; - case Functions.ColorTransparent: - SetBackgroundTransparent(); - break; - case Functions.FrontCamera: - CameraManager.Current.ChangeCamera(CameraTypes.Front); - break; - case Functions.BackCamera: - CameraManager.Current.ChangeCamera(CameraTypes.Back); - break; - case Functions.FreeCamera: - CameraManager.Current.ChangeCamera(CameraTypes.Free); - break; - case Functions.PositionFixedCamera: - CameraManager.Current.ChangeCamera(CameraTypes.PositionFixed); - break; - case Functions.PauseTracking: - DeviceInfo.pauseTracking = !DeviceInfo.pauseTracking; - break; - case Functions.ShowCalibrationWindow: - server?.SendCommandAsync(new PipeCommands.ShowCalibrationWindow { }); - break; - case Functions.ShowPhotoWindow: - server?.SendCommandAsync(new PipeCommands.ShowPhotoWindow { }); - break; - } - } - } - - - #endregion - - #region EyeTracking - - - private void SetEyeTracking_TobiiOffsets(PipeCommands.SetEyeTracking_TobiiOffsets offsets) - { - Settings.Current.EyeTracking_TobiiOffsetHorizontal = offsets.OffsetHorizontal; - Settings.Current.EyeTracking_TobiiOffsetVertical = offsets.OffsetVertical; - Settings.Current.EyeTracking_TobiiScaleHorizontal = offsets.ScaleHorizontal; - Settings.Current.EyeTracking_TobiiScaleVertical = offsets.ScaleVertical; - SetEyeTracking_TobiiOffsetsAction?.Invoke(offsets); - } - - public void SetEyeTracking_TobiiPosition(Transform position, float centerX, float centerY) - { - Settings.Current.EyeTracking_TobiiPosition = StoreTransform.Create(position); - Settings.Current.EyeTracking_TobiiCenterX = centerX; - Settings.Current.EyeTracking_TobiiCenterY = centerY; - } - - public Vector2 GetEyeTracking_TobiiLocalPosition(Transform saveto) - { - if (Settings.Current.EyeTracking_TobiiPosition != null) Settings.Current.EyeTracking_TobiiPosition.ToLocalTransform(saveto); - return new Vector2(Settings.Current.EyeTracking_TobiiCenterX, Settings.Current.EyeTracking_TobiiCenterY); - } - private void SetEyeTracking_ViveProEyeOffsets(PipeCommands.SetEyeTracking_ViveProEyeOffsets offsets) - { - Settings.Current.EyeTracking_ViveProEyeOffsetHorizontal = offsets.OffsetHorizontal; - Settings.Current.EyeTracking_ViveProEyeOffsetVertical = offsets.OffsetVertical; - Settings.Current.EyeTracking_ViveProEyeScaleHorizontal = offsets.ScaleHorizontal; - Settings.Current.EyeTracking_ViveProEyeScaleVertical = offsets.ScaleVertical; - SetEyeTracking_ViveProEyeOffsetsAction?.Invoke(offsets); - } - private void SetEyeTracking_ViveProEyeUseEyelidMovements(PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements useEyelidMovements) - { - Settings.Current.EyeTracking_ViveProEyeUseEyelidMovements = useEyelidMovements.Use; - SetEyeTracking_ViveProEyeUseEyelidMovementsAction?.Invoke(useEyelidMovements); - } - - - #endregion - - #region ExternalMotionSender - - private void SetExternalMotionSenderEnable(bool enable) - { - if (IsPreRelease == false) return; - Settings.Current.ExternalMotionSenderEnable = enable; - ExternalMotionSenderObject.SetActive(enable); - } - - private void SetExternalMotionReceiverEnable(bool enable, int index) - { - Settings.Current.ExternalMotionReceiverEnableList[index] = enable; - externalMotionReceivers[index].SetObjectActive(enable); - } - - private void SetExternalBonesReceiverEnable(bool enable) - { - Settings.Current.ExternalBonesReceiverEnable = enable; - foreach (var externalMotionReceiver in externalMotionReceivers) - { - externalMotionReceiver.receiveBonesFlag = enable; - } - } - - private void ChangeExternalMotionSenderAddress(string address, int port, int pstatus, int proot, int pbone, int pblendshape, int pcamera, int pdevices, string optionstring, bool responderEnable) - { - Settings.Current.ExternalMotionSenderAddress = address; - Settings.Current.ExternalMotionSenderPort = port; - Settings.Current.ExternalMotionSenderPeriodStatus = pstatus; - Settings.Current.ExternalMotionSenderPeriodRoot = proot; - Settings.Current.ExternalMotionSenderPeriodBone = pbone; - Settings.Current.ExternalMotionSenderPeriodBlendShape = pblendshape; - Settings.Current.ExternalMotionSenderPeriodCamera = pcamera; - Settings.Current.ExternalMotionSenderPeriodDevices = pdevices; - Settings.Current.ExternalMotionSenderOptionString = optionstring; - Settings.Current.ExternalMotionSenderResponderEnable = responderEnable; - - externalMotionSender.periodStatus = pstatus; - externalMotionSender.periodRoot = proot; - externalMotionSender.periodBone = pbone; - externalMotionSender.periodBlendShape = pblendshape; - externalMotionSender.periodCamera = pcamera; - externalMotionSender.periodDevices = pdevices; - externalMotionSender.ChangeOSCAddress(address, port); - externalMotionSender.optionString = optionstring; - easyDeviceDiscoveryProtocolManager.responderEnable = responderEnable; - } - - public void ChangeExternalMotionSenderAddress(string address, int port) - { - Settings.Current.ExternalMotionSenderAddress = address; - Settings.Current.ExternalMotionSenderPort = port; - - externalMotionSender.ChangeOSCAddress(address, port); - } - - private void ChangeExternalMotionReceiverPort(int[] ports, bool requesterEnable) - { - Settings.Current.ExternalMotionReceiverPortList = ports.ToList(); - for (int index = 0; index < externalMotionReceivers.Length; index++) - { - externalMotionReceivers[index].ChangeOSCPort(ports[index]); - } - - Settings.Current.ExternalMotionReceiverRequesterEnable = requesterEnable; - easyDeviceDiscoveryProtocolManager.requesterEnable = requesterEnable; - } - - private void WaitOneFrameAction(Action action) - { - StartCoroutine(WaitOneFrameCoroutine(action)); - } - - private IEnumerator WaitOneFrameCoroutine(Action action) - { - yield return null; - action?.Invoke(); - } - - #endregion - - private void SetTrackingFilterEnable(bool global, bool hmd, bool controller, bool tracker) - { - DeviceInfo.globalEnable = global; - DeviceInfo.hmdEnable = hmd; - DeviceInfo.controllerEnable = controller; - DeviceInfo.trackerEnable = tracker; - Settings.Current.TrackingFilterEnable = global; - Settings.Current.TrackingFilterHmdEnable = hmd; - Settings.Current.TrackingFilterControllerEnable = controller; - Settings.Current.TrackingFilterTrackerEnable = tracker; - } - - private void SetModelModifierEnable(bool fixKneeRotation, bool fixElbowRotation) - { - Settings.Current.FixKneeRotation = fixKneeRotation; - Settings.Current.FixElbowRotation = fixElbowRotation; - } - - private void SetHandleControllerAsTracker(bool handleCasT) - { - Settings.Current.HandleControllerAsTracker = handleCasT; - } - - #region Setting - - - [Serializable] - public class CommonSettings - { - public string LoadSettingFilePathOnStart = ""; //起動時に読み込む設定ファイルパス - - //初期値 - [OnDeserializing()] - internal void OnDeserializingMethod(StreamingContext context) - { - LoadSettingFilePathOnStart = ""; - } - } - - public static CommonSettings CurrentCommonSettings = new CommonSettings(); - - //共通設定の書き込み - private void SaveCommonSettings() - { - string path = Path.GetFullPath(Application.dataPath + "/../Settings/common.json"); - var directoryName = Path.GetDirectoryName(path); - if (Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); - File.WriteAllText(path, Json.Serializer.ToReadable(Json.Serializer.Serialize(CurrentCommonSettings))); - } - - //共通設定の読み込み - public void LoadCommonSettings() - { - string path = Path.GetFullPath(Application.dataPath + "/../Settings/common.json"); - if (!File.Exists(path)) - { - return; - } - CurrentCommonSettings = Json.Serializer.Deserialize(File.ReadAllText(path)); //設定を読み込み - } - - private NotifyLogTypes notifyLogLevel = NotifyLogTypes.Warning; - private async void LogMessageHandler(string cond, string trace, LogType type) - { - NotifyLogTypes notifyType = NotifyLogTypes.Log; - switch (type) - { - case LogType.Assert: notifyType = NotifyLogTypes.Assert; CriticalErrorCount++; break; - case LogType.Error: notifyType = NotifyLogTypes.Error; CriticalErrorCount++; break; - case LogType.Exception: notifyType = NotifyLogTypes.Exception; CriticalErrorCount++; break; - case LogType.Log: notifyType = NotifyLogTypes.Log; break; - case LogType.Warning: notifyType = NotifyLogTypes.Warning; break; - default: notifyType = NotifyLogTypes.Log; break; - } - - if (notifyLogLevel < notifyType) - { - return; //Logはうるさいので飛ばさない - } - - //あまりにも致命的エラーが多すぎる場合は強制終了する - if (CriticalErrorCount > 10000) - { -#if UNITY_EDITOR - UnityEditor.EditorApplication.isPlaying = false; -#else - Application.Quit(); -#endif - Debug.Log("CriticalErrorCount over"); - } - - await server.SendCommandAsync(new PipeCommands.LogNotify - { - condition = cond, - stackTrace = trace, - type = notifyType, - errorCount = CriticalErrorCount, - }); - } - - private bool IsRegisteredEventCallBack = false; - private void RegisterEventCallBack() - { - if (IsRegisteredEventCallBack == false) - { - IsRegisteredEventCallBack = true; - TrackingPointManager.Instance.TrackerMovedEvent += TransformExtensions_TrackerMovedEvent; - ExternalReceiverForVMC.StatusStringUpdated += StatusStringUpdatedEvent; - } - } - - private void SaveSettings(string path) - { - if (string.IsNullOrEmpty(path)) - { - return; - } - - Settings.Current.AAA_SavedVersion = baseVersionString; - - File.WriteAllText(path, Json.Serializer.ToReadable(Json.Serializer.Serialize(Settings.Current))); - - //ファイルが正常に書き込めたので、現在共通設定に記録されているパスと違う場合、共通設定に書き込む - if (CurrentCommonSettings.LoadSettingFilePathOnStart != path) - { - CurrentCommonSettings.LoadSettingFilePathOnStart = path; - SaveCommonSettings(); - Debug.Log("Save last loaded file of " + path); - } - } - - //設定の読み込み - public void LoadSettings(string path = null) - { - //設定パスがnull or 存在しないなら、default読み込み - //パスが渡されていれば2回目以降の読み込み - if (string.IsNullOrEmpty(path) || (!File.Exists(path))) - { - //共通設定を読み込み - LoadCommonSettings(); - - //初回読み込みファイルが存在しなければdefault.jsonを - if (string.IsNullOrEmpty(CurrentCommonSettings.LoadSettingFilePathOnStart) || (!File.Exists(CurrentCommonSettings.LoadSettingFilePathOnStart))) - { - path = Application.dataPath + "/../default.json"; - Debug.Log("Load default.json"); - } - else - { - //存在すればそのPathを読みに行こうとする - path = CurrentCommonSettings.LoadSettingFilePathOnStart; - Debug.Log("Load last loaded file of " + path); - } - } - - //設定の読み込みを試みる - try - { - path = Path.GetFullPath(path); //フルパスに変換 - Settings.Current = Json.Serializer.Deserialize(File.ReadAllText(path)); //設定を読み込み - float divide = 0; - //腰情報を読み込む - if (float.TryParse(File.ReadAllText(Application.dataPath + "/../PelvisTrackerOffsetDivide.txt"), out divide)) - { - Calibrator.pelvisOffsetDivide = divide;//腰オフセット分割数を記録 - } - } - catch (Exception ex) - { - //読み込めなかったときはエラーをファイルとして出力 - File.WriteAllText(Application.dataPath + "/../exception.txt", ex.ToString() + ":" + ex.Message); - Debug.LogError(ex.ToString() + ":" + ex.Message); - } - - Debug.Log("Loaded config: " + path); - - //スケールを元に戻す - ResetTrackerScale(); - //設定を適用する - ApplySettings(); - - //有効なJSONが取得できたかチェック - if (Settings.Current != null) - { - lastLoadedConfigPath = path; //パスを記録 - - //ファイルが正常に存在したので、現在共通設定に記録されているパスと違う場合、共通設定に書き込む - if (CurrentCommonSettings.LoadSettingFilePathOnStart != path) - { - CurrentCommonSettings.LoadSettingFilePathOnStart = path; - SaveCommonSettings(); - Debug.Log("Save last loaded file of " + path); - } - } - - //設定の変更を通知 - VMCEvents.OnLoadedConfigPathChanged?.Invoke(path); - } - - private void ResetTrackerScale() - { - //jsonが正しくデコードできていなければ無視する - if (Settings.Current == null) - { - return; - } - - //トラッカーのルートスケールを初期値に戻す - HandTrackerRoot.localScale = new Vector3(1.0f, 1.0f, 1.0f); - PelvisTrackerRoot.localScale = new Vector3(1.0f, 1.0f, 1.0f); - HandTrackerRoot.position = Vector3.zero; - PelvisTrackerRoot.position = Vector3.zero; - - //スケール変更時の位置オフセット設定 - var handTrackerOffset = HandTrackerRoot.GetComponent(); - var footTrackerOffset = PelvisTrackerRoot.GetComponent(); - handTrackerOffset.ResetTargetAndPosition(); - footTrackerOffset.ResetTargetAndPosition(); - } - - //Settings.Currentを各種設定に適用 - private async void ApplySettings() - { - //VRMのパスが有効で、存在するなら読み込む - if (string.IsNullOrWhiteSpace(Settings.Current.VRMPath) == false - && File.Exists(Settings.Current.VRMPath)) - { - await server.SendCommandAsync(new PipeCommands.LoadVRMPath { Path = Settings.Current.VRMPath }); - await ImportVRM(Settings.Current.VRMPath, false, Settings.Current.EnableNormalMapFix, Settings.Current.DeleteHairNormalMap); - - //メタ情報をOSC送信する - VRMmetaLodedAction?.Invoke(LoadVRM(Settings.Current.VRMPath)); - } - - //SetResolutionは強制的にウインドウ枠を復活させるのでBorder設定の前にやっておく必要がある - if (Screen.resolutions.Any(d => d.width == Settings.Current.ScreenWidth && d.height == Settings.Current.ScreenHeight && d.refreshRate == Settings.Current.ScreenRefreshRate)) - { - UpdateActionQueue.Enqueue(() => Screen.SetResolution(Settings.Current.ScreenWidth, Settings.Current.ScreenHeight, false, Settings.Current.ScreenRefreshRate)); - } - - if (Settings.Current.BackgroundColor != null) - { - UpdateActionQueue.Enqueue(() => ChangeBackgroundColor(Settings.Current.BackgroundColor.r, Settings.Current.BackgroundColor.g, Settings.Current.BackgroundColor.b, false)); - } - - if (Settings.Current.CustomBackgroundColor != null) - { - await server.SendCommandAsync(new PipeCommands.LoadCustomBackgroundColor { r = Settings.Current.CustomBackgroundColor.r, g = Settings.Current.CustomBackgroundColor.g, b = Settings.Current.CustomBackgroundColor.b }); - } - - if (Settings.Current.IsTransparent) - { - UpdateActionQueue.Enqueue(() => SetBackgroundTransparent()); - } - - UpdateActionQueue.Enqueue(() => HideWindowBorder(Settings.Current.HideBorder)); - await server.SendCommandAsync(new PipeCommands.LoadHideBorder { enable = Settings.Current.HideBorder }); - - UpdateActionQueue.Enqueue(() => SetWindowTopMost(Settings.Current.IsTopMost)); - await server.SendCommandAsync(new PipeCommands.LoadIsTopMost { enable = Settings.Current.IsTopMost }); - - await server.SendCommandAsync(new PipeCommands.LoadCameraFOV { fov = Settings.Current.CameraFOV }); - await server.SendCommandAsync(new PipeCommands.LoadCameraSmooth { speed = Settings.Current.CameraSmooth }); - - SetGridVisible(Settings.Current.ShowCameraGrid); - await server.SendCommandAsync(new PipeCommands.LoadShowCameraGrid { enable = Settings.Current.ShowCameraGrid }); - await server.SendCommandAsync(new PipeCommands.LoadCameraMirror { enable = Settings.Current.CameraMirrorEnable }); - SetWindowClickThrough(Settings.Current.WindowClickThrough); - await server.SendCommandAsync(new PipeCommands.LoadSetWindowClickThrough { enable = Settings.Current.WindowClickThrough }); - SetLipSyncDevice(Settings.Current.LipSyncDevice); - await server.SendCommandAsync(new PipeCommands.LoadLipSyncDevice { device = Settings.Current.LipSyncDevice }); - SetLipSyncGain(Settings.Current.LipSyncGain); - await server.SendCommandAsync(new PipeCommands.LoadLipSyncGain { gain = Settings.Current.LipSyncGain }); - SetLipSyncMaxWeightEnable(Settings.Current.LipSyncMaxWeightEnable); - await server.SendCommandAsync(new PipeCommands.LoadLipSyncMaxWeightEnable { enable = Settings.Current.LipSyncMaxWeightEnable }); - SetLipSyncWeightThreashold(Settings.Current.LipSyncWeightThreashold); - await server.SendCommandAsync(new PipeCommands.LoadLipSyncWeightThreashold { threashold = Settings.Current.LipSyncWeightThreashold }); - SetLipSyncMaxWeightEmphasis(Settings.Current.LipSyncMaxWeightEmphasis); - await server.SendCommandAsync(new PipeCommands.LoadLipSyncMaxWeightEmphasis { enable = Settings.Current.LipSyncMaxWeightEmphasis }); - - SetAutoBlinkEnable(Settings.Current.AutoBlinkEnable); - await server.SendCommandAsync(new PipeCommands.LoadAutoBlinkEnable { enable = Settings.Current.AutoBlinkEnable }); - SetBlinkTimeMin(Settings.Current.BlinkTimeMin); - await server.SendCommandAsync(new PipeCommands.LoadBlinkTimeMin { time = Settings.Current.BlinkTimeMin }); - SetBlinkTimeMax(Settings.Current.BlinkTimeMax); - await server.SendCommandAsync(new PipeCommands.LoadBlinkTimeMax { time = Settings.Current.BlinkTimeMax }); - SetCloseAnimationTime(Settings.Current.CloseAnimationTime); - await server.SendCommandAsync(new PipeCommands.LoadCloseAnimationTime { time = Settings.Current.CloseAnimationTime }); - SetOpenAnimationTime(Settings.Current.OpenAnimationTime); - await server.SendCommandAsync(new PipeCommands.LoadOpenAnimationTime { time = Settings.Current.OpenAnimationTime }); - SetClosingTime(Settings.Current.ClosingTime); - await server.SendCommandAsync(new PipeCommands.LoadClosingTime { time = Settings.Current.ClosingTime }); - SetDefaultFace(Settings.Current.DefaultFace); - await server.SendCommandAsync(new PipeCommands.LoadDefaultFace { face = Settings.Current.DefaultFace }); - - await server.SendCommandAsync(new PipeCommands.LoadControllerTouchPadPoints - { - IsOculus = Settings.Current.IsOculus, - LeftPoints = Settings.Current.LeftTouchPadPoints, - LeftCenterEnable = Settings.Current.LeftCenterEnable, - RightPoints = Settings.Current.RightTouchPadPoints, - RightCenterEnable = Settings.Current.RightCenterEnable - }); - await server.SendCommandAsync(new PipeCommands.LoadControllerStickPoints - { - LeftPoints = Settings.Current.LeftThumbStickPoints, - RightPoints = Settings.Current.RightThumbStickPoints, - }); - - KeyAction.KeyActionsUpgrade(Settings.Current.KeyActions); - - if (string.IsNullOrWhiteSpace(Settings.Current.AAA_SavedVersion)) - { - //before 0.47 _SaveVersion is null. - - //v0.48 BlendShapeKey case sensitive. - foreach (var keyAction in Settings.Current.KeyActions) - { - if (keyAction.FaceNames != null && keyAction.FaceNames.Count > 0) - { - keyAction.FaceNames = keyAction.FaceNames.Select(d => faceController.GetCaseSensitiveKeyName(d)).ToList(); - } - } - } - - SteamVR2Input.EnableSkeletal = Settings.Current.EnableSkeletal; - - await server.SendCommandAsync(new PipeCommands.LoadSkeletalInputEnable { enable = Settings.Current.EnableSkeletal }); - - await server.SendCommandAsync(new PipeCommands.LoadKeyActions { KeyActions = Settings.Current.KeyActions }); - await server.SendCommandAsync(new PipeCommands.SetHandFreeOffset - { - LeftHandPositionX = (int)Mathf.Round(Settings.Current.LeftHandPositionX * 1000), - LeftHandPositionY = (int)Mathf.Round(Settings.Current.LeftHandPositionY * 1000), - LeftHandPositionZ = (int)Mathf.Round(Settings.Current.LeftHandPositionZ * 1000), - LeftHandRotationX = (int)Settings.Current.LeftHandRotationX, - LeftHandRotationY = (int)Settings.Current.LeftHandRotationY, - LeftHandRotationZ = (int)Settings.Current.LeftHandRotationZ, - RightHandPositionX = (int)Mathf.Round(Settings.Current.RightHandPositionX * 1000), - RightHandPositionY = (int)Mathf.Round(Settings.Current.RightHandPositionY * 1000), - RightHandPositionZ = (int)Mathf.Round(Settings.Current.RightHandPositionZ * 1000), - RightHandRotationX = (int)Settings.Current.RightHandRotationX, - RightHandRotationY = (int)Settings.Current.RightHandRotationY, - RightHandRotationZ = (int)Settings.Current.RightHandRotationZ, - SwivelOffset = Settings.Current.SwivelOffset, - }); - SetHandFreeOffset(); - - await server.SendCommandAsync(new PipeCommands.LoadLipSyncEnable { enable = Settings.Current.LipSyncEnable }); - SetLipSyncEnable(Settings.Current.LipSyncEnable); - - await server.SendCommandAsync(new PipeCommands.SetLightAngle { X = Settings.Current.LightRotationX, Y = Settings.Current.LightRotationY }); - SetLightAngle(Settings.Current.LightRotationX, Settings.Current.LightRotationY); - await server.SendCommandAsync(new PipeCommands.ChangeLightColor { a = Settings.Current.LightColor.a, r = Settings.Current.LightColor.r, g = Settings.Current.LightColor.g, b = Settings.Current.LightColor.b }); - ChangeLightColor(Settings.Current.LightColor.a, Settings.Current.LightColor.r, Settings.Current.LightColor.g, Settings.Current.LightColor.b); - - SetExternalMotionSenderEnable(Settings.Current.ExternalMotionSenderEnable); - ChangeExternalMotionSenderAddress(Settings.Current.ExternalMotionSenderAddress, Settings.Current.ExternalMotionSenderPort, Settings.Current.ExternalMotionSenderPeriodStatus, Settings.Current.ExternalMotionSenderPeriodRoot, Settings.Current.ExternalMotionSenderPeriodBone, Settings.Current.ExternalMotionSenderPeriodBlendShape, Settings.Current.ExternalMotionSenderPeriodCamera, Settings.Current.ExternalMotionSenderPeriodDevices, Settings.Current.ExternalMotionSenderOptionString, Settings.Current.ExternalMotionSenderResponderEnable); - - if (Settings.Current.ExternalMotionReceiverPortList == null) Settings.Current.ExternalMotionReceiverPortList = new List() { Settings.Current.ExternalMotionReceiverPort, Settings.Current.ExternalMotionReceiverPort + 1 }; - ChangeExternalMotionReceiverPort(Settings.Current.ExternalMotionReceiverPortList.ToArray(), Settings.Current.ExternalMotionReceiverRequesterEnable); - if (Settings.Current.ExternalMotionReceiverEnableList == null) Settings.Current.ExternalMotionReceiverEnableList = new List() { Settings.Current.ExternalMotionReceiverEnable, false }; - for (int index = 0; index < Settings.Current.ExternalMotionReceiverEnableList.Count; index++) - { - SetExternalMotionReceiverEnable(Settings.Current.ExternalMotionReceiverEnableList[index], index); - } - - SetMidiCCBlendShape(Settings.Current.MidiCCBlendShape); - SetMidiEnable(Settings.Current.MidiEnable); - - SetEyeTracking_TobiiOffsetsAction?.Invoke(new PipeCommands.SetEyeTracking_TobiiOffsets - { - OffsetHorizontal = Settings.Current.EyeTracking_TobiiOffsetHorizontal, - OffsetVertical = Settings.Current.EyeTracking_TobiiOffsetVertical, - ScaleHorizontal = Settings.Current.EyeTracking_TobiiScaleHorizontal, - ScaleVertical = Settings.Current.EyeTracking_TobiiScaleVertical - }); - - SetEyeTracking_ViveProEyeOffsetsAction?.Invoke(new PipeCommands.SetEyeTracking_ViveProEyeOffsets - { - OffsetHorizontal = Settings.Current.EyeTracking_ViveProEyeOffsetHorizontal, - OffsetVertical = Settings.Current.EyeTracking_ViveProEyeOffsetVertical, - ScaleHorizontal = Settings.Current.EyeTracking_ViveProEyeScaleHorizontal, - ScaleVertical = Settings.Current.EyeTracking_ViveProEyeScaleVertical - }); - - SetEyeTracking_ViveProEyeUseEyelidMovementsAction?.Invoke(new PipeCommands.SetEyeTracking_ViveProEyeUseEyelidMovements - { - Use = Settings.Current.EyeTracking_ViveProEyeUseEyelidMovements - }); - SetEyeTracking_ViveProEyeEnable(Settings.Current.EyeTracking_ViveProEyeEnable); - - SetTrackingFilterEnable(Settings.Current.TrackingFilterEnable, Settings.Current.TrackingFilterHmdEnable, Settings.Current.TrackingFilterControllerEnable, Settings.Current.TrackingFilterTrackerEnable); - - SetModelModifierEnable(Settings.Current.FixKneeRotation, Settings.Current.FixElbowRotation); - SetHandleControllerAsTracker(Settings.Current.HandleControllerAsTracker); - SetQualitySettings(new PipeCommands.SetQualitySettings - { - antiAliasing = Settings.Current.AntiAliasing, - }); - SetVMT(Settings.Current.VirtualMotionTrackerEnable, Settings.Current.VirtualMotionTrackerNo); - - SetLipShapeToBlendShapeStringMapAction?.Invoke(Settings.Current.LipShapesToBlendShapeMap); - SetLipTracking_ViveEnable(Settings.Current.LipTracking_ViveEnable); - - SetExternalBonesReceiverEnable(Settings.Current.ExternalBonesReceiverEnable); - - LoadAdvancedGraphicsOption(); - - AdditionalSettingAction?.Invoke(null); - - await server.SendCommandAsync(new PipeCommands.SetWindowNum { Num = CurrentWindowNum }); - } - - private void SetEyeTracking_ViveProEyeEnable(bool enable) - { - if (EyeTracking_ViveProEyeComponent != null) EyeTracking_ViveProEyeComponent.enabled = enable; - if (SRanipal_Eye_FrameworkComponent != null) SRanipal_Eye_FrameworkComponent.enabled = enable; - } - - private void SetLipTracking_ViveEnable(bool enable) - { - if (LipTracking_ViveComponent != null) LipTracking_ViveComponent.enabled = enable; - if (SRanipal_Lip_FrameworkComponent != null) SRanipal_Lip_FrameworkComponent.enabled = enable; - } - - #endregion - - private void SetMidiCCBlendShape(List blendshapes) - { - Settings.Current.MidiCCBlendShape = blendshapes; - midiCCBlendShape.KnobToBlendShape = blendshapes.ToArray(); - } - - private void SetMidiEnable(bool enable) - { - Settings.Current.MidiEnable = enable; - InputManager.Current.midiCCWrapper.gameObject.SetActive(enable); - } - - private void SetHandFreeOffset() - { - if (vrik == null) return; - if (leftHandFreeOffsetRotation == null) return; - if (rightHandFreeOffsetRotation == null) return; - if (leftHandFreeOffsetPosition == null) return; - if (rightHandFreeOffsetPosition == null) return; - - // Beat Saber compatible - - leftHandFreeOffsetRotation.localRotation = Quaternion.Euler( - Settings.Current.LeftHandRotationX, - -Settings.Current.LeftHandRotationY, - Settings.Current.LeftHandRotationZ - ); - leftHandFreeOffsetPosition.localPosition = new Vector3( - -Settings.Current.LeftHandPositionX, - Settings.Current.LeftHandPositionY, - Settings.Current.LeftHandPositionZ - ); - - rightHandFreeOffsetRotation.localRotation = Quaternion.Euler( - Settings.Current.RightHandRotationX, - Settings.Current.RightHandRotationY, - Settings.Current.RightHandRotationZ - ); - rightHandFreeOffsetPosition.localPosition = new Vector3( - Settings.Current.RightHandPositionX, - Settings.Current.RightHandPositionY, - Settings.Current.RightHandPositionZ - ); - - vrik.solver.leftArm.swivelOffset = Settings.Current.SwivelOffset; - vrik.solver.rightArm.swivelOffset = -Settings.Current.SwivelOffset; - } - - private ConcurrentQueue UpdateActionQueue = new ConcurrentQueue(); - - // Update is called once per frame - void Update() - { - KeyboardAction.Update(); - - //if (Input.GetKeyDown(KeyCode.P)) - //{ - // TakePhoto(16000, true); - //} - - Action action; - if (UpdateActionQueue.TryDequeue(out action)) action(); - } - - private int WindowX; - private int WindowY; - private Vector2 OldMousePos; - private bool isWindowDragging = false; - - void LateUpdate() - { - //Windowの移動操作 - //ドラッグ開始 - if (Input.GetMouseButtonDown((int)MouseButtons.Left) && Input.GetKey(KeyCode.LeftAlt) == false && Input.GetKey(KeyCode.RightAlt) == false) - { - var r = GetUnityWindowPosition(); - WindowX = r.left; - WindowY = r.top; - OldMousePos = GetWindowsMousePosition(); - isWindowDragging = true; - } - - //ドラッグ中 - if (Input.GetMouseButton((int)MouseButtons.Left) && isWindowDragging) - { - Vector2 pos = GetWindowsMousePosition(); - if (pos != OldMousePos) - { - WindowX += (int)(pos.x - OldMousePos.x); - WindowY += (int)(pos.y - OldMousePos.y); - SetUnityWindowPosition(WindowX, WindowY); - OldMousePos = pos; - } - } - - if (Input.GetMouseButtonUp((int)MouseButtons.Left) && isWindowDragging) - { - isWindowDragging = false; - } - } - } -} \ No newline at end of file +using sh_akira; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; +using UnityMemoryMappedFile; +using VMCMod; +using static VMC.NativeMethods; +using UniGLTF; +using UniVRM10; + +#if UNITY_EDITOR // エディタ上でしか動きません。 +using UnityEditor; +#endif + +namespace VMC +{ + public class ControlWPFWindow : MonoBehaviour + { + public bool IsBeta = false; + public bool IsPreRelease = false; + + public string VersionString; + private string baseVersionString; + + public Transform LeftWristTransform = null; + public Transform RightWristTransform = null; + + public Renderer BackgroundRenderer; + + public GameObject GridCanvas; + + public DynamicOVRLipSync LipSync; + + public FaceController faceController; + + public GameObject ExternalMotionSenderObject; + private ExternalSender externalMotionSender; + private MotionPlayer motionPlayer; + private MotionRecorder motionRecorder; + + public GameObject ExternalMotionReceiverObject; + public List externalMotionReceivers = new List(); + public MidiCCWrapper midiCCWrapper; + + public MemoryMappedFileServer server; + private string pipeName = Guid.NewGuid().ToString(); + + private GameObject CurrentModel = null; + + private int CurrentWindowNum = 1; + + public int CriticalErrorCount = 0; + public bool IsCriticalErrorCountOver = false; + + public VMTClient vmtClient; + + public PostProcessingManager postProcessingManager; + + private uint defaultWindowStyle; + private uint defaultExWindowStyle; + + private System.Threading.SynchronizationContext context = null; + + public Action AdditionalSettingAction = null; + public Action VRMmetaLoadedAction = null; + public Action VRMRemoteLoadedAction = null; + + public MIDICCBlendShape midiCCBlendShape; + + public string lastLoadedConfigPath = ""; + + public EasyDeviceDiscoveryProtocolManager easyDeviceDiscoveryProtocolManager; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void SetDpiAwareness() + { + // From https://note.com/taqssoft/n/n69521402e39e +#if UNITY_STANDALONE_WIN + try + { + // Windows 8.1 以降に対応 + NativeMethods.SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); + } + catch + { + try + { + // 古いWindows向けフォールバック(Vista以降) + NativeMethods.SetProcessDPIAware(); + } + catch + { + Debug.LogWarning("DPI設定の適用に失敗しました。"); + } + } +#endif + } + + public ModManager modManager; + + //公式プラグイン(Plugins/配下)。ユーザーMod(Mods/配下)とは別系統 + private PluginManager pluginManager; + private PluginHost pluginHost; + + // コントロールパネル起動監視用の変数を追加 + private bool showControlPanelMessage = false; + private float controlPanelStartTime = -1f; // -1で初期化(監視無効) + private const float CONTROL_PANEL_TIMEOUT = 10f; // 10秒 + + private void Awake() + { + Application.targetFrameRate = 60; + +#if UNITY_EDITOR // エディタ上でしか動きません。 + pipeName = "VMCTest"; +#else + //Debug.unityLogger.logEnabled = false; + + bool isRunWithPipeName = false; + + var args = Environment.GetCommandLineArgs(); + if (args.Length > 1) + { + for (int i = 1; i < args.Length - 1; i++) + { + if (args[i].StartsWith("/pipeName") || args[i].StartsWith("-pipeName")) + { + // コマンドライン引数からパイプ名を取得 + pipeName = args[i + 1]; + isRunWithPipeName = true; + break; + } + } + } + + if (isRunWithPipeName == false) + { + // パイプ名をランダムに生成 + pipeName = "VMCpipe" + Guid.NewGuid().ToString(); + } +#endif + +#if !UNITY_EDITOR + //start control panel + if (isRunWithPipeName == false) + { + ExecuteControlPanel(); + // コントロールパネル起動監視開始 + controlPanelStartTime = Time.time; + } +#endif + + context = System.Threading.SynchronizationContext.Current; + + baseVersionString = VersionString.Split('f').First(); + defaultWindowStyle = GetWindowLong(GetUnityWindowHandle(), GWL_STYLE); + defaultExWindowStyle = GetWindowLong(GetUnityWindowHandle(), GWL_EXSTYLE); + + server = new MemoryMappedFileServer(); + server.ReceivedEvent += Server_Received; + server.Start(pipeName); + + externalMotionSender = ExternalMotionSenderObject.GetComponent(); + +#if !UNITY_EDITOR // エディタ上では動きません。 + var hwnd = GetUnityWindowHandle(); + SetWindowLong(hwnd, GWL_STYLE, defaultWindowStyle | WS_CLIPCHILDREN); +#endif + + //モーション再生・記録 + var motionPlayerObject = new GameObject("MotionPlayer"); + motionPlayerObject.transform.SetParent(transform, false); + motionPlayer = motionPlayerObject.AddComponent(); + var motionRecorderObject = new GameObject("MotionRecorder"); + motionRecorderObject.transform.SetParent(transform, false); + motionRecorder = motionRecorderObject.AddComponent(); + + //公式プラグインの読み込み。 + //設定の読み込み・適用より前に済ませる必要があるためここで行う + //(ユーザーModはコントロールパネル接続後にModManagerが読み込む) + var pluginManagerObject = new GameObject("PluginManager"); + pluginManagerObject.transform.SetParent(transform, false); + pluginManager = pluginManagerObject.AddComponent(); + pluginHost = new PluginHost(this, faceController); + pluginManager.LoadPlugins(pluginHost); + } + + void Start() + { + Settings.Current.BackgroundColor = BackgroundRenderer.material.color; + Settings.Current.CustomBackgroundColor = BackgroundRenderer.material.color; + + OpenVRTrackerManager.Instance.OpenVREventAction += async () => + { + await server.SendCommandAsync(new PipeCommands.OpenVRStatus { DashboardOpened = OpenVRTrackerManager.Instance.isDashboardActivated }); + }; + } + + private int SetWindowTitle() + { + int setWindowNum = 1; +#if !UNITY_EDITOR + var allWindowList = GetAllWindowHandle(); + var numlist = allWindowList.Where(p => p.Value.StartsWith(Application.productName + " ") && p.Value.EndsWith(")") && p.Value.Contains('(')).Select(t => int.Parse(t.Value.Split('(').Last().Replace(")", ""))).OrderBy(d => d); + while (numlist.Contains(setWindowNum)) + { + setWindowNum++; + } + var buildString = ""; + if (IsBeta) + { + buildString = "b" + VersionString.Split('b').Last(); + } + else if (IsPreRelease) + { + buildString = "r" + VersionString.Split('r').Last().Split('b').First(); + } + else + { + buildString = "f" + VersionString.Split('f').Last().Split('r').First(); + } + NativeMethods.SetUnityWindowTitle($"{Application.productName} {baseVersionString + buildString} ({setWindowNum})"); +#endif + return setWindowNum; + } + + private int doSendTrackerMoved = 0; + private Dictionary trackerMovedLastSendTime = new Dictionary(); + private async void TransformExtensions_TrackerMovedEvent(object sender, string e) + { + if (doSendTrackerMoved > 0) + { + if (trackerMovedLastSendTime.ContainsKey(e) == false) + { + trackerMovedLastSendTime.Add(e, DateTime.Now); + } + else if (DateTime.Now - trackerMovedLastSendTime[e] < TimeSpan.FromSeconds(1)) + { + return; + } + await server.SendCommandAsync(new PipeCommands.TrackerMoved { SerialNumber = e }); + trackerMovedLastSendTime[e] = DateTime.Now; + } + } + + private bool doStatusStringUpdated = false; + private async void StatusStringUpdatedEvent(string e) + { + if (doStatusStringUpdated) + { + await server.SendCommandAsync(new PipeCommands.StatusStringChanged { StatusString = e }); + } + } + + private bool ControlPanelExecuted = false; + private System.Diagnostics.Process controlPanelProcess = null; + private void ExecuteControlPanel() + { + if (ControlPanelExecuted == false) + { + var path = Application.dataPath + "/../ControlPanel/VirtualMotionCaptureControlPanel.exe"; + controlPanelProcess = new System.Diagnostics.Process(); + controlPanelProcess.StartInfo.FileName = path; + controlPanelProcess.StartInfo.Arguments = "/pipeName " + pipeName; + controlPanelProcess.EnableRaisingEvents = true; + controlPanelProcess.Exited += ControlPanelProcess_Exited; + controlPanelProcess.Start(); + ControlPanelExecuted = true; + } + } + + private void ControlPanelProcess_Exited(object sender, EventArgs e) + { + ControlPanelExecuted = false; + controlPanelProcess.Dispose(); + } + + private void OnApplicationQuit() + { + // アプリが終了したらコントロールパネルも終了する。 + // ここは同期呼び出しなので、相手が居ない/応答しないときに待つとアプリが固まる。 + // 終了時に待つ意味は無いので短めに打ち切る。 + server?.SendCommand(new PipeCommands.QuitApplication { }, timeoutMs: 200); + + server.ReceivedEvent -= Server_Received; + server?.Dispose(); + + Application.logMessageReceived -= LogMessageHandler; + } + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(async s => + { + if (e.CommandType == typeof(PipeCommands.SetIsBeta)) + { + var d = (PipeCommands.SetIsBeta)e.Data; + IsBeta = d.IsBeta; + IsPreRelease = d.IsPreRelease; + + //エラー情報をWPFに飛ばす + Application.logMessageReceived += LogMessageHandler; + + if (IsPreRelease) + { + modManager.ImportMods(); + } + } + else if (e.CommandType == typeof(PipeCommands.LoadVRMMeta)) + { + var d = (PipeCommands.LoadVRMMeta)e.Data; + await server.SendCommandAsync(new PipeCommands.ReturnLoadVRMMeta { Data = await LoadVRMMetaAsync(d.Path) }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.LoadRemoteVRM)) + { + var d = (PipeCommands.LoadRemoteVRM)e.Data; + VRMRemoteLoadedAction?.Invoke(d.Path); + } + else if (e.CommandType == typeof(PipeCommands.ImportVRM)) + { + var d = (PipeCommands.ImportVRM)e.Data; + var t = ImportVRM(d.Path); + + //メタ情報をOSC送信する + VRMmetaLoadedAction?.Invoke(await LoadVRMMetaAsync(d.Path)); + } + + else if (e.CommandType == typeof(PipeCommands.SetLipSyncEnable)) + { + var d = (PipeCommands.SetLipSyncEnable)e.Data; + SetLipSyncEnable(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.GetLipSyncDevices)) + { + var d = (PipeCommands.GetLipSyncDevices)e.Data; + await server.SendCommandAsync(new PipeCommands.ReturnGetLipSyncDevices { Devices = GetLipSyncDevices() }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetLipSyncDevice)) + { + var d = (PipeCommands.SetLipSyncDevice)e.Data; + SetLipSyncDevice(d.device); + } + else if (e.CommandType == typeof(PipeCommands.SetLipSyncGain)) + { + var d = (PipeCommands.SetLipSyncGain)e.Data; + SetLipSyncGain(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetLipSyncMaxWeightEnable)) + { + var d = (PipeCommands.SetLipSyncMaxWeightEnable)e.Data; + SetLipSyncMaxWeightEnable(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.SetLipSyncWeightThreashold)) + { + var d = (PipeCommands.SetLipSyncWeightThreashold)e.Data; + SetLipSyncWeightThreashold(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetLipSyncMaxWeightEmphasis)) + { + var d = (PipeCommands.SetLipSyncMaxWeightEmphasis)e.Data; + SetLipSyncMaxWeightEmphasis(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.ChangeBackgroundColor)) + { + var d = (PipeCommands.ChangeBackgroundColor)e.Data; + ChangeBackgroundColor(d.r, d.g, d.b, d.isCustom); + } + else if (e.CommandType == typeof(PipeCommands.SetBackgroundTransparent)) + { + SetBackgroundTransparent(); + } + else if (e.CommandType == typeof(PipeCommands.SetWindowBorder)) + { + var d = (PipeCommands.SetWindowBorder)e.Data; + HideWindowBorder(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.SetWindowTopMost)) + { + var d = (PipeCommands.SetWindowTopMost)e.Data; + SetWindowTopMost(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.SetWindowClickThrough)) + { + var d = (PipeCommands.SetWindowClickThrough)e.Data; + SetWindowClickThrough(d.enable); + } + + else if (e.CommandType == typeof(PipeCommands.SetGridVisible)) + { + var d = (PipeCommands.SetGridVisible)e.Data; + SetGridVisible(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.SetAutoBlinkEnable)) + { + var d = (PipeCommands.SetAutoBlinkEnable)e.Data; + SetAutoBlinkEnable(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.SetBlinkTimeMin)) + { + var d = (PipeCommands.SetBlinkTimeMin)e.Data; + SetBlinkTimeMin(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetBlinkTimeMax)) + { + var d = (PipeCommands.SetBlinkTimeMax)e.Data; + SetBlinkTimeMax(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetCloseAnimationTime)) + { + var d = (PipeCommands.SetCloseAnimationTime)e.Data; + SetCloseAnimationTime(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetOpenAnimationTime)) + { + var d = (PipeCommands.SetOpenAnimationTime)e.Data; + SetOpenAnimationTime(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetClosingTime)) + { + var d = (PipeCommands.SetClosingTime)e.Data; + SetClosingTime(d.value); + } + else if (e.CommandType == typeof(PipeCommands.SetDefaultFace)) + { + var d = (PipeCommands.SetDefaultFace)e.Data; + SetDefaultFace(d.face); + } + else if (e.CommandType == typeof(PipeCommands.LoadSettings)) + { + var d = (PipeCommands.LoadSettings)e.Data; + LoadSettings(d.Path); + //イベントを登録(何度呼び出しても1回のみ) + RegisterEventCallBack(); + + // ウィンドウ情報を反映 + SendWindowInfo(); + } + else if (e.CommandType == typeof(PipeCommands.SaveSettings)) + { + var d = (PipeCommands.SaveSettings)e.Data; + SaveSettings(d.Path); + } + else if (e.CommandType == typeof(PipeCommands.SetControllerTouchPadPoints)) + { + var d = (PipeCommands.SetControllerTouchPadPoints)e.Data; + if (d.isStick) + { + Settings.Current.LeftThumbStickPoints = d.LeftPoints; + Settings.Current.RightThumbStickPoints = d.RightPoints; + } + else + { + Settings.Current.LeftCenterEnable = d.LeftCenterEnable; + Settings.Current.RightCenterEnable = d.RightCenterEnable; + Settings.Current.LeftTouchPadPoints = d.LeftPoints; + Settings.Current.RightTouchPadPoints = d.RightPoints; + } + } + else if (e.CommandType == typeof(PipeCommands.GetFaceKeys)) + { + //保存済み設定との互換性のため、プリセット表情はVRM0.xの名称(Joy, A, Blink_L等)でUIに渡す + await server.SendCommandAsync(new PipeCommands.ReturnFaceKeys { Keys = faceController.BlendShapeClips.Select(d => VRM10CompatibleNames.GetVRM0CompatibleName(d)).Distinct().ToList() }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetFace)) + { + var d = (PipeCommands.SetFace)e.Data; + faceController.SetFace(d.Keys, d.Strength, true); + } + else if (e.CommandType == typeof(PipeCommands.ExitControlPanel)) + { + ControlPanelExecuted = false; + } + else if (e.CommandType == typeof(PipeCommands.GetResolutions)) + { + await server.SendCommandAsync(new PipeCommands.ReturnResolutions + { + List = new List>(Screen.resolutions.Select(r => (r.width, r.height)).Distinct().Select(r => new Tuple(r.width, r.height))), + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetResolution)) + { + var d = (PipeCommands.SetResolution)e.Data; + Settings.Current.ScreenWidth = d.Width; + Settings.Current.ScreenHeight = d.Height; + ResizeWindow(d.Width, d.Height); + } + else if (e.CommandType == typeof(PipeCommands.SetLightAngle)) + { + var d = (PipeCommands.SetLightAngle)e.Data; + SetLightAngle(d.X, d.Y); + } + else if (e.CommandType == typeof(PipeCommands.ChangeLightColor)) + { + var d = (PipeCommands.ChangeLightColor)e.Data; + ChangeLightColor(d.a, d.r, d.g, d.b); + } + else if (e.CommandType == typeof(PipeCommands.TrackerMovedRequest)) + { + //イベントを登録(何度呼び出しても1回のみ) + RegisterEventCallBack(); + + var d = (PipeCommands.TrackerMovedRequest)e.Data; + if (d.doSend) + { + doSendTrackerMoved++; + } + else + { + doSendTrackerMoved--; + } + } + else if (e.CommandType == typeof(PipeCommands.LoadCurrentSettings)) + { + if (isFirstTimeExecute) + { + isFirstTimeExecute = false; + // コントロールパネルが正常に起動したので監視を停止し、メッセージを非表示 + controlPanelStartTime = -1f; + showControlPanelMessage = false; + + CurrentWindowNum = SetWindowTitle(); + //起動時は初期設定ロード + LoadSettings(null); + //イベントを登録(何度呼び出しても1回のみ) + RegisterEventCallBack(); + } + else + { + //現在の設定を再適用する + ApplySettings(); + } + + // ウィンドウ情報を反映 + SendWindowInfo(); + } + else if (e.CommandType == typeof(PipeCommands.EnableExternalMotionSender)) + { + var d = (PipeCommands.EnableExternalMotionSender)e.Data; + SetExternalMotionSenderEnable(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.GetEnableExternalMotionSender)) + { + await server.SendCommandAsync(new PipeCommands.EnableExternalMotionSender + { + enable = Settings.Current.ExternalMotionSenderEnable + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.ChangeExternalMotionSenderAddress)) + { + var d = (PipeCommands.ChangeExternalMotionSenderAddress)e.Data; + ChangeExternalMotionSenderAddress(d.address, d.port, d.PeriodStatus, d.PeriodRoot, d.PeriodBone, d.PeriodBlendShape, d.PeriodCamera, d.PeriodDevices, d.OptionString, d.ResponderEnable, d.UseNormalizedBone, d.SendVRM1Expression); + + } + else if (e.CommandType == typeof(PipeCommands.GetExternalMotionSenderAddress)) + { + await server.SendCommandAsync(new PipeCommands.ChangeExternalMotionSenderAddress + { + address = Settings.Current.ExternalMotionSenderAddress, + port = Settings.Current.ExternalMotionSenderPort, + PeriodStatus = Settings.Current.ExternalMotionSenderPeriodStatus, + PeriodRoot = Settings.Current.ExternalMotionSenderPeriodRoot, + PeriodBone = Settings.Current.ExternalMotionSenderPeriodBone, + PeriodBlendShape = Settings.Current.ExternalMotionSenderPeriodBlendShape, + PeriodCamera = Settings.Current.ExternalMotionSenderPeriodCamera, + PeriodDevices = Settings.Current.ExternalMotionSenderPeriodDevices, + OptionString = Settings.Current.ExternalMotionSenderOptionString, + ResponderEnable = Settings.Current.ExternalMotionSenderResponderEnable, + UseNormalizedBone = Settings.Current.ExternalMotionSenderUseNormalizedBone, + SendVRM1Expression = Settings.Current.ExternalMotionSenderSendVRM1Expression + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetVMCProtocolReceiverSetting)) + { + var d = (PipeCommands.SetVMCProtocolReceiverSetting)e.Data; + SetVMCProtocolReceiverSetting(d); + } + else if (e.CommandType == typeof(PipeCommands.GetVMCProtocolReceiverSetting)) + { + var d = (PipeCommands.GetVMCProtocolReceiverSetting)e.Data; + if (d.Index == -1) + { + var newsetting = new VMCProtocolReceiverSettings(); + newsetting.Port = 39539; + newsetting.Name = $"Receiver {Settings.Current.VMCProtocolReceiverSettingsList.Count + 1}"; + Settings.Current.VMCProtocolReceiverSettingsList.Add(newsetting); + newsetting.Port = Settings.Current.VMCProtocolReceiverSettingsList.Max(d => d.Port) + 1; + AddVMCProtocolReceiver(newsetting); + d.Index = Settings.Current.VMCProtocolReceiverSettingsList.Count - 1; + } + await server.SendCommandAsync( + Settings.Current.VMCProtocolReceiverSettingsList[d.Index].Export(d.Index) + , e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetVMCProtocolReceiverList)) + { + var d = (PipeCommands.GetVMCProtocolReceiverList)e.Data; + await server.SendCommandAsync(new PipeCommands.SetVMCProtocolReceiverList + { + Items = Settings.Current.VMCProtocolReceiverSettingsList.Select(d => Tuple.Create(d.Enable, d.Name, d.Port)).ToList(), + } + , e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.RemoveVMCProtocolReceiver)) + { + var d = (PipeCommands.RemoveVMCProtocolReceiver)e.Data; + RemoveVMCProtocolReceiver(d.Index); + } + else if (e.CommandType == typeof(PipeCommands.VMCProtocolReceiverRecenter)) + { + var d = (PipeCommands.VMCProtocolReceiverRecenter)e.Data; + externalMotionReceivers[d.Index].Recenter(); + } + else if (e.CommandType == typeof(PipeCommands.SetVMCProtocolReceiverEnable)) + { + var d = (PipeCommands.SetVMCProtocolReceiverEnable)e.Data; + SetVMCProtocolReceiverEnable(d.Index, d.Enable); + } + else if (e.CommandType == typeof(PipeCommands.ChangeExternalMotionReceiverRequester)) + { + var d = (PipeCommands.ChangeExternalMotionReceiverRequester)e.Data; + SetExternalMotionReceiverRequester(d.Enable); + + } + else if (e.CommandType == typeof(PipeCommands.GetExternalMotionReceiverRequester)) + { + await server.SendCommandAsync(new PipeCommands.ChangeExternalMotionReceiverRequester + { + Enable = Settings.Current.ExternalMotionReceiverRequesterEnable + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetMidiCCBlendShape)) + { + var bs = Settings.Current.MidiCCBlendShape; + await server.SendCommandAsync(new PipeCommands.SetMidiCCBlendShape + { + BlendShapes = Settings.Current.MidiCCBlendShape, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetMidiCCBlendShape)) + { + var d = (PipeCommands.SetMidiCCBlendShape)e.Data; + SetMidiCCBlendShape(d.BlendShapes); + } + else if (e.CommandType == typeof(PipeCommands.GetMidiEnable)) + { + await server.SendCommandAsync(new PipeCommands.MidiEnable + { + enable = Settings.Current.MidiEnable, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.MidiEnable)) + { + var d = (PipeCommands.MidiEnable)e.Data; + SetMidiEnable(d.enable); + } + else if (e.CommandType == typeof(PipeCommands.EnableTrackingFilter)) + { + var d = (PipeCommands.EnableTrackingFilter)e.Data; + SetTrackingFilterEnable(d.globalEnable, d.hmdEnable, d.controllerEnable, d.trackerEnable); + } + else if (e.CommandType == typeof(PipeCommands.GetPauseTracking)) + { + await server.SendCommandAsync(new PipeCommands.PauseTracking + { + enable = DeviceInfo.pauseTracking + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.PauseTracking)) + { + var d = (PipeCommands.PauseTracking)e.Data; + DeviceInfo.pauseTracking = d.enable; + } + else if (e.CommandType == typeof(PipeCommands.GetEnableTrackingFilter)) + { + await server.SendCommandAsync(new PipeCommands.EnableTrackingFilter + { + globalEnable = Settings.Current.TrackingFilterEnable, + hmdEnable = Settings.Current.TrackingFilterHmdEnable, + controllerEnable = Settings.Current.TrackingFilterControllerEnable, + trackerEnable = Settings.Current.TrackingFilterTrackerEnable, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.EnableModelModifier)) + { + var d = (PipeCommands.EnableModelModifier)e.Data; + SetModelModifierEnable(d.fixKneeRotation, d.fixElbowRotation); + } + else if (e.CommandType == typeof(PipeCommands.GetEnableModelModifier)) + { + await server.SendCommandAsync(new PipeCommands.EnableModelModifier + { + fixKneeRotation = Settings.Current.FixKneeRotation, + fixElbowRotation = Settings.Current.FixElbowRotation, + }, e.RequestId); + } + //------------------------ + else if (e.CommandType == typeof(PipeCommands.GetStatusString)) + { + string statusStringBuf = ""; + //有効な場合だけ送る + if (externalMotionReceivers.Any() && externalMotionReceivers[0].isActiveAndEnabled) + { + statusStringBuf = externalMotionReceivers[0]?.statusString; + } + await server.SendCommandAsync(new PipeCommands.SetStatusString + { + StatusString = statusStringBuf, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.StatusStringChangedRequest)) + { + var d = (PipeCommands.StatusStringChangedRequest)e.Data; + doStatusStringUpdated = d.doSend; + } + else if (e.CommandType == typeof(PipeCommands.EnableHandleControllerAsTracker)) + { + var d = (PipeCommands.EnableHandleControllerAsTracker)e.Data; + SetHandleControllerAsTracker(d.HandleControllerAsTracker); + } + else if (e.CommandType == typeof(PipeCommands.GetHandleControllerAsTracker)) + { + await server.SendCommandAsync(new PipeCommands.EnableHandleControllerAsTracker + { + HandleControllerAsTracker = Settings.Current.HandleControllerAsTracker + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetLaunchSteamVROnStartup)) + { + var d = (PipeCommands.SetLaunchSteamVROnStartup)e.Data; + CommonSettings.Current.LaunchSteamVROnStartup = d.Enable; + CommonSettings.Save(); + } + else if (e.CommandType == typeof(PipeCommands.GetLaunchSteamVROnStartup)) + { + await server.SendCommandAsync(new PipeCommands.SetLaunchSteamVROnStartup + { + Enable = CommonSettings.Current.LaunchSteamVROnStartup, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.EnableTrackerReassignmentWhenChestAvailable)) + { + var d = (PipeCommands.EnableTrackerReassignmentWhenChestAvailable)e.Data; + Settings.Current.TrackerReassignmentWhenChestAvailable = d.TrackerReassignmentWhenChestAvailable; + } + else if (e.CommandType == typeof(PipeCommands.GetTrackerReassignmentWhenChestAvailable)) + { + await server.SendCommandAsync(new PipeCommands.EnableTrackerReassignmentWhenChestAvailable + { + TrackerReassignmentWhenChestAvailable = Settings.Current.TrackerReassignmentWhenChestAvailable + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetQualitySettings)) + { + await server.SendCommandAsync(new PipeCommands.SetQualitySettings + { + antiAliasing = Settings.Current.AntiAliasing, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetQualitySettings)) + { + var d = (PipeCommands.SetQualitySettings)e.Data; + SetQualitySettings(d); + } + else if (e.CommandType == typeof(PipeCommands.GetVirtualMotionTracker)) + { + await server.SendCommandAsync(new PipeCommands.SetVirtualMotionTracker + { + enable = vmtClient.GetEnable(), + no = vmtClient.GetNo() + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.SetVirtualMotionTracker)) + { + var d = (PipeCommands.SetVirtualMotionTracker)e.Data; + SetVMT(d.enable, d.no); + } + else if (e.CommandType == typeof(PipeCommands.SetupVirtualMotionTracker)) + { + var d = (PipeCommands.SetupVirtualMotionTracker)e.Data; + var ret = d.install ? await VMTServer.InstallVMT() : await VMTServer.UninstallVMT(); + await server.SendCommandAsync(new PipeCommands.ResultSetupVirtualMotionTracker + { + result = ret, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetAdvancedGraphicsOption)) + { + LoadAdvancedGraphicsOption(); + } + else if (e.CommandType == typeof(PipeCommands.SetAdvancedGraphicsOption)) + { + var d = (PipeCommands.SetAdvancedGraphicsOption)e.Data; + + Settings.Current.PPS_Enable = d.PPS_Enable; + + Settings.Current.PPS_Bloom_Enable = d.Bloom_Enable; + Settings.Current.PPS_Bloom_Intensity = d.Bloom_Intensity; + Settings.Current.PPS_Bloom_Threshold = d.Bloom_Threshold; + + Settings.Current.PPS_DoF_Enable = d.DoF_Enable; + Settings.Current.PPS_DoF_FocusDistance = d.DoF_FocusDistance; + Settings.Current.PPS_DoF_Aperture = d.DoF_Aperture; + Settings.Current.PPS_DoF_FocusLength = d.DoF_FocusLength; + Settings.Current.PPS_DoF_MaxBlurSize = d.DoF_MaxBlurSize; + + Settings.Current.PPS_CG_Enable = d.CG_Enable; + Settings.Current.PPS_CG_Temperature = d.CG_Temperature; + Settings.Current.PPS_CG_Saturation = d.CG_Saturation; + Settings.Current.PPS_CG_Contrast = d.CG_Contrast; + Settings.Current.PPS_CG_Gamma = d.CG_Gamma; + + Settings.Current.PPS_Vignette_Enable = d.Vignette_Enable; + Settings.Current.PPS_Vignette_Intensity = d.Vignette_Intensity; + Settings.Current.PPS_Vignette_Smoothness = d.Vignette_Smoothness; + Settings.Current.PPS_Vignette_Roundness = d.Vignette_Roundness; + + Settings.Current.PPS_AO_Enable = d.AO_Enable; + Settings.Current.PPS_AO_IsScalable = d.AO_IsScalable; + Settings.Current.PPS_AO_Intensity = d.AO_Intensity; + Settings.Current.PPS_AO_Thickness = d.AO_Thickness; + + Settings.Current.PPS_CA_Enable = d.CA_Enable; + Settings.Current.PPS_CA_Intensity = d.CA_Intensity; + Settings.Current.PPS_CA_FastMode = d.CA_FastMode; + + Settings.Current.PPS_Bloom_Color_a = d.Bloom_Color_a; + Settings.Current.PPS_Bloom_Color_r = d.Bloom_Color_r; + Settings.Current.PPS_Bloom_Color_g = d.Bloom_Color_g; + Settings.Current.PPS_Bloom_Color_b = d.Bloom_Color_b; + + Settings.Current.PPS_CG_ColorFilter_a = d.CG_ColorFilter_a; + Settings.Current.PPS_CG_ColorFilter_r = d.CG_ColorFilter_r; + Settings.Current.PPS_CG_ColorFilter_g = d.CG_ColorFilter_g; + Settings.Current.PPS_CG_ColorFilter_b = d.CG_ColorFilter_b; + + Settings.Current.PPS_Vignette_Color_a = d.Vignette_Color_a; + Settings.Current.PPS_Vignette_Color_r = d.Vignette_Color_r; + Settings.Current.PPS_Vignette_Color_g = d.Vignette_Color_g; + Settings.Current.PPS_Vignette_Color_b = d.Vignette_Color_b; + + Settings.Current.PPS_AO_Color_a = d.AO_Color_a; + Settings.Current.PPS_AO_Color_r = d.AO_Color_r; + Settings.Current.PPS_AO_Color_g = d.AO_Color_g; + Settings.Current.PPS_AO_Color_b = d.AO_Color_b; + + Settings.Current.TurnOffAmbientLight = d.TurnOffAmbientLight; + + SetAdvancedGraphicsOption(); + } + else if (e.CommandType == typeof(PipeCommands.GetModIsLoaded)) + { + await server.SendCommandAsync(new PipeCommands.ReturnModIsLoaded + { + IsLoaded = modManager.IsModLoaded, + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_CheckAvailable)) + { + //VRoid SDKが組み込まれているか(VMC_VROIDSDK)を常時コンパイルされる本ハンドラで返す。 + //SDK未同梱時はVRoidSDKConnector自体が除外されるため、可否判定はここで行う。 + await server.SendCommandAsync(new PipeCommands.VRoidSDK_ReturnAvailable + { +#if VMC_VROIDSDK + Available = true, +#else + Available = false, +#endif + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetPluginList)) + { + await server.SendCommandAsync(new PipeCommands.ReturnPluginList + { + PluginList = GetPluginList(), + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.GetModList)) + { + await server.SendCommandAsync(new PipeCommands.ReturnModList + { + ModList = GetModList(), + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.ModSettingEvent)) + { + var d = (PipeCommands.ModSettingEvent)e.Data; + modManager.InvokeSetting(d.InstanceId); + } + else if (e.CommandType == typeof(PipeCommands.SetLogNotifyLevel)) + { + var d = (PipeCommands.SetLogNotifyLevel)e.Data; + notifyLogLevel = d.type; + } + + else if (e.CommandType == typeof(PipeCommands.Alive)) + { + await server.SendCommandAsync(new PipeCommands.Alive { }); + } + else if (e.CommandType == typeof(PipeCommands.GetUnityChildWindowEnable)) + { + await server.SendCommandAsync(new PipeCommands.SetUnityChildWindowEnable + { + enable = Settings.Current.UnityChildWindowEnable + }, e.RequestId); + SendWindowInfo(); + } + else if (e.CommandType == typeof(PipeCommands.SetUnityChildWindowEnable)) + { + var d = (PipeCommands.SetUnityChildWindowEnable)e.Data; + Settings.Current.UnityChildWindowEnable = d.enable; + SendWindowInfo(); + } + + }, null); + } + + private List GetModList() + { + var modList = new List(); + + foreach (var attribute in modManager.GetModsList()) + { + var item = new ModItem + { + Name = attribute.Name, + Version = attribute.Version, + Author = attribute.Author, + AuthorURL = attribute.AuthorURL, + Description = attribute.Description, + PluginURL = attribute.PluginURL, + InstanceId = attribute.InstanceId, + AssemblyPath = attribute.AssemblyPath, + }; + modList.Add(item); + } + + return modList; + } + + private List GetPluginList() + { + var pluginList = new List(); + + if (pluginManager == null) return pluginList; + + foreach (var plugin in pluginManager.LoadedPlugins) + { + pluginList.Add(new PluginItem + { + Id = plugin.Id, + Name = plugin.DisplayName, + Version = plugin.Version, + AssemblyPath = plugin.AssemblyPath, + }); + } + + return pluginList; + } + + public Transform MainDirectionalLightTransform; + public Light MainDirectionalLight; + + private void SetLightAngle(float x, float y) + { + if (MainDirectionalLightTransform != null) + { + MainDirectionalLightTransform.eulerAngles = new Vector3(x, y, MainDirectionalLightTransform.eulerAngles.z); + Settings.Current.LightRotationX = x; + Settings.Current.LightRotationY = y; + + VMCEvents.OnLightChanged?.Invoke(); + } + } + + private void ChangeLightColor(float a, float r, float g, float b) + { + if (MainDirectionalLight != null) + { + Settings.Current.LightColor = new Color(r, g, b, a); + MainDirectionalLight.color = Settings.Current.LightColor; + + VMCEvents.OnLightChanged?.Invoke(); + } + } + + private void SetQualitySettings(PipeCommands.SetQualitySettings setting) + { + Settings.Current.AntiAliasing = setting.antiAliasing; + QualitySettings.antiAliasing = setting.antiAliasing; + } + + private void SetVMT(bool enable, int no) + { + vmtClient.SetNo(no); + vmtClient.SetEnable(enable); + vmtClient.SendRoomMatrixTemporary(); + + Settings.Current.VirtualMotionTrackerNo = no; + Settings.Current.VirtualMotionTrackerEnable = enable; + } + + private async void LoadAdvancedGraphicsOption() + { + SetAdvancedGraphicsOption(); + await server.SendCommandAsync(new PipeCommands.SetAdvancedGraphicsOption + { + PPS_Enable = Settings.Current.PPS_Enable, + + Bloom_Enable = Settings.Current.PPS_Bloom_Enable, + Bloom_Intensity = Settings.Current.PPS_Bloom_Intensity, + Bloom_Threshold = Settings.Current.PPS_Bloom_Threshold, + + DoF_Enable = Settings.Current.PPS_DoF_Enable, + DoF_FocusDistance = Settings.Current.PPS_DoF_FocusDistance, + DoF_Aperture = Settings.Current.PPS_DoF_Aperture, + DoF_FocusLength = Settings.Current.PPS_DoF_FocusLength, + DoF_MaxBlurSize = Settings.Current.PPS_DoF_MaxBlurSize, + + CG_Enable = Settings.Current.PPS_CG_Enable, + CG_Temperature = Settings.Current.PPS_CG_Temperature, + CG_Saturation = Settings.Current.PPS_CG_Saturation, + CG_Contrast = Settings.Current.PPS_CG_Contrast, + CG_Gamma = Settings.Current.PPS_CG_Gamma, + + Vignette_Enable = Settings.Current.PPS_Vignette_Enable, + Vignette_Intensity = Settings.Current.PPS_Vignette_Intensity, + Vignette_Smoothness = Settings.Current.PPS_Vignette_Smoothness, + Vignette_Roundness = Settings.Current.PPS_Vignette_Roundness, + + AO_Enable = Settings.Current.PPS_AO_Enable, + AO_IsScalable = Settings.Current.PPS_AO_IsScalable, + AO_Intensity = Settings.Current.PPS_AO_Intensity, + AO_Thickness = Settings.Current.PPS_AO_Thickness, + + CA_Enable = Settings.Current.PPS_CA_Enable, + CA_Intensity = Settings.Current.PPS_CA_Intensity, + CA_FastMode = Settings.Current.PPS_CA_FastMode, + + Bloom_Color_a = Settings.Current.PPS_Bloom_Color_a, + Bloom_Color_r = Settings.Current.PPS_Bloom_Color_r, + Bloom_Color_g = Settings.Current.PPS_Bloom_Color_g, + Bloom_Color_b = Settings.Current.PPS_Bloom_Color_b, + + CG_ColorFilter_a = Settings.Current.PPS_CG_ColorFilter_a, + CG_ColorFilter_r = Settings.Current.PPS_CG_ColorFilter_r, + CG_ColorFilter_g = Settings.Current.PPS_CG_ColorFilter_g, + CG_ColorFilter_b = Settings.Current.PPS_CG_ColorFilter_b, + + Vignette_Color_a = Settings.Current.PPS_Vignette_Color_a, + Vignette_Color_r = Settings.Current.PPS_Vignette_Color_r, + Vignette_Color_g = Settings.Current.PPS_Vignette_Color_g, + Vignette_Color_b = Settings.Current.PPS_Vignette_Color_b, + + AO_Color_a = Settings.Current.PPS_AO_Color_a, + AO_Color_r = Settings.Current.PPS_AO_Color_r, + AO_Color_g = Settings.Current.PPS_AO_Color_g, + AO_Color_b = Settings.Current.PPS_AO_Color_b, + + TurnOffAmbientLight = Settings.Current.TurnOffAmbientLight + }); + } + + private void SetAdvancedGraphicsOption() + { + postProcessingManager.Apply(Settings.Current); + } + + private bool isFirstTimeExecute = true; + + #region VRM + + public async Task LoadVRMMetaAsync(string path) + { + if (string.IsNullOrEmpty(path) || File.Exists(path) == false) + { + return null; + } + + var vrmdata = new UnityMemoryMappedFile.VRMData(); + vrmdata.FilePath = path; + + IAwaitCaller awaitCaller = Application.isPlaying ? new RuntimeOnlyAwaitCaller() : new ImmediateCaller(); + + using var data = await awaitCaller.Run(() => { return new AutoGltfFileParser(path).Parse(); }); + if (data == null) + return null; + + var vrm10Data = Vrm10Data.Parse(data); + + MigrationData migration = null; + GltfData migratedData = null; + if (vrm10Data == null) + { + migratedData = await awaitCaller.Run(() => Vrm10Data.Migrate(data, out vrm10Data, out migration)); + } + + try + { + if (vrm10Data == null) + return null; + + if (migration != null) + { + // VRM 0.x (マイグレーション前のオリジナルのメタ情報を表示する) + vrmdata.MetaVersion = 0; + vrmdata.Title = migration.OriginalMetaBeforeMigration.title; + vrmdata.Version = migration.OriginalMetaBeforeMigration.version; + vrmdata.Author = migration.OriginalMetaBeforeMigration.author; + vrmdata.ContactInformation = migration.OriginalMetaBeforeMigration.contactInformation; + vrmdata.Reference = migration.OriginalMetaBeforeMigration.reference; + vrmdata.AllowedUser = (UnityMemoryMappedFile.AllowedUser)migration.OriginalMetaBeforeMigration.allowedUser; + vrmdata.ViolentUssage = migration.OriginalMetaBeforeMigration.violentUsage ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.SexualUssage = migration.OriginalMetaBeforeMigration.sexualUsage ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.CommercialUssage = migration.OriginalMetaBeforeMigration.commercialUsage ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.OtherPermissionUrl = migration.OriginalMetaBeforeMigration.otherPermissionUrl; + vrmdata.LicenseType = (UnityMemoryMappedFile.LicenseType)migration.OriginalMetaBeforeMigration.licenseType; + vrmdata.OtherLicenseUrl = migration.OriginalMetaBeforeMigration.otherLicenseUrl; + } + else + { + // VRM 1.0 + var meta = vrm10Data.VrmExtension.Meta; + if (meta == null) + return null; + vrmdata.MetaVersion = 1; + vrmdata.Title = meta.Name; + vrmdata.Version = meta.Version; + vrmdata.Author = meta.Authors != null ? string.Join(", ", meta.Authors) : null; + vrmdata.ContactInformation = meta.ContactInformation; + vrmdata.Reference = meta.References != null ? string.Join(", ", meta.References) : null; + + // Permission (AvatarPermissionTypeはAllowedUserと3値とも同順) + vrmdata.AllowedUser = (UnityMemoryMappedFile.AllowedUser)meta.AvatarPermission; + vrmdata.ViolentUssage = meta.AllowExcessivelyViolentUsage == true ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.SexualUssage = meta.AllowExcessivelySexualUsage == true ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + // 旧バージョンのコントロールパネル向けの近似値(personalNonProfit以外は商用利用可扱い) + vrmdata.CommercialUssage = meta.CommercialUsage == UniGLTF.Extensions.VRMC_vrm.CommercialUsageType.personalNonProfit ? UnityMemoryMappedFile.UssageLicense.Disallow : UnityMemoryMappedFile.UssageLicense.Allow; + vrmdata.CommercialUsageType = (int)meta.CommercialUsage; + vrmdata.PoliticalOrReligiousUsage = meta.AllowPoliticalOrReligiousUsage == true ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.AntisocialOrHateUsage = meta.AllowAntisocialOrHateUsage == true ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + + // Distribution License + vrmdata.CreditNotation = (int)meta.CreditNotation; + vrmdata.Redistribution = meta.AllowRedistribution == true ? UnityMemoryMappedFile.UssageLicense.Allow : UnityMemoryMappedFile.UssageLicense.Disallow; + vrmdata.ModificationType = (int)meta.Modification; + vrmdata.CopyrightInformation = meta.CopyrightInformation; + vrmdata.ThirdPartyLicenses = meta.ThirdPartyLicenses; + vrmdata.LicenseUrl = meta.LicenseUrl; + vrmdata.OtherLicenseUrl = meta.OtherLicenseUrl; + } + + using var loader = new Vrm10Importer(vrm10Data); + var thumbnail = await loader.LoadVrmThumbnailAsync(awaitCaller); + if (thumbnail != null) + { + vrmdata.ThumbnailPNGBytes = EncodeTextureToPNG(thumbnail); + } + } + finally + { + migratedData?.Dispose(); + } + + return vrmdata; + } + + /// + /// テクスチャをPNGバイト列に変換する。 + /// UniVRM 0.131系のサムネイルはnon-readableで生成されEncodeToPNGが失敗するため、 + /// 一度RenderTexture経由で読み取り可能なコピーを作ってからエンコードする。 + /// + private static byte[] EncodeTextureToPNG(Texture2D source) + { + if (source == null) return null; + if (source.isReadable) + { + return source.EncodeToPNG(); + } + + var previousActive = RenderTexture.active; + var rt = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); + Texture2D readable = null; + try + { + Graphics.Blit(source, rt); + RenderTexture.active = rt; + readable = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false); + readable.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); + readable.Apply(); + return readable.EncodeToPNG(); + } + finally + { + RenderTexture.active = previousActive; + RenderTexture.ReleaseTemporary(rt); + if (readable != null) Destroy(readable); + } + } + + /// + /// 現在のモデルのオリジナル(非正規化)ボーンと正規化ボーンの変換器。 + /// VMCProtocolの送受信でのみ使用する(内部処理は正規化ボーンで統一)。 + /// + public BonePostureConverter BonePostureConverter { get; private set; } + + /// 読み込み中のVRMファイルのハッシュ(/VMC/Ext/VRM の第3引数) + public string CurrentVRMHash { get; private set; } + + /// VRMファイル内容のSHA-256(16進小文字) + private static string ComputeHash(byte[] bytes) + { + try + { + using (var sha = System.Security.Cryptography.SHA256.Create()) + { + var hash = sha.ComputeHash(bytes); + var builder = new System.Text.StringBuilder(hash.Length * 2); + foreach (var b in hash) builder.Append(b.ToString("x2")); + return builder.ToString(); + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to compute VRM hash: {ex.Message}"); + return ""; + } + } + + public async Task ImportVRM(string path) + { + await server.SendCommandAsync(new PipeCommands.VRMLoadStatus { Valid = false }); + + Settings.Current.VRMPath = path; + + Vrm10Instance vrm10Instance = null; + try + { + IAwaitCaller awaitCaller = Application.isPlaying ? new RuntimeOnlyAwaitCaller() : new ImmediateCaller(); + + //ファイルの読み込みは1回だけにして、ハッシュ計算とパースの両方に使い回す。 + //(Vrm10.LoadPathAsync は内部で File.ReadAllBytes するので、 + // ハッシュのために別途読むと同じファイルを2回読むことになる) + var bytes = await awaitCaller.Run(() => File.ReadAllBytes(path)); + + //モデルの同一性判定用ハッシュ(/VMC/Ext/VRM の第3引数) + CurrentVRMHash = await awaitCaller.Run(() => ComputeHash(bytes)); + + // ControlRigGenerationOption.Generate: AnimatorはVRM0.x互換の正規化ボーン(Control Rig)にマップされるため、 + // FinalIKやVMCProtocolのボーン送信は非正規化のVRM1.0モデルでも従来通り動作する + vrm10Instance = await Vrm10.LoadBytesAsync(bytes, + canLoadVrm0X: true, + controlRigGenerationOption: ControlRigGenerationOption.Generate, + showMeshes: false, + awaitCaller: awaitCaller); + } + catch (Exception ex) + { + Debug.LogError($"Failed to load VRM: {path}\n{ex}"); + } + + if (vrm10Instance == null) + { + return; + } + + // BlendShape(Expression)目線制御時の表情とのぶつかり防止は、UniVRM10のRuntimeが + // Expressionの適用と目線(LookAt)の合成・Override設定を一括処理するため追加対応不要 + + //VMCProtocolはオリジナル(非正規化)ボーン姿勢での送受信が推奨されているため、 + //正規化(ControlRig)との変換に必要なレスト回転をここで記録する。 + //VRIK等がボーンを動かす前(=Tポーズ)でなければ正しい値が取れないので、LoadNewModelより先に行う。 + BonePostureConverter = BonePostureConverter.Capture(vrm10Instance); + + var runtimeGltfInstance = vrm10Instance.GetComponent(); + runtimeGltfInstance.ShowMeshes(); + + LoadNewModel(runtimeGltfInstance.Root); + await server.SendCommandAsync(new PipeCommands.VRMLoadStatus { Valid = true }); + } + + public void LoadNewModel(GameObject model) + { + if (CurrentModel != null) + { + VMCEvents.OnModelUnloading?.Invoke(CurrentModel); + CurrentModel.transform.SetParent(null); + CurrentModel.SetActive(false); + Destroy(CurrentModel); + CurrentModel = null; + } + CurrentModel = model; + + VMCEvents.OnCurrentModelChanged?.Invoke(CurrentModel); + + //モデルのSkinnedMeshRendererがカリングされないように、すべてのオプション変更 + foreach (var renderer in CurrentModel.GetComponentsInChildren(true)) + { + renderer.updateWhenOffscreen = true; + } + + IKManager.Instance.ModelInitialize(); + + VMCEvents.OnModelLoaded?.Invoke(CurrentModel); + } + + + #endregion + + #region LipSync + + private void SetLipSyncEnable(bool enable) + { + LipSync.EnableLipSync = enable; + Settings.Current.LipSyncEnable = enable; + } + + private string[] GetLipSyncDevices() + { + return LipSync.GetMicrophoneDevices(); + } + + private void SetLipSyncDevice(string device) + { + LipSync.SetMicrophoneDevice(device); + Settings.Current.LipSyncDevice = device; + } + + private void SetLipSyncGain(float gain) + { + if (gain < 1.0f) gain = 1.0f; + if (gain > 256.0f) gain = 256.0f; + LipSync.Gain = gain; + Settings.Current.LipSyncGain = gain; + } + + private void SetLipSyncMaxWeightEnable(bool enable) + { + LipSync.MaxWeightEnable = enable; + Settings.Current.LipSyncMaxWeightEnable = enable; + } + + private void SetLipSyncWeightThreashold(float threashold) + { + LipSync.WeightThreashold = threashold; + Settings.Current.LipSyncWeightThreashold = threashold; + } + + private void SetLipSyncMaxWeightEmphasis(bool enable) + { + LipSync.MaxWeightEmphasis = enable; + Settings.Current.LipSyncMaxWeightEmphasis = enable; + } + + #endregion + + #region Color + + private void ChangeBackgroundColor(float r, float g, float b, bool isCustom) + { + BackgroundRenderer.material.color = new Color(r, g, b, 1.0f); + Settings.Current.BackgroundColor = BackgroundRenderer.material.color; + if (isCustom) Settings.Current.CustomBackgroundColor = BackgroundRenderer.material.color; + Settings.Current.IsTransparent = false; + SetDwmTransparent(false); + } + + private void SetBackgroundTransparent() + { + Settings.Current.IsTransparent = true; +#if !UNITY_EDITOR // エディタ上では動きません。 + BackgroundRenderer.material.color = new Color(0.0f, 0.0f, 0.0f, 0.0f); + SetDwmTransparent(true); +#endif + } + + private bool lastHideWindowBorder = false; + private int? windowBorderWidth = null; + private int? windowBorderHeight = null; + + void HideWindowBorder(bool enable) + { + if (lastHideWindowBorder == enable) return; + lastHideWindowBorder = enable; + Settings.Current.HideBorder = enable; +#if !UNITY_EDITOR // エディタ上では動きません。 + var hwnd = GetUnityWindowHandle(); + var clientrect = GetUnityWindowClientPosition(); + if (windowBorderWidth.HasValue == false) + { + var windowrect = GetUnityWindowPosition(); + windowBorderWidth = windowrect.width - clientrect.width; + windowBorderHeight = windowrect.height - clientrect.height; + } + if (enable) + { + SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE | WS_CLIPCHILDREN); //ウインドウ枠の削除 + SetUnityWindowFrameChanged(); + WaitOneFrameAction(() => SetUnityWindowSize(clientrect.width, clientrect.height)); + } + else + { + SetWindowLong(hwnd, GWL_STYLE, defaultWindowStyle | WS_CLIPCHILDREN); + SetUnityWindowFrameChanged(); + WaitOneFrameAction(() => SetUnityWindowSize(clientrect.width + windowBorderWidth.Value, clientrect.height + windowBorderHeight.Value)); + } +#endif + } + + private void ResizeWindow(int width, int height) + { +#if !UNITY_EDITOR + var clientrect = GetUnityWindowClientPosition(); + var windowrect = GetUnityWindowPosition(); + if (windowBorderWidth.HasValue == false) + { + windowBorderWidth = windowrect.width - clientrect.width; + windowBorderHeight = windowrect.height - clientrect.height; + } + if (clientrect.width == windowrect.width) + { + SetUnityWindowSize(width, height); + } + else + { + SetUnityWindowSize(width + windowBorderWidth.Value, height + windowBorderHeight.Value); + } +#endif + } + + void SetWindowTopMost(bool enable) + { + Settings.Current.IsTopMost = enable; +#if !UNITY_EDITOR // エディタ上では動きません。 + SetUnityWindowTopMost(enable); +#endif + } + + void SetWindowClickThrough(bool enable) + { + Settings.Current.WindowClickThrough = enable; +#if !UNITY_EDITOR // エディタ上では動きません。 + var hwnd = GetUnityWindowHandle(); + //var hwnd = GetActiveWindow(); + if (enable) + { + SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT); //クリックを透過する + } + else + { + SetWindowLong(hwnd, GWL_EXSTYLE, defaultExWindowStyle); + } +#endif + } + + void OnRenderImage(RenderTexture from, RenderTexture to) + { + Graphics.Blit(from, to, BackgroundRenderer.material); + } + + #endregion + + #region CameraControl + + + + private void SetGridVisible(bool enable) + { + GridCanvas?.SetActive(enable); + Settings.Current.ShowCameraGrid = enable; + } + + #endregion + + #region BlinkControl + void SetAutoBlinkEnable(bool enable) + { + faceController.EnableBlink = enable; + Settings.Current.AutoBlinkEnable = enable; + } + void SetBlinkTimeMin(float time) + { + faceController.BlinkTimeMin = time; + Settings.Current.BlinkTimeMin = time; + } + void SetBlinkTimeMax(float time) + { + faceController.BlinkTimeMax = time; + Settings.Current.BlinkTimeMax = time; + } + void SetCloseAnimationTime(float time) + { + faceController.CloseAnimationTime = time; + Settings.Current.CloseAnimationTime = time; + } + void SetOpenAnimationTime(float time) + { + faceController.OpenAnimationTime = time; + Settings.Current.OpenAnimationTime = time; + } + void SetClosingTime(float time) + { + faceController.ClosingTime = time; + Settings.Current.ClosingTime = time; + } + + private Dictionary BlendShapeNameDictionary = new Dictionary + { + { "通常(NEUTRAL)", ExpressionPreset.neutral }, + { "喜(JOY)", ExpressionPreset.happy }, // joy -> happy + { "怒(ANGRY)", ExpressionPreset.angry }, + { "哀(SORROW)", ExpressionPreset.sad }, // sorrow -> sad + { "楽(FUN)", ExpressionPreset.relaxed }, // fun -> relaxed + { "上見(LOOKUP)", ExpressionPreset.lookUp }, + { "下見(LOOKDOWN)", ExpressionPreset.lookDown }, + { "左見(LOOKLEFT)", ExpressionPreset.lookLeft }, + { "右見(LOOKRIGHT)", ExpressionPreset.lookRight }, + }; + + void SetDefaultFace(string face) + { + faceController.StopBlink = false; + if (string.IsNullOrEmpty(face)) + { + } + else if (BlendShapeNameDictionary.ContainsKey(face)) + { + faceController.DefaultFace = BlendShapeNameDictionary[face]; + faceController.FacePresetName = null; + } + else + { + faceController.DefaultFace = ExpressionPreset.custom; + faceController.FacePresetName = face; + } + } + #endregion + + #region HandFaceControll + + + + /// + /// 名前でショートカット(キーアクション)を呼び出す。 + /// VMCProtocol V3.1 の /VMC/Ext/Set/Shortcut から使う。 + /// + public void DoShortcutByName(string shortcutName) + { + if (string.IsNullOrEmpty(shortcutName)) return; + if (Settings.Current.KeyActions == null) return; + + var action = Settings.Current.KeyActions.FirstOrDefault(d => d.Name == shortcutName); + if (action == null) + { + Debug.LogWarning($"Shortcut not found: {shortcutName}"); + return; + } + DoKeyAction(action); + } + + public void DoKeyAction(KeyAction action) + { + if (action.HandAction) + { + IKManager.Instance.HandController.SetHandAngle(action.Hand == Hands.Left || action.Hand == Hands.Both, action.Hand == Hands.Right || action.Hand == Hands.Both, action.HandAngles, action.HandChangeTime); + } + else if (action.FaceAction) + { + foreach (var externalMotionReceiver in externalMotionReceivers) + { + externalMotionReceiver.DisableBlendShapeReception = action.DisableBlendShapeReception; + } + LipSync.MaxLevel = action.LipSyncMaxLevel; + faceController.SetFace(action.FaceNames, action.FaceStrength, action.StopBlink); + } + else if (action.FunctionAction) + { + switch (action.Function) + { + case Functions.ShowControlPanel: + ExecuteControlPanel(); + break; + case Functions.ColorGreen: + ChangeBackgroundColor(0.0f, 1.0f, 0.0f, false); + break; + case Functions.ColorBlue: + ChangeBackgroundColor(0.0f, 0.0f, 1.0f, false); + break; + case Functions.ColorWhite: + ChangeBackgroundColor(0.9375f, 0.9375f, 0.9375f, false); + break; + case Functions.ColorCustom: + ChangeBackgroundColor(Settings.Current.CustomBackgroundColor.r, Settings.Current.CustomBackgroundColor.g, Settings.Current.CustomBackgroundColor.b, true); + break; + case Functions.ColorTransparent: + SetBackgroundTransparent(); + break; + case Functions.FrontCamera: + CameraManager.Current.ChangeCamera(CameraTypes.Front); + break; + case Functions.BackCamera: + CameraManager.Current.ChangeCamera(CameraTypes.Back); + break; + case Functions.FreeCamera: + CameraManager.Current.ChangeCamera(CameraTypes.Free); + break; + case Functions.PositionFixedCamera: + CameraManager.Current.ChangeCamera(CameraTypes.PositionFixed); + break; + case Functions.PauseTracking: + DeviceInfo.pauseTracking = !DeviceInfo.pauseTracking; + break; + case Functions.ShowCalibrationWindow: + server?.SendCommandAsync(new PipeCommands.ShowCalibrationWindow { }); + break; + case Functions.ShowPhotoWindow: + server?.SendCommandAsync(new PipeCommands.ShowPhotoWindow { }); + break; + case Functions.StartMotionRecording: + motionRecorder?.StartRecording(); + break; + case Functions.StopMotionRecording: + motionRecorder?.StopRecording(); + break; + } + } + else if (action.MotionAction) + { + if (action.MotionPlayType == 0) //モーション再生 + { + motionPlayer?.PlayByPath(action.MotionFilePath); + } + else if (action.MotionPlayType == 1) //ポーズ適用 + { + motionPlayer?.ApplyPoseByPath(action.MotionFilePath, action.MotionFrame); + } + else //解除(停止) + { + motionPlayer?.Stop(); + } + } + } + + + #endregion + + + #region ExternalMotionSender + + private void SetExternalMotionSenderEnable(bool enable) + { + if (IsPreRelease == false) return; + Settings.Current.ExternalMotionSenderEnable = enable; + ExternalMotionSenderObject.SetActive(enable); + } + + private void ChangeExternalMotionSenderAddress(string address, int port, int pstatus, int proot, int pbone, int pblendshape, int pcamera, int pdevices, string optionstring, bool responderEnable, bool useNormalizedBone, bool sendVRM1Expression) + { + //VMCProtocolの仕様準拠オプション + Settings.Current.ExternalMotionSenderUseNormalizedBone = useNormalizedBone; + Settings.Current.ExternalMotionSenderSendVRM1Expression = sendVRM1Expression; + + Settings.Current.ExternalMotionSenderAddress = address; + Settings.Current.ExternalMotionSenderPort = port; + Settings.Current.ExternalMotionSenderPeriodStatus = pstatus; + Settings.Current.ExternalMotionSenderPeriodRoot = proot; + Settings.Current.ExternalMotionSenderPeriodBone = pbone; + Settings.Current.ExternalMotionSenderPeriodBlendShape = pblendshape; + Settings.Current.ExternalMotionSenderPeriodCamera = pcamera; + Settings.Current.ExternalMotionSenderPeriodDevices = pdevices; + Settings.Current.ExternalMotionSenderOptionString = optionstring; + Settings.Current.ExternalMotionSenderResponderEnable = responderEnable; + + externalMotionSender.periodStatus = pstatus; + externalMotionSender.periodRoot = proot; + externalMotionSender.periodBone = pbone; + externalMotionSender.periodBlendShape = pblendshape; + externalMotionSender.periodCamera = pcamera; + externalMotionSender.periodDevices = pdevices; + externalMotionSender.ChangeOSCAddress(address, port); + externalMotionSender.optionString = optionstring; + easyDeviceDiscoveryProtocolManager.responderEnable = responderEnable; + if (responderEnable) easyDeviceDiscoveryProtocolManager.gameObject.SetActive(true); + } + + public void ChangeExternalMotionSenderAddress(string address, int port) + { + Settings.Current.ExternalMotionSenderAddress = address; + Settings.Current.ExternalMotionSenderPort = port; + + externalMotionSender.ChangeOSCAddress(address, port); + } + + private void AddVMCProtocolReceiver(VMCProtocolReceiverSettings setting) + { + var obj = new GameObject("ExternalReceiver " + setting.Name); + obj.transform.parent = ExternalMotionReceiverObject.transform; + var receiver = obj.AddComponent(); + receiver.externalSender = externalMotionSender; + receiver.MIDICCWrapper = midiCCWrapper; + receiver.eddp = easyDeviceDiscoveryProtocolManager; + receiver.CurrentModel = CurrentModel; + receiver.Initialize(); + + externalMotionReceivers.Add(receiver); + externalMotionSender.externalReceiver = externalMotionReceivers.FirstOrDefault(); + easyDeviceDiscoveryProtocolManager.externalReceiver = externalMotionSender.externalReceiver; + receiver.SetSetting(setting); + receiver.ChangeOSCPort(setting.Port); + receiver.SetObjectActive(setting.Enable); + } + + private void RemoveVMCProtocolReceiver(int index) + { + DestroyImmediate(externalMotionReceivers[index].gameObject); + externalMotionReceivers.RemoveAt(index); + Settings.Current.VMCProtocolReceiverSettingsList.RemoveAt(index); + if (index == 0) + { + externalMotionSender.externalReceiver = externalMotionReceivers.FirstOrDefault(); + easyDeviceDiscoveryProtocolManager.externalReceiver = externalMotionSender.externalReceiver; + } + } + + private void SetVMCProtocolReceiverSetting(PipeCommands.SetVMCProtocolReceiverSetting d) + { + int index = d.Index; + bool changePort = d.Port != Settings.Current.VMCProtocolReceiverSettingsList[index].Port; + var setting = Settings.Current.VMCProtocolReceiverSettingsList[index].Import(d); + + externalMotionReceivers[index].SetSetting(setting); + externalMotionReceivers[index].SetObjectActive(setting.Enable); + + if (changePort) + { + externalMotionReceivers[index].ChangeOSCPort(setting.Port); + } + } + + private void SetVMCProtocolReceiverEnable(int index, bool enable) + { + var setting = Settings.Current.VMCProtocolReceiverSettingsList[index]; + setting.Enable = enable; + + externalMotionReceivers[index].SetSetting(setting); + externalMotionReceivers[index].SetObjectActive(setting.Enable); + } + + private void SetExternalMotionReceiverRequester(bool requesterEnable) + { + Settings.Current.ExternalMotionReceiverRequesterEnable = requesterEnable; + easyDeviceDiscoveryProtocolManager.requesterEnable = requesterEnable; + if (requesterEnable) easyDeviceDiscoveryProtocolManager.gameObject.SetActive(true); + } + + private void WaitOneFrameAction(Action action) + { + StartCoroutine(WaitOneFrameCoroutine(action)); + } + + private IEnumerator WaitOneFrameCoroutine(Action action) + { + yield return null; + action?.Invoke(); + } + + #endregion + + private void SetTrackingFilterEnable(bool global, bool hmd, bool controller, bool tracker) + { + DeviceInfo.globalEnable = global; + DeviceInfo.hmdEnable = hmd; + DeviceInfo.controllerEnable = controller; + DeviceInfo.trackerEnable = tracker; + Settings.Current.TrackingFilterEnable = global; + Settings.Current.TrackingFilterHmdEnable = hmd; + Settings.Current.TrackingFilterControllerEnable = controller; + Settings.Current.TrackingFilterTrackerEnable = tracker; + } + + private void SetModelModifierEnable(bool fixKneeRotation, bool fixElbowRotation) + { + Settings.Current.FixKneeRotation = fixKneeRotation; + Settings.Current.FixElbowRotation = fixElbowRotation; + } + + private void SetHandleControllerAsTracker(bool handleCasT) + { + Settings.Current.HandleControllerAsTracker = handleCasT; + } + + #region Setting + + + private NotifyLogTypes notifyLogLevel = NotifyLogTypes.Warning; + private async void LogMessageHandler(string cond, string trace, LogType type) + { + NotifyLogTypes notifyType = NotifyLogTypes.Warning; + switch (type) + { + case LogType.Assert: notifyType = NotifyLogTypes.Assert; CriticalErrorCount++; break; + case LogType.Error: notifyType = NotifyLogTypes.Error; CriticalErrorCount++; break; + case LogType.Exception: notifyType = NotifyLogTypes.Exception; CriticalErrorCount++; break; + case LogType.Log: notifyType = NotifyLogTypes.Log; break; + case LogType.Warning: notifyType = NotifyLogTypes.Warning; break; + default: notifyType = NotifyLogTypes.Log; break; + } + + //通知レベルがLog以外の時かつ、Warning以下かつ、*から始まらないものはうるさいので飛ばさない + if (cond.StartsWith("*")) + { + cond = cond.Substring(1); + } + else if (notifyLogLevel != NotifyLogTypes.Log && notifyLogLevel <= notifyType) + { + return; + } + + //あまりにも致命的エラーが多すぎる場合は強制終了する + if ((!IsCriticalErrorCountOver) && CriticalErrorCount > PipeCommands.ErrorCountMax) + { + IsCriticalErrorCountOver = true; + + //最後のエラーをファイルとして出力 + string message = "[" + type.ToString() + "] " + cond + "\n\n" + trace + "\n\n" + DateTime.Now.ToString(); + File.WriteAllText(Application.dataPath + "/../CriticalErrorCountOver.txt", message); +#if UNITY_EDITOR + UnityEditor.EditorApplication.isPlaying = false; +#else + Application.Quit(); +#endif + Debug.Log("CriticalErrorCount over"); + } + + await server.SendCommandAsync(new PipeCommands.LogNotify + { + condition = cond, + stackTrace = trace, + type = notifyType, + errorCount = CriticalErrorCount, + }); + + if ( + (type == LogType.Error && cond.StartsWith("[Calib Fail]")) || + (type == LogType.Exception && cond.StartsWith("NullReferenceException")) + ) + { + //状態を失敗で上書き + IKManager.Instance.CalibrationResult = new PipeCommands.CalibrationResult + { + Type = PipeCommands.CalibrateType.Invalid, + Message = cond, + UserHeight = -1 + }; + + //エラー送信 + await server.SendCommandAsync(IKManager.Instance.CalibrationResult); + } + } + + private bool IsRegisteredEventCallBack = false; + private void RegisterEventCallBack() + { + if (IsRegisteredEventCallBack == false) + { + IsRegisteredEventCallBack = true; + TrackingPointManager.Instance.TrackerMovedEvent += TransformExtensions_TrackerMovedEvent; + ExternalReceiverForVMC.StatusStringUpdated += StatusStringUpdatedEvent; + } + } + + private void SaveSettings(string path) + { + if (string.IsNullOrEmpty(path)) + { + return; + } + + Settings.Current.AAA_SavedVersion = baseVersionString; + + File.WriteAllText(path, Json.Serializer.ToReadable(Json.Serializer.Serialize(Settings.Current))); + + //ファイルが正常に書き込めたので、現在共通設定に記録されているパスと違う場合、共通設定に書き込む + if (CommonSettings.Current.LoadSettingFilePathOnStart != path) + { + CommonSettings.Current.LoadSettingFilePathOnStart = path; + CommonSettings.Save(); + Debug.Log("Save last loaded file of " + path); + } + } + + //設定の読み込み + public void LoadSettings(string path = null) + { + //設定パスがnull or 存在しないなら、default読み込み + //パスが渡されていれば2回目以降の読み込み + if (string.IsNullOrEmpty(path) || (!File.Exists(path))) + { + //共通設定を読み込み + CommonSettings.Load(); + + //初回読み込みファイルが存在しなければdefault.jsonを + if (string.IsNullOrEmpty(CommonSettings.Current.LoadSettingFilePathOnStart) || (!File.Exists(CommonSettings.Current.LoadSettingFilePathOnStart))) + { + path = Application.dataPath + "/../default.json"; + Debug.Log("Load default.json"); + } + else + { + //存在すればそのPathを読みに行こうとする + path = CommonSettings.Current.LoadSettingFilePathOnStart; + Debug.Log("Load last loaded file of " + path); + } + } + + //設定の読み込みを試みる + try + { + path = Path.GetFullPath(path); //フルパスに変換 + Settings.Current = Json.Serializer.Deserialize(File.ReadAllText(path)); //設定を読み込み + //mocopi/VIVE/Tobiiが本体機能だった頃の設定をプラグインの設定領域へ移す + PluginSettingsMigration.Migrate(Settings.Current); + float divide = 0; + //腰情報を読み込む + if (float.TryParse(File.ReadAllText(Application.dataPath + "/../PelvisTrackerOffsetDivide.txt"), out divide)) + { + Calibrator.pelvisOffsetDivide = divide;//腰オフセット分割数を記録 + } + } + catch (Exception ex) + { + //読み込めなかったときはエラーをファイルとして出力 + File.WriteAllText(Application.dataPath + "/../exception.txt", ex.ToString() + ":" + ex.Message); + Debug.LogError(ex.ToString() + ":" + ex.Message); + } + + Debug.Log("Loaded config: " + path); + + //スケールを元に戻す + IKManager.Instance.ResetTrackerScale(); + //設定を適用する + ApplySettings(); + + //有効なJSONが取得できたかチェック + if (Settings.Current != null) + { + lastLoadedConfigPath = path; //パスを記録 + + //ファイルが正常に存在したので、現在共通設定に記録されているパスと違う場合、共通設定に書き込む + if (CommonSettings.Current.LoadSettingFilePathOnStart != path) + { + CommonSettings.Current.LoadSettingFilePathOnStart = path; + CommonSettings.Save(); + Debug.Log("Save last loaded file of " + path); + } + } + + //設定の変更を通知 + VMCEvents.OnLoadedConfigPathChanged?.Invoke(path); + } + + //Settings.Currentを各種設定に適用 + private async void ApplySettings() + { + //VRMのパスが有効で、存在するなら読み込む + if (string.IsNullOrWhiteSpace(Settings.Current.VRMPath) == false + && File.Exists(Settings.Current.VRMPath)) + { + await server.SendCommandAsync(new PipeCommands.LoadVRMPath { Path = Settings.Current.VRMPath }); + await ImportVRM(Settings.Current.VRMPath); + + //メタ情報をOSC送信する + VRMmetaLoadedAction?.Invoke(await LoadVRMMetaAsync(Settings.Current.VRMPath)); + } + + //SetResolutionは強制的にウインドウ枠を復活させるのでBorder設定の前にやっておく必要がある + if (Screen.resolutions.Any(d => d.width == Settings.Current.ScreenWidth && d.height == Settings.Current.ScreenHeight)) + { + UpdateActionQueue.Enqueue(() => ResizeWindow(Settings.Current.ScreenWidth, Settings.Current.ScreenHeight)); + } + + if (Settings.Current.BackgroundColor != null) + { + UpdateActionQueue.Enqueue(() => ChangeBackgroundColor(Settings.Current.BackgroundColor.r, Settings.Current.BackgroundColor.g, Settings.Current.BackgroundColor.b, false)); + } + + if (Settings.Current.IsTransparent) + { + UpdateActionQueue.Enqueue(() => SetBackgroundTransparent()); + } + + if (Settings.Current.CustomBackgroundColor != null) + { + await server.SendCommandAsync(new PipeCommands.LoadCustomBackgroundColor { r = Settings.Current.CustomBackgroundColor.r, g = Settings.Current.CustomBackgroundColor.g, b = Settings.Current.CustomBackgroundColor.b }); + } + + UpdateActionQueue.Enqueue(() => HideWindowBorder(Settings.Current.HideBorder)); + await server.SendCommandAsync(new PipeCommands.LoadHideBorder { enable = Settings.Current.HideBorder }); + + UpdateActionQueue.Enqueue(() => SetWindowTopMost(Settings.Current.IsTopMost)); + await server.SendCommandAsync(new PipeCommands.LoadIsTopMost { enable = Settings.Current.IsTopMost }); + + await server.SendCommandAsync(new PipeCommands.LoadCameraFOV { fov = Settings.Current.CameraFOV }); + await server.SendCommandAsync(new PipeCommands.LoadCameraSmooth { speed = Settings.Current.CameraSmooth }); + + SetGridVisible(Settings.Current.ShowCameraGrid); + await server.SendCommandAsync(new PipeCommands.LoadShowCameraGrid { enable = Settings.Current.ShowCameraGrid }); + await server.SendCommandAsync(new PipeCommands.LoadCameraMirror { enable = Settings.Current.CameraMirrorEnable }); + SetWindowClickThrough(Settings.Current.WindowClickThrough); + await server.SendCommandAsync(new PipeCommands.LoadSetWindowClickThrough { enable = Settings.Current.WindowClickThrough }); + SetLipSyncDevice(Settings.Current.LipSyncDevice); + await server.SendCommandAsync(new PipeCommands.LoadLipSyncDevice { device = Settings.Current.LipSyncDevice }); + SetLipSyncGain(Settings.Current.LipSyncGain); + await server.SendCommandAsync(new PipeCommands.LoadLipSyncGain { gain = Settings.Current.LipSyncGain }); + SetLipSyncMaxWeightEnable(Settings.Current.LipSyncMaxWeightEnable); + await server.SendCommandAsync(new PipeCommands.LoadLipSyncMaxWeightEnable { enable = Settings.Current.LipSyncMaxWeightEnable }); + SetLipSyncWeightThreashold(Settings.Current.LipSyncWeightThreashold); + await server.SendCommandAsync(new PipeCommands.LoadLipSyncWeightThreashold { threashold = Settings.Current.LipSyncWeightThreashold }); + SetLipSyncMaxWeightEmphasis(Settings.Current.LipSyncMaxWeightEmphasis); + await server.SendCommandAsync(new PipeCommands.LoadLipSyncMaxWeightEmphasis { enable = Settings.Current.LipSyncMaxWeightEmphasis }); + + SetAutoBlinkEnable(Settings.Current.AutoBlinkEnable); + await server.SendCommandAsync(new PipeCommands.LoadAutoBlinkEnable { enable = Settings.Current.AutoBlinkEnable }); + SetBlinkTimeMin(Settings.Current.BlinkTimeMin); + await server.SendCommandAsync(new PipeCommands.LoadBlinkTimeMin { time = Settings.Current.BlinkTimeMin }); + SetBlinkTimeMax(Settings.Current.BlinkTimeMax); + await server.SendCommandAsync(new PipeCommands.LoadBlinkTimeMax { time = Settings.Current.BlinkTimeMax }); + SetCloseAnimationTime(Settings.Current.CloseAnimationTime); + await server.SendCommandAsync(new PipeCommands.LoadCloseAnimationTime { time = Settings.Current.CloseAnimationTime }); + SetOpenAnimationTime(Settings.Current.OpenAnimationTime); + await server.SendCommandAsync(new PipeCommands.LoadOpenAnimationTime { time = Settings.Current.OpenAnimationTime }); + SetClosingTime(Settings.Current.ClosingTime); + await server.SendCommandAsync(new PipeCommands.LoadClosingTime { time = Settings.Current.ClosingTime }); + SetDefaultFace(Settings.Current.DefaultFace); + await server.SendCommandAsync(new PipeCommands.LoadDefaultFace { face = Settings.Current.DefaultFace }); + + await server.SendCommandAsync(new PipeCommands.LoadControllerTouchPadPoints + { + IsOculus = Settings.Current.IsOculus, + LeftPoints = Settings.Current.LeftTouchPadPoints, + LeftCenterEnable = Settings.Current.LeftCenterEnable, + RightPoints = Settings.Current.RightTouchPadPoints, + RightCenterEnable = Settings.Current.RightCenterEnable + }); + await server.SendCommandAsync(new PipeCommands.LoadControllerStickPoints + { + LeftPoints = Settings.Current.LeftThumbStickPoints, + RightPoints = Settings.Current.RightThumbStickPoints, + }); + + KeyAction.KeyActionsUpgrade(Settings.Current.KeyActions); + + if (Settings.Current.IsSettingVersionBefore(0, 48)) + { + //v0.48 BlendShapeKey case sensitive. + foreach (var keyAction in Settings.Current.KeyActions) + { + if (keyAction.FaceNames != null && keyAction.FaceNames.Count > 0) + { + keyAction.FaceNames = keyAction.FaceNames.Select(d => faceController.GetCaseSensitiveKeyName(d)).ToList(); + } + } + } + + if (Settings.Current.IsSettingVersionBefore(0, 56)) + { + //v0.56 Configure multiple VMCProtocol receivers + + if (Settings.Current.ExternalMotionReceiverPortList == null) Settings.Current.ExternalMotionReceiverPortList = new List() { Settings.Current.ExternalMotionReceiverPort, Settings.Current.ExternalMotionReceiverPort + 1 }; + if (Settings.Current.ExternalMotionReceiverDelayMsList == null) Settings.Current.ExternalMotionReceiverDelayMsList = new List() { 0, 0 }; + if (Settings.Current.ExternalMotionReceiverEnableList == null) Settings.Current.ExternalMotionReceiverEnableList = new List() { Settings.Current.ExternalMotionReceiverEnable, false }; + + for (int i = 0; i < Settings.Current.ExternalMotionReceiverEnableList.Count; i++) + { + Settings.Current.VMCProtocolReceiverSettingsList.Add(new VMCProtocolReceiverSettings + { + Enable = Settings.Current.ExternalMotionReceiverEnableList[i], + Port = Settings.Current.ExternalMotionReceiverPortList[i], + DelayMs = Settings.Current.ExternalMotionReceiverDelayMsList[i], + Name = $"Receiver {i + 1}", + ApplyRootRotation = false, + ApplyRootPosition = false, + ApplySpine = false, + ApplyChest = false, + ApplyHead = false, + ApplyLeftArm = false, + ApplyRightArm = false, + ApplyLeftHand = false, + ApplyRightHand = false, + ApplyLeftLeg = false, + ApplyRightLeg = false, + ApplyLeftFoot = false, + ApplyRightFoot = false, + ApplyEye = false, + ApplyLeftFinger = false, + ApplyRightFinger = false, + }); ; + } + } + + SteamVR2Input.EnableSkeletal = Settings.Current.EnableSkeletal; + + await server.SendCommandAsync(new PipeCommands.LoadSkeletalInputEnable { enable = Settings.Current.EnableSkeletal }); + + await server.SendCommandAsync(new PipeCommands.LoadKeyActions { KeyActions = Settings.Current.KeyActions }); + await server.SendCommandAsync(new PipeCommands.SetHandFreeOffset + { + LeftHandPositionX = (int)Mathf.Round(Settings.Current.LeftHandPositionX * 1000), + LeftHandPositionY = (int)Mathf.Round(Settings.Current.LeftHandPositionY * 1000), + LeftHandPositionZ = (int)Mathf.Round(Settings.Current.LeftHandPositionZ * 1000), + LeftHandRotationX = (int)Settings.Current.LeftHandRotationX, + LeftHandRotationY = (int)Settings.Current.LeftHandRotationY, + LeftHandRotationZ = (int)Settings.Current.LeftHandRotationZ, + RightHandPositionX = (int)Mathf.Round(Settings.Current.RightHandPositionX * 1000), + RightHandPositionY = (int)Mathf.Round(Settings.Current.RightHandPositionY * 1000), + RightHandPositionZ = (int)Mathf.Round(Settings.Current.RightHandPositionZ * 1000), + RightHandRotationX = (int)Settings.Current.RightHandRotationX, + RightHandRotationY = (int)Settings.Current.RightHandRotationY, + RightHandRotationZ = (int)Settings.Current.RightHandRotationZ, + SwivelOffset = Settings.Current.SwivelOffset, + }); + IKManager.Instance.SetHandFreeOffset(); + + await server.SendCommandAsync(new PipeCommands.LoadLipSyncEnable { enable = Settings.Current.LipSyncEnable }); + SetLipSyncEnable(Settings.Current.LipSyncEnable); + + await server.SendCommandAsync(new PipeCommands.SetLightAngle { X = Settings.Current.LightRotationX, Y = Settings.Current.LightRotationY }); + SetLightAngle(Settings.Current.LightRotationX, Settings.Current.LightRotationY); + await server.SendCommandAsync(new PipeCommands.ChangeLightColor { a = Settings.Current.LightColor.a, r = Settings.Current.LightColor.r, g = Settings.Current.LightColor.g, b = Settings.Current.LightColor.b }); + ChangeLightColor(Settings.Current.LightColor.a, Settings.Current.LightColor.r, Settings.Current.LightColor.g, Settings.Current.LightColor.b); + + SetExternalMotionSenderEnable(Settings.Current.ExternalMotionSenderEnable); + ChangeExternalMotionSenderAddress(Settings.Current.ExternalMotionSenderAddress, Settings.Current.ExternalMotionSenderPort, Settings.Current.ExternalMotionSenderPeriodStatus, Settings.Current.ExternalMotionSenderPeriodRoot, Settings.Current.ExternalMotionSenderPeriodBone, Settings.Current.ExternalMotionSenderPeriodBlendShape, Settings.Current.ExternalMotionSenderPeriodCamera, Settings.Current.ExternalMotionSenderPeriodDevices, Settings.Current.ExternalMotionSenderOptionString, Settings.Current.ExternalMotionSenderResponderEnable, Settings.Current.ExternalMotionSenderUseNormalizedBone, Settings.Current.ExternalMotionSenderSendVRM1Expression); + + foreach(var receiver in externalMotionReceivers) + { + DestroyImmediate(receiver.gameObject); + } + externalMotionReceivers.Clear(); + + foreach(var receiverSetting in Settings.Current.VMCProtocolReceiverSettingsList) + { + AddVMCProtocolReceiver(receiverSetting); + } + + SetMidiCCBlendShape(Settings.Current.MidiCCBlendShape); + SetMidiEnable(Settings.Current.MidiEnable); + + SetTrackingFilterEnable(Settings.Current.TrackingFilterEnable, Settings.Current.TrackingFilterHmdEnable, Settings.Current.TrackingFilterControllerEnable, Settings.Current.TrackingFilterTrackerEnable); + + SetModelModifierEnable(Settings.Current.FixKneeRotation, Settings.Current.FixElbowRotation); + SetHandleControllerAsTracker(Settings.Current.HandleControllerAsTracker); + SetQualitySettings(new PipeCommands.SetQualitySettings + { + antiAliasing = Settings.Current.AntiAliasing, + }); + SetVMT(Settings.Current.VirtualMotionTrackerEnable, Settings.Current.VirtualMotionTrackerNo); + + + LoadAdvancedGraphicsOption(); + + AdditionalSettingAction?.Invoke(null); + + //プラグインへ設定の適用を通知する + pluginHost?.RaiseSettingsApplied(); + + await server.SendCommandAsync(new PipeCommands.SetWindowNum { Num = CurrentWindowNum }); + } + + + #endregion + + private void SetMidiCCBlendShape(List blendshapes) + { + Settings.Current.MidiCCBlendShape = blendshapes; + midiCCBlendShape.KnobToBlendShape = blendshapes.ToArray(); + } + + private void SetMidiEnable(bool enable) + { + Settings.Current.MidiEnable = enable; + InputManager.Current.midiCCWrapper.gameObject.SetActive(enable); + } + + + private ConcurrentQueue UpdateActionQueue = new ConcurrentQueue(); + + // Update is called once per frame + void Update() + { + KeyboardAction.Update(); + + //if (Input.GetKeyDown(KeyCode.P)) + //{ + // TakePhoto(16000, true); + //} + + Action action; + if (UpdateActionQueue.TryDequeue(out action)) action(); + + // コントロールパネル起動監視 + if (!showControlPanelMessage && controlPanelStartTime >= 0 && + Time.time - controlPanelStartTime > CONTROL_PANEL_TIMEOUT && + isFirstTimeExecute) // まだLoadCurrentSettingsが来ていない + { + showControlPanelMessage = true; + controlPanelStartTime = -1f; // 一度だけ表示(監視停止) + } + } + + private int WindowX; + private int WindowY; + private Vector2 OldMousePos; + private bool isWindowDragging = false; + + private DateTime lastWindowMoveTime = DateTime.MinValue; + private bool windowPositionSent = true; + private int lastWindowLeft = 0; + private int lastWindowTop = 0; + + void LateUpdate() + { + var r = GetUnityWindowPosition(); + //Windowの移動操作 + //ドラッグ開始 + if (Input.GetMouseButtonDown((int)MouseButtons.Left) && Input.GetKey(KeyCode.LeftAlt) == false && Input.GetKey(KeyCode.RightAlt) == false) + { + WindowX = r.left; + WindowY = r.top; + OldMousePos = GetWindowsMousePosition(); + isWindowDragging = true; + } + + //ドラッグ中 + if (Input.GetMouseButton((int)MouseButtons.Left) && isWindowDragging) + { + Vector2 pos = GetWindowsMousePosition(); + if (pos != OldMousePos) + { + WindowX += (int)(pos.x - OldMousePos.x); + WindowY += (int)(pos.y - OldMousePos.y); + SetUnityWindowPosition(WindowX, WindowY); + OldMousePos = pos; + } + } + + if (Input.GetMouseButtonUp((int)MouseButtons.Left) && isWindowDragging) + { + isWindowDragging = false; + } + + // 位置が変わったら時刻を記録し、送信済みフラグをリセット + if (r.left != lastWindowLeft || r.top != lastWindowTop) + { + lastWindowMoveTime = DateTime.Now; + windowPositionSent = false; + lastWindowLeft = r.left; + lastWindowTop = r.top; + } + + // 指定ミリ秒間動きがなければ、まだ送信していなければ送信 + if (!windowPositionSent && (DateTime.Now - lastWindowMoveTime).TotalMilliseconds >= 200) + { + SendWindowInfo(); + windowPositionSent = true; + } + } + + // 現在のウィンドウハンドル、子ウィンドウ有効可否を送信し、ウィンドウ情報の更新を促す + void SendWindowInfo() { + context.Post(async s => + { + await server.SendCommandAsync(new PipeCommands.WindowInfo + { + Hwnd = GetUnityWindowHandle(), + Child = Settings.Current.UnityChildWindowEnable, + }); + }, null); + } + + void OnGUI() + { + // コントロールパネル起動監視メッセージ表示(左上に4言語) + if (showControlPanelMessage) + { + var textStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 24, + normal = { textColor = Color.yellow } + }; + + // 4言語のメッセージ + string message = "コントロールパネルの起動を待機しています。コントロールパネルが起動しない場合は、\n" + + "同じフォルダの「起動しない時は(If VMC does not start).txt」を確認してください。\n\n" + + "Waiting for control panel to start. If the control panel does not start,\n" + + "Please check \"起動しない時は(If VMC does not start).txt\" file in the same folder.\n\n" + + "컨트롤 패널의 시작을 기다리고 있습니다. 컨트롤 패널이 시작되지 않는 경우,\n" + + "같은 폴더의 \"起動しない時は(If VMC does not start).txt\" 파일을 확인해주세요.\n\n" + + "正在等待控制面板启动。如果控制面板未启动,\n" + + "请检查同一文件夹中的\"起動しない時は(If VMC does not start).txt\"文件。"; + + GUI.Label(new Rect(10, 10, Screen.width - 20, Screen.height - 20), message, textStyle); + } + } + + #region 自動テスト用フック + + //自動テストハーネス(Assets/Tests)からのみ使用する。 + //コントロールパネル(WPF)からのコマンド経由でしか呼べない処理を、テストから直接呼べるようにするためのもの。 + + internal GameObject Test_CurrentModel => CurrentModel; + + internal void Test_AddVMCProtocolReceiver(VMCProtocolReceiverSettings setting) => AddVMCProtocolReceiver(setting); + + internal void Test_SaveSettings(string path) => SaveSettings(path); + + internal MotionPlayer Test_MotionPlayer => motionPlayer; + + internal MotionRecorder Test_MotionRecorder => motionRecorder; + + #endregion + } +} diff --git a/Assets/Scripts/Debug/TrackerPositionsExporter.cs b/Assets/Scripts/Debug/TrackerPositionsExporter.cs index e9c6fa30..03f70626 100644 --- a/Assets/Scripts/Debug/TrackerPositionsExporter.cs +++ b/Assets/Scripts/Debug/TrackerPositionsExporter.cs @@ -8,20 +8,10 @@ public class TrackerPositionsExporter : MonoBehaviour { ControlWPFWindow window = null; VRIK vrik = null; - GameObject CurrentModel; private void Start() { window = GameObject.Find("ControlWPFWindow").GetComponent(); - VMCEvents.OnModelLoaded += (GameObject CurrentModel) => - { - if (CurrentModel != null) - { - this.CurrentModel = CurrentModel; - vrik = CurrentModel.GetComponent(); - } - }; - KeyboardAction.KeyDownEvent += (object sender, KeyboardEventArgs e) => { if (this.isActiveAndEnabled) @@ -36,10 +26,9 @@ private void Start() private void Export() { - if (vrik == null && CurrentModel != null) + if (vrik == null) { - vrik = CurrentModel.GetComponent(); - Debug.Log("ExternalSender: VRIK Updated"); + vrik = IKManager.Instance.vrik; } if (vrik == null) return; var Trackers = new List(); diff --git a/Assets/Scripts/Debug/TrackerPositionsImporter.cs b/Assets/Scripts/Debug/TrackerPositionsImporter.cs index a5c7fc6f..5e927ebd 100644 --- a/Assets/Scripts/Debug/TrackerPositionsImporter.cs +++ b/Assets/Scripts/Debug/TrackerPositionsImporter.cs @@ -9,7 +9,6 @@ public class TrackerPositionsImporter : MonoBehaviour { ControlWPFWindow window = null; private VRIK vrik = null; - GameObject CurrentModel; private Transform RootObject; private bool isLeftShiftKeyDown = false; @@ -19,15 +18,6 @@ private void Start() { window = GameObject.Find("ControlWPFWindow").GetComponent(); - VMCEvents.OnModelLoaded += (GameObject CurrentModel) => - { - if (CurrentModel != null) - { - this.CurrentModel = CurrentModel; - vrik = CurrentModel.GetComponent(); - } - }; - KeyboardAction.KeyDownEvent += (object sender, KeyboardEventArgs e) => { if (this.isActiveAndEnabled) @@ -67,10 +57,9 @@ private void Start() private void Import(bool createObject) { - if (vrik == null && CurrentModel != null) + if (vrik == null) { - vrik = CurrentModel.GetComponent(); - Debug.Log("ExternalSender: VRIK Updated"); + vrik = IKManager.Instance.vrik; } if (vrik == null) return; var path = Application.dataPath + "/../SavedTrackerPositions/"; diff --git a/Assets/Scripts/ExternalSender/BonePostureConverter.cs b/Assets/Scripts/ExternalSender/BonePostureConverter.cs new file mode 100644 index 00000000..4f968da9 --- /dev/null +++ b/Assets/Scripts/ExternalSender/BonePostureConverter.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using UnityEngine; +using UniVRM10; + +namespace VMC +{ + /// + /// VMCProtocolで受信したオリジナル(非正規化)ボーンのローカル回転を、 + /// ControlRigへ与える正規化ローカル回転へ変換する。 + /// + /// 【送信側は変換不要】 + /// 送信は Vrm10Instance.Humanoid.GetBoneTransform をそのまま読めばオリジナル姿勢が得られる。 + /// + /// 【受信側だけ変換が要る理由】 + /// VMCは受信したボーンを VirtualAvatar のクローン → MotionManager → animator.GetBoneTransform + /// という経路で適用するが、ControlRig生成時の animator は正規化ボーンを指す。 + /// また ControlRig の Process() は毎フレーム正規化ボーンからオリジナルボーンを上書きするため、 + /// オリジナルボーンへ直接書いても次のフレームで消える。 + /// (VRIK・キャリブレーション・モーション再生も全て正規化ボーン前提) + /// そのため、プロトコルの境界でだけ正規化空間へ持ち上げる。 + /// + /// 変換式は UniVRM の と同一。 + /// 初期回転(Tポーズ時のローカル/ワールド回転)の取得もその型に任せている。 + /// + /// VRM0.x由来のモデルは元から正規化されているため変換は恒等写像になり、挙動は変わらない。 + /// + public class BonePostureConverter + { + private readonly Dictionary initialRotations + = new Dictionary(); + + /// 変換が不要か(全ボーンの初期回転が単位=既に正規化済み) + public bool IsIdentity { get; private set; } = true; + + /// + /// モデル読み込み直後(Tポーズかつ ControlRig 構築直後)に呼ぶこと。 + /// VRIK等がボーンを動かした後では初期回転が取れない。 + /// + public static BonePostureConverter Capture(Vrm10Instance vrm10Instance) + { + var converter = new BonePostureConverter(); + if (vrm10Instance == null || vrm10Instance.Humanoid == null) return converter; + + foreach (var (boneTransform, bone) in vrm10Instance.Humanoid.BoneMap) + { + if (boneTransform == null) continue; + + //Tポーズ時のローカル回転・ワールド回転をUniVRMの型に記録させる + var initial = new BoneInitialRotation(boneTransform); + converter.initialRotations[bone] = initial; + + if (Quaternion.Angle(initial.InitialLocalRotation, Quaternion.identity) > 0.001f + || Quaternion.Angle(initial.InitialGlobalRotation, Quaternion.identity) > 0.001f) + { + converter.IsIdentity = false; + } + } + return converter; + } + + /// + /// 受信したオリジナルのローカル回転を、ControlRigへ与える正規化ローカル回転へ変換する。 + /// BoneInitialRotation.NormalizedLocalRotation は Transform の現在値を読むため、 + /// ここでは同じ式に受信値を当てはめる。 + /// + public Quaternion ToNormalizedLocalRotation(HumanBodyBones bone, Quaternion originalLocalRotation) + { + if (initialRotations.TryGetValue(bone, out var initial) == false) return originalLocalRotation; + + return initial.InitialGlobalRotation + * Quaternion.Inverse(initial.InitialLocalRotation) + * originalLocalRotation + * Quaternion.Inverse(initial.InitialGlobalRotation); + } + } +} diff --git a/Assets/Scripts/ExternalSender/BonePostureConverter.cs.meta b/Assets/Scripts/ExternalSender/BonePostureConverter.cs.meta new file mode 100644 index 00000000..d4a47a13 --- /dev/null +++ b/Assets/Scripts/ExternalSender/BonePostureConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 973c7e14cd78bcd45b155cfe81b31444 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ExternalSender/EasyDeviceDiscoveryProtocolManager.cs b/Assets/Scripts/ExternalSender/EasyDeviceDiscoveryProtocolManager.cs index f061c99d..39a7a8d6 100644 --- a/Assets/Scripts/ExternalSender/EasyDeviceDiscoveryProtocolManager.cs +++ b/Assets/Scripts/ExternalSender/EasyDeviceDiscoveryProtocolManager.cs @@ -44,6 +44,8 @@ void Start() void Update() { + if (externalReceiver == null) return; + //受信ポートを常に反映 responder.servicePort = externalReceiver.receivePort; requester.servicePort = externalReceiver.receivePort; diff --git a/Assets/Scripts/ExternalSender/ExternalReceiverForVMC.cs b/Assets/Scripts/ExternalSender/ExternalReceiverForVMC.cs index 7cbb597b..dfa8d480 100644 --- a/Assets/Scripts/ExternalSender/ExternalReceiverForVMC.cs +++ b/Assets/Scripts/ExternalSender/ExternalReceiverForVMC.cs @@ -6,7 +6,7 @@ using System.Reflection; using UnityEngine; using UnityMemoryMappedFile; -using VRM; +using UniVRM10; namespace VMC { @@ -19,9 +19,11 @@ public class ExternalReceiverForVMC : MonoBehaviour public MidiCCWrapper MIDICCWrapper; //仮想コントローラソート済み辞書 + public SortedDictionary virtualHmd = new SortedDictionary(); public SortedDictionary virtualController = new SortedDictionary(); public SortedDictionary virtualTracker = new SortedDictionary(); + public SortedDictionary virtualHmdFiltered = new SortedDictionary(); public SortedDictionary virtualControllerFiltered = new SortedDictionary(); public SortedDictionary virtualTrackerFiltered = new SortedDictionary(); @@ -29,33 +31,35 @@ public class ExternalReceiverForVMC : MonoBehaviour public string statusString = ""; private string statusStringOld = ""; + public bool CorrectRotationWhenCalibration = true; + private int lastCalibrationState = 0; + private bool doCalibration = false; + private Quaternion calibrateRotationOffset = Quaternion.identity; + public EasyDeviceDiscoveryProtocolManager eddp; - public bool receiveBonesFlag; static public Action StatusStringUpdated = null; ControlWPFWindow window = null; - GameObject CurrentModel = null; + public GameObject CurrentModel = null; Camera currentCamera = null; FaceController faceController = null; - VRMLookAtHead vrmLookAtHead = null; + Vrm10Instance vrm10Instance = null; Transform headTransform = null; //仮想視線操作用 GameObject lookTargetOSC; - Action beforeFaceApply; - bool setFaceApplyAction = false; //バッファ Vector3 pos; Quaternion rot; + private Queue<(float timestamp, uOSC.Message message)> MessageBuffer = new Queue<(float timestamp, uOSC.Message message)>(); + public int packets = 0; //ボーン情報取得 Animator animator = null; - //VRMのブレンドシェーププロキシ - VRMBlendShapeProxy blendShapeProxy = null; //ボーンENUM情報テーブル Dictionary HumanBodyBonesTable = new Dictionary(); @@ -63,14 +67,75 @@ public class ExternalReceiverForVMC : MonoBehaviour //ボーン情報テーブル Dictionary HumanBodyBonesPositionTable = new Dictionary(); Dictionary HumanBodyBonesRotationTable = new Dictionary(); - - public bool BonePositionSynchronize = true; //ボーン位置適用(回転は強制) + private VirtualAvatar virtualAvatar; private Dictionary blendShapeBuffer = new Dictionary(); public bool DisableBlendShapeReception { get; set; } - void Start() + private bool enableLocalHandFix = true; + private float lastBoneReceivedTime = 0; + + private VMCProtocolReceiverSettings receiverSetting; + + private bool ApplyBlendShape; + private bool ApplyLookAt; + private bool ApplyTracker; + private bool ApplyCamera; + private bool ApplyLight; + private bool ApplyMidi; + private bool ApplyStatus; + private bool ApplyControl; + private bool ApplySetting; + private bool ApplyControllerInput; + private bool ApplyKeyboardInput; + + public void SetSetting(VMCProtocolReceiverSettings setting) + { + receiverSetting = setting; + + if (virtualAvatar != null) + { + virtualAvatar.Enable = setting.Enable; + virtualAvatar.ApplyRootRotation = setting.ApplyRootRotation; + virtualAvatar.ApplyRootPosition = setting.ApplyRootPosition; + virtualAvatar.ApplySpine = setting.ApplySpine; + virtualAvatar.ApplyChest = setting.ApplyChest; + virtualAvatar.ApplyHead = setting.ApplyHead; + virtualAvatar.ApplyLeftArm = setting.ApplyLeftArm; + virtualAvatar.ApplyRightArm = setting.ApplyRightArm; + virtualAvatar.ApplyLeftHand = setting.ApplyLeftHand; + virtualAvatar.ApplyRightHand = setting.ApplyRightHand; + virtualAvatar.ApplyLeftLeg = setting.ApplyLeftLeg; + virtualAvatar.ApplyRightLeg = setting.ApplyRightLeg; + virtualAvatar.ApplyLeftFoot = setting.ApplyLeftFoot; + virtualAvatar.ApplyRightFoot = setting.ApplyRightFoot; + virtualAvatar.ApplyEye = setting.ApplyEye; + virtualAvatar.ApplyLeftFinger = setting.ApplyLeftFinger; + virtualAvatar.ApplyRightFinger = setting.ApplyRightFinger; + virtualAvatar.CorrectHipBone = setting.CorrectHipBone; + virtualAvatar.IgnoreDefaultBone = setting.IgnoreDefaultBone; + } + + ApplyBlendShape = setting.ApplyBlendShape; + ApplyLookAt = setting.ApplyLookAt; + ApplyTracker = setting.ApplyTracker; + ApplyCamera = setting.ApplyCamera; + ApplyLight = setting.ApplyLight; + ApplyMidi = setting.ApplyMidi; + ApplyStatus = setting.ApplyStatus; + ApplyControl = setting.ApplyControl; + ApplySetting = setting.ApplySetting; + ApplyControllerInput = setting.ApplyControllerInput; + ApplyKeyboardInput = setting.ApplyKeyboardInput; + } + + public void Recenter() + { + virtualAvatar.Recenter(); + } + + public void Initialize() { var server = GetComponent(); server.onDataReceived.AddListener(OnDataReceived); @@ -79,40 +144,58 @@ void Start() faceController = GameObject.Find("AnimationController").GetComponent(); VMCEvents.OnModelLoaded += (GameObject CurrentModel) => { - if (CurrentModel != null) - { - this.CurrentModel = CurrentModel; - vrmLookAtHead = CurrentModel.GetComponent(); - animator = CurrentModel.GetComponent(); - headTransform = null; - if (animator != null) - { - headTransform = animator.GetBoneTransform(HumanBodyBones.Head); - } - } + this.CurrentModel = CurrentModel; + OnModelChanged(); }; VMCEvents.OnCameraChanged += (Camera currentCamera) => { this.currentCamera = currentCamera; }; - beforeFaceApply = () => + var modelRoot = new GameObject("ModelRoot").transform; + modelRoot.SetParent(transform, false); + virtualAvatar = new VirtualAvatar(modelRoot, MotionSource.VMCProtocol); + virtualAvatar.Enable = false; + MotionManager.Instance.AddVirtualAvatar(virtualAvatar); + if (receiverSetting != null) { - vrmLookAtHead.Target = lookTargetOSC.transform; - vrmLookAtHead.LookWorldPosition(); - vrmLookAtHead.Target = null; - }; + SetSetting(receiverSetting); + } + + OnModelChanged(); this.gameObject.SetActive(false); server.enabled = true; } + private void OnDestroy() + { + if (virtualAvatar != null) + { + MotionManager.Instance.RemoveVirtualAvatar(virtualAvatar); + } + } + + private void OnModelChanged() + { + if (CurrentModel != null) + { + vrm10Instance = CurrentModel.GetComponent(); + animator = CurrentModel.GetComponent(); + headTransform = null; + if (animator != null) + { + headTransform = animator.GetBoneTransform(HumanBodyBones.Head); + } + } + } + private object LockObject = new object(); void OnDataReceived(uOSC.Message message) { //有効なとき以外処理しない - if (this.isActiveAndEnabled) + if (this.isActiveAndEnabled && receiverSetting != null) { //生存チェックのためのパケットカウンタ packets++; @@ -121,8 +204,68 @@ void OnDataReceived(uOSC.Message message) packets = 0; } + if (receiverSetting.DelayMs == 0) + { + ProcessMessage(message); + } + else + { + MessageBuffer.Enqueue((Time.realtimeSinceStartup, message)); + } + } + } + /// このクラスが参照しうる引数の最大数(/VMC/Ext/Light の12個) + private const int MaxReferencedValueCount = 12; + + void ProcessMessage(uOSC.Message message) + { + //有効なとき以外処理しない + if (this.isActiveAndEnabled) + { + //引数が足りないメッセージ(他アプリの実装差や古い版、壊れたパケット)でも + //IndexOutOfRangeで受信処理ごと落ちないように、参照しうる長さまでnullで埋める。 + //nullはどの型チェック(is float 等)にも一致しないので、該当ブランチは自然に無視される。 + var originalValues = message.values; + if (message.values == null || message.values.Length < MaxReferencedValueCount) + { + var padded = new object[MaxReferencedValueCount]; + if (message.values != null) + { + Array.Copy(message.values, padded, message.values.Length); + } + message.values = padded; + } + + //仮想Hmd V2.3 + if (message.address == "/VMC/Ext/Hmd/Pos" && ApplyTracker + && (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 serial = (string)message.values[0]; + var rigidTransform = SetTransform(ref pos, ref rot, ref message); + + lock (LockObject) + { + if (virtualHmd.ContainsKey(serial)) + { + virtualHmd[serial] = rigidTransform; + } + else + { + virtualHmd.Add(serial, rigidTransform); + virtualHmdFiltered.Add(serial, rigidTransform); + } + } + } //仮想コントローラー V2.3 - if (message.address == "/VMC/Ext/Con/Pos" + else if (message.address == "/VMC/Ext/Con/Pos" && ApplyTracker && (message.values[0] is string) && (message.values[1] is float) && (message.values[2] is float) @@ -150,8 +293,7 @@ void OnDataReceived(uOSC.Message message) } } //仮想トラッカー V2.3 - else if ((message.address == "/VMC/Ext/Hmd/Pos" - || message.address == "/VMC/Ext/Tra/Pos") + else if (message.address == "/VMC/Ext/Tra/Pos" && ApplyTracker && (message.values[0] is string) && (message.values[1] is float) && (message.values[2] is float) @@ -179,7 +321,7 @@ void OnDataReceived(uOSC.Message message) } } //フレーム設定 V2.3 - else if (message.address == "/VMC/Ext/Set/Period" + else if (message.address == "/VMC/Ext/Set/Period" && ApplySetting && (message.values[0] is int) && (message.values[1] is int) && (message.values[2] is int) @@ -195,8 +337,65 @@ void OnDataReceived(uOSC.Message message) externalSender.periodCamera = (int)message.values[4]; externalSender.periodDevices = (int)message.values[5]; } + + //コントローラ操作情報 v2.1 + if (message.address == "/VMC/Ext/Con" && ApplyControllerInput + && (message.values[0] is int) + && (message.values[1] is string) + && (message.values[2] is int) + && (message.values[3] is int) + && (message.values[4] is int) + && (message.values[5] is float) + && (message.values[6] is float) + && (message.values[7] is float) + ) + { + var active = (int)message.values[0]; + var name = (string)message.values[1]; + var isLeft = (int)message.values[2] == 1; + var isTouch = (int)message.values[3] == 1; + var isAxis = (int)message.values[4] == 1; + var axis = new Vector3((float)message.values[5], (float)message.values[6], (float)message.values[7]); + + var keyArgs = new OVRKeyEventArgs(name, axis, isLeft, isAxis, isTouch); + if (active == 1) + { + SteamVR2Input.Instance.KeyDownEvent?.Invoke(this, keyArgs); + } + else if (active == 0) + { + SteamVR2Input.Instance.KeyUpEvent?.Invoke(this, keyArgs); + } + else if (active == 2) + { + SteamVR2Input.Instance.AxisChangedEvent?.Invoke(this, keyArgs); + } + } + //キーボード操作情報 v2.1 + else if (message.address == "/VMC/Ext/Key" && ApplyKeyboardInput + && (message.values[0] is int) + && (message.values[1] is string) + && (message.values[2] is int) + ) + { + var active = (int)message.values[0] == 1; + var name = (string)message.values[1]; + var keycode = (int)message.values[2]; + + var keyArgs = new KeyboardEventArgs(keycode); + + if (active) + { + KeyboardAction.KeyDownEvent?.Invoke(this, keyArgs); + } + else + { + KeyboardAction.KeyUpEvent?.Invoke(this, keyArgs); + } + } + //Virtual MIDI CC V2.3 - else if (message.address == "/VMC/Ext/Midi/CC/Val" + else if (message.address == "/VMC/Ext/Midi/CC/Val" && ApplyMidi && (message.values[0] is int) && (message.values[1] is float) ) @@ -204,7 +403,7 @@ void OnDataReceived(uOSC.Message message) MIDICCWrapper.KnobUpdated(0, (int)message.values[0], (float)message.values[1]); } //Camera Control V2.3 - else if (message.address == "/VMC/Ext/Cam" + else if (message.address == "/VMC/Ext/Cam" && ApplyCamera && (message.values[0] is string) && (message.values[1] is float) && (message.values[2] is float) @@ -235,11 +434,15 @@ void OnDataReceived(uOSC.Message message) CameraManager.Current.FreeCamera.GetComponent().enabled = false; //座標とFOVを適用 + //カメラは HandTrackerRoot の子で、この親はキャリブレーションで + //身長比のスケールとオフセットを持つ。ローカル座標として入れることで、 + //送られてきた座標が受信側アバターのスケールへ写像される。 + //(ワールドとして入れるとスケールぶん位置がずれる) CameraManager.Current.FreeCamera.transform.localPosition = pos; CameraManager.Current.FreeCamera.transform.localRotation = rot; CameraManager.Current.ControlCamera.fieldOfView = fov; } //ブレンドシェープ同期 - else if (message.address == "/VMC/Ext/Blend/Val" + else if (message.address == "/VMC/Ext/Blend/Val" && ApplyBlendShape && (message.values[0] is string) && (message.values[1] is float) ) @@ -247,7 +450,7 @@ void OnDataReceived(uOSC.Message message) blendShapeBuffer[(string)message.values[0]] = (float)message.values[1]; } //ブレンドシェープ適用 - else if (message.address == "/VMC/Ext/Blend/Apply") + else if (message.address == "/VMC/Ext/Blend/Apply" && ApplyBlendShape) { if (DisableBlendShapeReception == true) { @@ -258,7 +461,7 @@ void OnDataReceived(uOSC.Message message) blendShapeBuffer.Clear(); }//外部アイトラ V2.3 - else if (message.address == "/VMC/Ext/Set/Eye" + else if (message.address == "/VMC/Ext/Set/Eye" && ApplyLookAt && (message.values[0] is int) && (message.values[1] is float) && (message.values[2] is float) @@ -272,80 +475,75 @@ void OnDataReceived(uOSC.Message message) if (enable) { - //ターゲットが存在しなければ作る + //ターゲットが存在しなければ作る(頭ボーン配下のためモデル入れ替え時は一緒に破棄される) if (lookTargetOSC == null) { lookTargetOSC = new GameObject(); lookTargetOSC.name = "lookTargetOSC"; } - //位置を書き込む - if (lookTargetOSC.transform != null) - { - lookTargetOSC.transform.parent = headTransform; - lookTargetOSC.transform.localPosition = pos; - } + //位置を書き込む(頭からの相対位置) + lookTargetOSC.transform.parent = headTransform; + lookTargetOSC.transform.localPosition = pos; - //視線に書き込む - if (vrmLookAtHead != null && setFaceApplyAction == false) + //視線に書き込む(UniVRM10のRuntimeが毎フレームLookAtTargetの方向を目線に反映する) + if (vrm10Instance != null && vrm10Instance.LookAtTarget != lookTargetOSC.transform) { - faceController.BeforeApply += beforeFaceApply; - setFaceApplyAction = true; + vrm10Instance.LookAtTargetType = VRM10ObjectLookAt.LookAtTargetTypes.SpecifiedTransform; + vrm10Instance.LookAtTarget = lookTargetOSC.transform; } } else { - //視線を止める - if (vrmLookAtHead != null && setFaceApplyAction == true) + //視線を止めて正面に戻す + if (vrm10Instance != null) { - faceController.BeforeApply -= beforeFaceApply; - setFaceApplyAction = false; + vrm10Instance.LookAtTarget = null; + vrm10Instance.Runtime.LookAt.SetYawPitchManually(0f, 0f); } } } //情報要求 V2.4 - else if (message.address == "/VMC/Ext/Set/Req") + else if (message.address == "/VMC/Ext/Set/Req" && ApplyControl) { - if (externalSender.isActiveAndEnabled && externalSender.uClient != null) + if (externalSender.isActiveAndEnabled) { externalSender.SendPerLowRate(); //即時送信 } } //情報表示 V2.4 - else if (message.address == "/VMC/Ext/Set/Res" && (message.values[0] is string)) + else if (message.address == "/VMC/Ext/Set/Res" && (message.values[0] is string) && ApplyStatus) { statusString = (string)message.values[0]; } //キャリブレーション準備 V2.5 - else if (message.address == "/VMC/Ext/Set/Calib/Ready") + else if (message.address == "/VMC/Ext/Set/Calib/Ready" && ApplyControl) { if (File.Exists(Settings.Current.VRMPath)) { - window.ImportVRM(Settings.Current.VRMPath, true, true, true); + IKManager.Instance.ModelCalibrationInitialize(); } } //キャリブレーション実行 V2.5 - else if (message.address == "/VMC/Ext/Set/Calib/Exec" && (message.values[0] is int)) + else if (message.address == "/VMC/Ext/Set/Calib/Exec" && (message.values[0] is int) && ApplyControl) { - PipeCommands.CalibrateType calibrateType = PipeCommands.CalibrateType.Default; - - switch ((int)message.values[0]) - { - case 0: - calibrateType = PipeCommands.CalibrateType.Default; - break; - case 1: - calibrateType = PipeCommands.CalibrateType.FixedHand; - break; - case 2: - calibrateType = PipeCommands.CalibrateType.FixedHandWithGround; - break; - default: return; //無視 - } - StartCoroutine(window.Calibrate(calibrateType)); + //仕様の mode(0=通常, 1=MR通常, 2=MR床補正) は PipeCommands.CalibrateType の値と一致している。 + //VMCの拡張として 3=Ipose, 4=Tpose も受け付ける。 + //(/VMC/Ext/OK の calibration mode も同じ値で送信しているので、送受信で一貫する) + var mode = (int)message.values[0]; + if (Enum.IsDefined(typeof(PipeCommands.CalibrateType), mode) == false) return; //未定義は無視 + var calibrateType = (PipeCommands.CalibrateType)mode; + if (calibrateType == PipeCommands.CalibrateType.Invalid) return; + + StartCoroutine(IKManager.Instance.Calibrate(calibrateType)); Invoke("EndCalibrate", 2f); } + //ショートカット呼び出し V3.1 + else if (message.address == "/VMC/Ext/Set/Shortcut" && (message.values[0] is string) && ApplyControl) + { + window.DoShortcutByName((string)message.values[0]); + } //設定読み込み V2.5 - else if (message.address == "/VMC/Ext/Set/Config" && (message.values[0] is string)) + else if (message.address == "/VMC/Ext/Set/Config" && (message.values[0] is string && ApplySetting)) { string path = (string)message.values[0]; if (File.Exists(path)) @@ -355,16 +553,16 @@ void OnDataReceived(uOSC.Message message) } } //スルー情報 V2.6 - else if (message.address != null && message.address.StartsWith("/VMC/Thru/")) + else if (message.address != null && message.address.StartsWith("/VMC/Thru/") && ApplyControl) { - //転送する - if (externalSender.isActiveAndEnabled && externalSender.uClient != null) + //転送する(nullで埋める前の、受け取ったままの引数を送る) + if (externalSender.isActiveAndEnabled) { - externalSender.uClient.Send(message.address, message.values); + externalSender.Send(message.address, originalValues ?? Array.Empty()); } } //Directional Light V2.9 - else if (message.address == "/VMC/Ext/Light" + else if (message.address == "/VMC/Ext/Light" && ApplyLight && (message.values[0] is string) && (message.values[1] is float) && (message.values[2] is float) @@ -396,6 +594,31 @@ void OnDataReceived(uOSC.Message message) window.MainDirectionalLightTransform.rotation = rot; } + //ルートボーン + else 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) + ) + { + 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]; + + HumanBodyBonesTable[boneName] = VirtualAvatar.HumanBodyBonesRoot; + HumanBodyBonesPositionTable[VirtualAvatar.HumanBodyBonesRoot] = pos; + HumanBodyBonesRotationTable[VirtualAvatar.HumanBodyBonesRoot] = rot; + } //ボーン姿勢 else if (message.address == "/VMC/Ext/Bone/Pos" && (message.values[0] is string) @@ -422,32 +645,54 @@ void OnDataReceived(uOSC.Message message) if (HumanBodyBonesTryParse(ref boneName, out bone)) { //あれば位置と回転をキャッシュする - if (HumanBodyBonesPositionTable.ContainsKey(bone)) + HumanBodyBonesPositionTable[bone] = pos; + HumanBodyBonesRotationTable[bone] = rot; + + // 手以外を受信したとき + if (!(bone == HumanBodyBones.LeftHand || + bone == HumanBodyBones.RightHand || + (bone >= HumanBodyBones.LeftThumbProximal && + bone <= HumanBodyBones.RightLittleDistal))) { - HumanBodyBonesPositionTable[bone] = pos; - } - else - { - HumanBodyBonesPositionTable.Add(bone, pos); + enableLocalHandFix = false; + lastBoneReceivedTime = Time.realtimeSinceStartup; } + } - if (HumanBodyBonesRotationTable.ContainsKey(bone)) - { - HumanBodyBonesRotationTable[bone] = rot; - } - else + //受信と更新のタイミングは切り離した + } + + //ボーン姿勢 + else if (message.address == "/VMC/Ext/OK" + && (message.values[0] is int) + ) + { + int loaded = (int)message.values[0]; + //引数の数ではなく型で判定する(不足分はnullで埋められているため) + if (message.values[1] is int && message.values[2] is int) + { + int calibrationState = (int)message.values[1]; + int calibrationMode = (int)message.values[2]; + + if (calibrationState != lastCalibrationState && calibrationState == 3) { - HumanBodyBonesRotationTable.Add(bone, rot); + doCalibration = true; } + lastCalibrationState = calibrationState; } - //受信と更新のタイミングは切り離した + } } } - void EndCalibrate() + /// + /// /VMC/Ext/Set/Calib/Exec 受信時の Invoke("EndCalibrate", 2f) から呼ばれる。 + /// これが無いとキャリブレーションがCalibratingのまま終わらず、 + /// MotionManagerがVRIK以外(VMCProtocol/mocopi/モーション再生)の適用を止め続けてしまう。 + /// + private void EndCalibrate() { - window.EndCalibrate(); + IKManager.Instance.EndCalibrate(); } SteamVR_Utils.RigidTransform SetTransform(ref Vector3 pos, ref Quaternion rot, ref uOSC.Message message) @@ -464,11 +709,23 @@ SteamVR_Utils.RigidTransform SetTransform(ref Vector3 pos, ref Quaternion rot, r public static float filterStrength = 10.0f; - //修正(LateUpdateに変更) private void Update() { + if (receiverSetting == null) return; + + while (MessageBuffer.Count > 0 && MessageBuffer.Peek().timestamp + (float)receiverSetting.DelayMs / 1000f < Time.realtimeSinceStartup) + { + ProcessMessage(MessageBuffer.Dequeue().message); + } + lock (LockObject) { + foreach (var pair in virtualHmd) + { + var newpos = Vector3.Lerp(virtualHmdFiltered[pair.Key].pos, pair.Value.pos, filterStrength * Time.deltaTime); + var newrot = Quaternion.Lerp(virtualHmdFiltered[pair.Key].rot, pair.Value.rot, filterStrength * Time.deltaTime); + virtualHmdFiltered[pair.Key] = new SteamVR_Utils.RigidTransform(newpos, newrot); + } foreach (var pair in virtualController) { var newpos = Vector3.Lerp(virtualControllerFiltered[pair.Key].pos, pair.Value.pos, filterStrength * Time.deltaTime); @@ -494,14 +751,29 @@ private void Update() } } } + + // VRIKのボーン情報を取得するためにLateUpdateを使う private void LateUpdate() { - if (receiveBonesFlag) + if (CorrectRotationWhenCalibration && doCalibration) { - BoneSynchronizeByTable(); + // 現在のアバターの正面方向回転オフセットを取得 + if (animator != null) + { + var hipBone = animator.GetBoneTransform(HumanBodyBones.Hips); + calibrateRotationOffset = Quaternion.Euler(0, hipBone.rotation.eulerAngles.y, 0); + } + } + doCalibration = false; + + BoneSynchronizeByTable(); + if (lastBoneReceivedTime + 5f < Time.realtimeSinceStartup) + { + enableLocalHandFix = true; } } + private bool internalActive = false; public void SetObjectActive(bool enable) @@ -539,7 +811,7 @@ private bool isPortFree(int port) public void ChangeOSCPort(int port) { receivePort = port; - eddp.found = false; + if (eddp != null) eddp.found = false; var uServer = GetComponent(); uServer.enabled = false; @@ -570,74 +842,59 @@ private void BoneSynchronizeByTable() private void BoneSynchronize(HumanBodyBones bone, Vector3 pos, Quaternion rot) { //操作可能な状態かチェック - if (animator != null && bone != HumanBodyBones.LastBone) + if (virtualAvatar != null && animator != null && bone != HumanBodyBones.LastBone) { - Transform targetTransform; + Transform targetTransform = virtualAvatar.GetCloneBoneTransform(bone); Transform tempTransform; //ボーンによって操作を分ける - Transform t = animator.GetBoneTransform(bone); - if (t != null) + if (targetTransform != null) { - //手首ボーン - if (bone == HumanBodyBones.LeftHand || bone == HumanBodyBones.RightHand) + //手首ボーンから先のみ受信した際は + if (receiverSetting.FixHandBone && enableLocalHandFix == true && (bone == HumanBodyBones.LeftHand || bone == HumanBodyBones.RightHand)) { //ローカル座標系の回転打ち消し Quaternion allLocalRotation = Quaternion.identity; - targetTransform = animator.GetBoneTransform(bone); - tempTransform = targetTransform; - while (tempTransform != CurrentModel.transform) + var setRotation = rot; + tempTransform = animator.GetBoneTransform(bone); + var rootTransform = animator.transform; + while (tempTransform != rootTransform) { tempTransform = tempTransform.parent; //後から逆回転をかけて打ち消し allLocalRotation = allLocalRotation * Quaternion.Inverse(tempTransform.localRotation); } + allLocalRotation = allLocalRotation * calibrateRotationOffset; Quaternion receivedRotation = allLocalRotation * rot; //外部からのボーンへの反映 - BoneSynchronizeSingle(t, ref bone, ref pos, ref receivedRotation); + BoneSynchronizeSingle(targetTransform, ref bone, ref pos, ref receivedRotation); } - //指ボーン - else 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) + else { - BoneSynchronizeSingle(t, ref bone, ref pos, ref rot); - } + BoneSynchronizeSingle(targetTransform, ref bone, ref pos, ref rot); + } } } } //1本のボーンの同期 private void BoneSynchronizeSingle(Transform t, ref HumanBodyBones bone, ref Vector3 pos, ref Quaternion rot) { - t.localPosition = pos; - t.localRotation = rot; + if (receiverSetting.UseBonePosition) t.localPosition = pos; + //VMCProtocolの仕様では受信するボーン姿勢はオリジナル(非正規化)。 + //VMC内部は正規化ボーンで統一しているため、ここで正規化ローカル回転へ変換する。 + //(送信元が正規化ボーンを送ってくる場合は UseNormalizedBone を有効にして変換を行わない) + t.localRotation = ConvertReceivedRotation(bone, rot); + virtualAvatar.SetPoseChanged(bone); + } + + private Quaternion ConvertReceivedRotation(HumanBodyBones bone, Quaternion receivedRotation) + { + if (bone == VirtualAvatar.HumanBodyBonesRoot) return receivedRotation; + if (receiverSetting != null && receiverSetting.UseNormalizedBone) return receivedRotation; + + var converter = window != null ? window.BonePostureConverter : null; + if (converter == null || converter.IsIdentity) return receivedRotation; + + return converter.ToNormalizedLocalRotation(bone, receivedRotation); } //ボーンENUM情報をキャッシュして高速化 private bool HumanBodyBonesTryParse(ref string boneName, out HumanBodyBones bone) diff --git a/Assets/Scripts/ExternalSender/ExternalSender.cs b/Assets/Scripts/ExternalSender/ExternalSender.cs index 0ea57f0b..65934582 100644 --- a/Assets/Scripts/ExternalSender/ExternalSender.cs +++ b/Assets/Scripts/ExternalSender/ExternalSender.cs @@ -1,25 +1,27 @@ //gpsnmeajp -using RootMotion.FinalIK; using sh_akira; using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using UnityEngine; -using UnityMemoryMappedFile; -using VRM; +using UniVRM10; +using uOSC; namespace VMC { - [RequireComponent(typeof(uOSC.uOscClient))] public class ExternalSender : MonoBehaviour { - public uOSC.uOscClient uClient = null; + public List uClients = new List(); GameObject CurrentModel = null; ControlWPFWindow window = null; Animator animator = null; - VRIK vrik = null; - VRMBlendShapeProxy blendShapeProxy = null; + //VRMBlendShapeProxy blendShapeProxy = null; + Vrm10RuntimeExpression vrm10RuntimeExpression = null; + //オリジナル(非正規化)ボーン姿勢の取得用。ControlRig生成時、animatorは正規化ボーンを返すため + Vrm10Instance vrm10Instance = null; Camera currentCamera = null; - VRMData vrmdata = null; + UnityMemoryMappedFile.VRMData vrmdata = null; string remoteName = null; string remoteJson = null; @@ -54,7 +56,6 @@ public class ExternalSender : MonoBehaviour void Start() { - uClient = GetComponent(); window = GameObject.Find("ControlWPFWindow").GetComponent(); handTrackerRoot = GameObject.Find("HandTrackerRoot"); @@ -64,8 +65,9 @@ void Start() { this.CurrentModel = CurrentModel; animator = CurrentModel.GetComponent(); - vrik = CurrentModel.GetComponent(); - blendShapeProxy = CurrentModel.GetComponent(); + //モデル入れ替え時に古いモデルのExpressionを参照し続けないように更新する + vrm10Instance = CurrentModel.GetComponent(); + vrm10RuntimeExpression = vrm10Instance != null ? vrm10Instance.Runtime.Expression : null; } }; @@ -74,7 +76,7 @@ void Start() this.currentCamera = currentCamera; }; - window.VRMmetaLodedAction += (VRMData vrmdata) => + window.VRMmetaLoadedAction += (UnityMemoryMappedFile.VRMData vrmdata) => { this.vrmdata = vrmdata; this.remoteName = null; @@ -110,7 +112,7 @@ void Start() //Debug.Log("Ext: ConDown"); try { - uClient?.Send("/VMC/Ext/Con", 1, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); + Send("/VMC/Ext/Con", 1, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); } catch (Exception ex) { @@ -126,7 +128,7 @@ void Start() //Debug.Log("Ext: ConUp"); try { - uClient?.Send("/VMC/Ext/Con", 0, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); + Send("/VMC/Ext/Con", 0, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); } catch (Exception ex) { @@ -144,7 +146,7 @@ void Start() { if (e.IsAxis) { - uClient?.Send("/VMC/Ext/Con", 2, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); + Send("/VMC/Ext/Con", 2, e.Name, e.IsLeft ? 1 : 0, e.IsTouch ? 1 : 0, e.IsAxis ? 1 : 0, e.Axis.x, e.Axis.y, e.Axis.z); } } catch (Exception ex) @@ -161,7 +163,7 @@ void Start() //Debug.Log("Ext: KeyDown"); try { - uClient?.Send("/VMC/Ext/Key", 1, e.KeyName, e.KeyCode); + Send("/VMC/Ext/Key", 1, e.KeyName, e.KeyCode); } catch (Exception ex) { @@ -176,7 +178,7 @@ void Start() //Debug.Log("Ext: KeyUp"); try { - uClient?.Send("/VMC/Ext/Key", 0, e.KeyName, e.KeyCode); + Send("/VMC/Ext/Key", 0, e.KeyName, e.KeyCode); } catch (Exception ex) { @@ -185,14 +187,14 @@ void Start() } }; - midiCCWrapper.noteOnDelegateProxy += (MidiJack.MidiChannel channel, int note, float velocity) => + midiCCWrapper.noteOnDelegateProxy += (MidiChannel channel, int note, float velocity) => { if (this.isActiveAndEnabled) { //Debug.Log("Ext: KeyDown"); try { - uClient?.Send("/VMC/Ext/Midi/Note", 1, (int)channel, note, velocity); + Send("/VMC/Ext/Midi/Note", 1, (int)channel, note, velocity); } catch (Exception ex) { @@ -200,14 +202,14 @@ void Start() } } }; - midiCCWrapper.noteOffDelegateProxy += (MidiJack.MidiChannel channel, int note) => + midiCCWrapper.noteOffDelegateProxy += (MidiChannel channel, int note) => { if (this.isActiveAndEnabled) { //Debug.Log("Ext: KeyDown"); try { - uClient?.Send("/VMC/Ext/Midi/Note", 0, (int)channel, note, (float)0f); + Send("/VMC/Ext/Midi/Note", 0, (int)channel, note, (float)0f); } catch (Exception ex) { @@ -222,7 +224,7 @@ void Start() //Debug.Log("Ext: KeyDown"); try { - uClient?.Send("/VMC/Ext/Midi/CC/Val", knobNo, value); + Send("/VMC/Ext/Midi/CC/Val", knobNo, value); } catch (Exception ex) { @@ -237,7 +239,7 @@ void Start() //Debug.Log("Ext: KeyDown"); try { - uClient?.Send("/VMC/Ext/Midi/CC/Bit", knobNo, (int)(value ? 1 : 0)); + Send("/VMC/Ext/Midi/CC/Bit", knobNo, (int)(value ? 1 : 0)); } catch (Exception ex) { @@ -247,10 +249,9 @@ void Start() }; this.gameObject.SetActive(false); - uClient.enabled = true; } - // Update is called once per frame - void Update() + + private void LateUpdate() { //基本的に毎フレーム送信するもの SendPerFrame(); @@ -272,6 +273,33 @@ void Update() } + public void Send(string address, params object[] values) + { + SendHook?.Invoke(new Message(address, values)); + foreach (var uClient in uClients) + { + uClient?.Send(address, values); + } + } + + public void Send(Message message) + { + SendHook?.Invoke(message); + foreach (var uClient in uClients) + { + uClient?.Send(message); + } + } + + public void Send(Bundle bundle) + { + SendHook?.Invoke(bundle); + foreach (var uClient in uClients) + { + uClient?.Send(bundle); + } + } + //低頻度(1秒以上)で送信する情報もの。ただし送信要求が来たら即時発信する public void SendPerLowRate() { @@ -282,8 +310,14 @@ public void SendPerLowRate() uOSC.Bundle infoBundle = new uOSC.Bundle(uOSC.Timestamp.Immediate); //受信有効情報(Receive enable) //有効可否と、ポート番号の送信 - infoBundle.Add(new uOSC.Message("/VMC/Ext/Rcv", (int)(externalReceiver.isActiveAndEnabled ? 1 : 0), externalReceiver.receivePort)); - + if (externalReceiver != null) + { + //V2.7: (int)enable (int)port (string)IP Address + infoBundle.Add(new uOSC.Message("/VMC/Ext/Rcv", + (int)(externalReceiver.isActiveAndEnabled ? 1 : 0), + externalReceiver.receivePort, + GetLocalIPAddress())); + } //【イベント送信】DirectionalLight位置・色(DirectionalLight transform & color) if ((window.MainDirectionalLightTransform != null) && (window.MainDirectionalLight.color != null)) @@ -310,28 +344,30 @@ public void SendPerLowRate() )); //送信 - uClient?.Send(infoBundle); + Send(infoBundle); //【イベント送信】VRM基本情報(VRM information) [独立送信](大きいため単独で送る) if (vrmdata != null) { - //ファイルパス, キャラ名 - uClient?.Send(new uOSC.Message("/VMC/Ext/VRM", vrmdata.FilePath, vrmdata.Title)); + //V2.7: (string)path (string)title (string)Hash + //Hashはモデルの同一性を判別するためのもの。VMCではファイル内容のSHA-256(16進小文字)を使う + Send(new uOSC.Message("/VMC/Ext/VRM", vrmdata.FilePath, vrmdata.Title, + window != null ? window.CurrentVRMHash ?? "" : "")); } else if (string.IsNullOrEmpty(remoteName) == false) { - uClient?.Send(new uOSC.Message("/VMC/Ext/Remote", remoteName, remoteJson)); + Send(new uOSC.Message("/VMC/Ext/Remote", remoteName, remoteJson)); } //【イベント送信】設定ファイルパス(Loaded config path) [独立送信](大きいため単独で送る) if (window != null) { //ファイルパス, キャラ名 - uClient?.Send(new uOSC.Message("/VMC/Ext/Config", window.lastLoadedConfigPath)); + Send(new uOSC.Message("/VMC/Ext/Config", window.lastLoadedConfigPath)); } //【イベント送信】Option文字列(Option string) [独立送信](大きいため単独で送る) - uClient?.Send(new uOSC.Message("/VMC/Ext/Opt", optionString)); + Send(new uOSC.Message("/VMC/Ext/Opt", optionString)); } } @@ -342,19 +378,12 @@ void SendPerFrame() if (CurrentModel != null && animator != null) { - //Root - if (vrik == null) - { - vrik = CurrentModel.GetComponent(); - Debug.Log("ExternalSender: VRIK Updated"); - } - if (frameOfRoot > periodRoot && periodRoot != 0) { frameOfRoot = 1; - if (vrik != null) + if (animator != null) { - var RootTransform = vrik.references.root; + var RootTransform = animator.transform; var offset = handTrackerRoot.transform; if (RootTransform != null && offset != null) { @@ -377,12 +406,21 @@ void SendPerFrame() uOSC.Bundle boneBundle = new uOSC.Bundle(uOSC.Timestamp.Immediate); int cnt = 0;//パケット分割カウンタ + //VMCProtocolの仕様では、送信するボーン姿勢はControlRigが適用されていない + //オリジナル(非正規化)ボーンが推奨。正規化ボーンの送信は既定で無効のオプション。 + //ControlRig生成時、animator.GetBoneTransform は正規化ボーンを返すので、 + //オリジナルを送るときは Vrm10Instance.Humanoid.GetBoneTransform を使う。 + var useNormalizedBone = Settings.Current.ExternalMotionSenderUseNormalizedBone; + var humanoid = vrm10Instance != null ? vrm10Instance.Humanoid : null; + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) { if (bone == HumanBodyBones.LastBone) { continue; } - var Transform = animator.GetBoneTransform(bone); + var Transform = useNormalizedBone || humanoid == null + ? animator.GetBoneTransform(bone) + : (humanoid.GetBoneTransform(bone) ?? animator.GetBoneTransform(bone)); if (Transform != null) { boneBundle.Add(new uOSC.Message("/VMC/Ext/Bone/Pos", @@ -394,7 +432,7 @@ void SendPerFrame() //1200バイトを超えない程度に分割する if (cnt > PACKET_DIV_BONE) { - uClient?.Send(boneBundle); + Send(boneBundle); boneBundle = new uOSC.Bundle(uOSC.Timestamp.Immediate); cnt = 0; @@ -407,10 +445,14 @@ void SendPerFrame() frameOfBone++; //Blendsharp - if (blendShapeProxy == null) + if (vrm10RuntimeExpression == null) { - blendShapeProxy = CurrentModel.GetComponent(); - Debug.Log("ExternalSender: VRMBlendShapeProxy Updated"); + vrm10Instance = CurrentModel.GetComponent(); + if (vrm10Instance != null) + { + vrm10RuntimeExpression = vrm10Instance.Runtime.Expression; + Debug.Log("ExternalSender: Vrm10RuntimeExpression Updated"); + } } if (frameOfBlendShape > periodBlendShape && periodBlendShape != 0) @@ -418,18 +460,36 @@ void SendPerFrame() frameOfBlendShape = 1; uOSC.Bundle blendShapeBundle = new uOSC.Bundle(uOSC.Timestamp.Immediate); - if (blendShapeProxy != null) + if (vrm10RuntimeExpression != null) { - foreach (var b in blendShapeProxy.GetValues()) + //VMCProtocolの仕様上、VRM1.0モデル使用時もプリセット表情はVRM0.xの名称での送信が必須。 + //VRM1.0形式(happy/aa等)での送信はオプション(既定は無効)。 + //(LookAt等の適用後の値を送るためActualWeightsを使用) + var sendVRM1 = Settings.Current.ExternalMotionSenderSendVRM1Expression; + foreach (var b in vrm10RuntimeExpression.ActualWeights) { + var vrm0Name = VRM10CompatibleNames.GetVRM0CompatibleName(b.Key); blendShapeBundle.Add(new uOSC.Message("/VMC/Ext/Blend/Val", - b.Key.ToString(), + vrm0Name, (float)b.Value )); + + if (sendVRM1) + { + //VRM1.0名がVRM0.x名と異なる場合のみ追加で送る(同名の重複送信を避ける) + var vrm1Name = b.Key.Name; + if (string.IsNullOrEmpty(vrm1Name) == false && vrm1Name != vrm0Name) + { + blendShapeBundle.Add(new uOSC.Message("/VMC/Ext/Blend/Val", + vrm1Name, + (float)b.Value + )); + } + } } blendShapeBundle.Add(new uOSC.Message("/VMC/Ext/Blend/Apply")); } - uClient?.Send(blendShapeBundle); + Send(blendShapeBundle); } frameOfBlendShape++; } @@ -438,12 +498,24 @@ void SendPerFrame() if (frameOfCamera > periodCamera && periodCamera != 0) { frameOfCamera = 1; + //OnCameraChangedはCameraManager.Start()でも発火するが、Start()同士の実行順は不定のため + //購読より先に発火するとカメラを知らないままになり、/VMC/Ext/Camが一切送信されなくなる。 + //(その後の再発火はChangeCamera経由のみで、Settings.CameraTypeが未設定だと呼ばれない) + if (currentCamera == null && CameraManager.Current != null) + { + currentCamera = CameraManager.Current.ControlCamera; + } if (currentCamera != null) { + //カメラはHandTrackerRootの子で、この親はキャリブレーションで身長比のスケールと + //オフセットを持つ。受信側(ExternalReceiverForVMC)はカメラ姿勢をlocalPosition/ + //localRotationとして適用し、受信側アバターのスケールへ写像するため、 + //送信もローカル座標に揃える。(ワールドで送るとVMC同士でスケールが二重に掛かる) + var cameraTransform = currentCamera.transform; rootBundle.Add(new uOSC.Message("/VMC/Ext/Cam", "Camera", - currentCamera.transform.position.x, currentCamera.transform.position.y, currentCamera.transform.position.z, - currentCamera.transform.rotation.x, currentCamera.transform.rotation.y, currentCamera.transform.rotation.z, currentCamera.transform.rotation.w, + cameraTransform.localPosition.x, cameraTransform.localPosition.y, cameraTransform.localPosition.z, + cameraTransform.localRotation.x, cameraTransform.localRotation.y, cameraTransform.localRotation.z, cameraTransform.localRotation.w, currentCamera.fieldOfView)); } } @@ -504,29 +576,124 @@ void SendPerFrame() } if (window != null) { - rootBundle.Add(new uOSC.Message("/VMC/Ext/OK", (int)available, (int)window.calibrationState, (int)window.lastCalibrateType)); + //V2.7: (int)loaded (int)calibration state (int)calibration mode (int)tracking status + //tracking status は 正常=1 / 不可=0 + rootBundle.Add(new uOSC.Message("/VMC/Ext/OK", + (int)available, + (int)IKManager.Instance.CalibrationState, + (int)IKManager.Instance.LastCalibrateType, + (int)(IsTrackingOK() ? 1 : 0))); } rootBundle.Add(new uOSC.Message("/VMC/Ext/T", Time.time)); } frameOfStatus++; - uClient?.Send(rootBundle); + Send(rootBundle); //---End of frame--- } + /// + /// トラッキング状態(/VMC/Ext/OK の tracking status)。 + /// 割り当てられている機器のいずれかがロストしていたら不可(0)とする。 + /// + private bool IsTrackingOK() + { + if (TrackingPointManager.Instance == null) return false; + var any = false; + foreach (var trackingPoint in TrackingPointManager.Instance.GetTrackingPoints()) + { + any = true; + if (trackingPoint.TrackingWatcher != null && trackingPoint.TrackingWatcher.ok == false) return false; + } + return any; + } + + /// 受信ポートを知らせるためのローカルIPアドレス(/VMC/Ext/Rcv 用) + private static string localIPAddress = null; + private static bool localIPAddressResolving = false; + + /// + /// Dns.GetHostEntry はネットワーク状況によって数秒ブロックすることがあるため、 + /// メインスレッドでは実行せずバックグラウンドで一度だけ解決する。 + /// 解決するまでは空文字を返す(低頻度送信なので次回以降の送信に載る)。 + /// + private static string GetLocalIPAddress() + { + if (localIPAddress != null) return localIPAddress; + if (localIPAddressResolving) return ""; + + localIPAddressResolving = true; + System.Threading.Tasks.Task.Run(() => + { + var resolved = ""; + try + { + var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()); + foreach (var address in host.AddressList) + { + if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + resolved = address.ToString(); + break; + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to get local IP address: {ex.Message}"); + } + localIPAddress = resolved; + }); + return ""; + } + public void ChangeOSCAddress(string address, int port) { - if (uClient == null) uClient = GetComponent(); - uClient.enabled = false; - var type = typeof(uOSC.uOscClient); - var addressfield = type.GetField("address", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance); - addressfield.SetValue(uClient, address); - var portfield = type.GetField("port", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance); - portfield.SetValue(uClient, port); - uClient.enabled = true; + var addresses = address.Split(',').Select(d => d.Trim()).ToList(); + + if (uClients.Count != addresses.Count) + { + foreach(var uClient in uClients) + { + DestroyImmediate(uClient.gameObject); + } + + uClients.Clear(); + + foreach(var addr in addresses) + { + var newobject = new GameObject("uOscClient"); + newobject.transform.SetParent(transform, false); + uClients.Add(newobject.AddComponent()); + } + } + + for (int i = 0; i < uClients.Count; i++) + { + var uClient = uClients[i]; + var addr = addresses[i]; + + uClient.enabled = false; + var type = typeof(uOSC.uOscClient); + var addressfield = type.GetField("address", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance); + addressfield.SetValue(uClient, addr); + var portfield = type.GetField("port", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance); + portfield.SetValue(uClient, port); + uClient.enabled = true; + } } + + #region 自動テスト用フック + + /// + /// 送信内容のキャプチャ用フック(通常の動作では誰も購読していない) + /// 引数はuOSC.MessageまたはuOSC.Bundle + /// + public static event Action SendHook; + + #endregion } [Serializable] diff --git a/Assets/Scripts/ExternalSender/MidiCCWrapper.cs b/Assets/Scripts/ExternalSender/MidiCCWrapper.cs index 9a5ec5d3..187ac024 100644 --- a/Assets/Scripts/ExternalSender/MidiCCWrapper.cs +++ b/Assets/Scripts/ExternalSender/MidiCCWrapper.cs @@ -1,18 +1,51 @@ -//gpsnmeajp +//gpsnmeajp using System; using UnityEngine; +using UnityEngine.InputSystem; +using Minis; namespace VMC { + /// + /// MIDIチャンネル(0-15)。従来のMidiJack.MidiChannelと同じ値・並び(Ch1=0 ... Ch16=15, All=16)。 + /// MidiJackからMinisへ移行した後も、下流(InputManager/ExternalSender)のシグネチャを維持するために定義。 + /// + public enum MidiChannel + { + Ch1, // 0 + Ch2, // 1 + Ch3, + Ch4, + Ch5, + Ch6, + Ch7, + Ch8, + Ch9, + Ch10, + Ch11, + Ch12, + Ch13, + Ch14, + Ch15, + Ch16, + All // 16 + } + + /// + /// MIDI入力の集約ラッパー。 + /// 旧MidiJackは毎フレームMIDIポートへ再接続する実装で、新しいWindows MIDIサービス上ではフリーズの原因になるため、 + /// Unity Input System上に構築された後継のMinis(デバイス数変化時のみ接続)へ移行した。 + /// MidiJackのAPI(MidiMaster.*Delegate)に触れていたのは本クラスのみで、外部インターフェースは従来通り。 + /// public class MidiCCWrapper : MonoBehaviour { public const int KNOBS = 128; //最大ノブ数 public const float Threshold = 0.5f; //bool判定しきい値 - //MIDIJack集約用デリゲートプロキシ(入力を即時通知する) - public Action noteOnDelegateProxy = null; - public Action noteOffDelegateProxy = null; - public Action knobDelegateProxy = null; + //集約用デリゲートプロキシ(入力を即時通知する) + public Action noteOnDelegateProxy = null; + public Action noteOffDelegateProxy = null; + public Action knobDelegateProxy = null; //フレーム単位にまるめて変化を通知するデリゲート public Action knobUpdateFloatDelegate = null; @@ -28,38 +61,60 @@ public class MidiCCWrapper : MonoBehaviour void Start() { - MidiJack.MidiMaster.noteOnDelegate += (MidiJack.MidiChannel channel, int note, float velocity) => + //既に接続済みのMIDIデバイスをフック + foreach (var device in InputSystem.devices) { - if (velocity != 0) + TryHookDevice(device); + } + //以降に接続されるMIDIデバイスをフック(Minisはデバイス数変化時のみ接続する) + InputSystem.onDeviceChange += OnDeviceChange; + } + + void OnDestroy() + { + InputSystem.onDeviceChange -= OnDeviceChange; + } + + private void OnDeviceChange(UnityEngine.InputSystem.InputDevice device, UnityEngine.InputSystem.InputDeviceChange change) + { + if (change == UnityEngine.InputSystem.InputDeviceChange.Added) + { + TryHookDevice(device); + } + } + + private void TryHookDevice(UnityEngine.InputSystem.InputDevice device) + { + if (device is MidiDevice midi == false) return; + + //MinisのMidiDeviceは1チャンネル=1デバイス。チャンネルは固定なのでフック時に確定させる + var channel = (MidiChannel)midi.channel; + + midi.onWillNoteOn += (MidiNoteControl note, float velocity) => + { + //velocity 0 のNoteOnはNoteOff扱い(MIDI慣習。旧MidiJack実装と同じ挙動) + if (velocity != 0f) { - if (noteOnDelegateProxy != null) - { - noteOnDelegateProxy.Invoke(channel, note, velocity); - } + noteOnDelegateProxy?.Invoke(channel, note.noteNumber, velocity); } else { - if (noteOffDelegateProxy != null) - { - noteOffDelegateProxy.Invoke(channel, note); - } + noteOffDelegateProxy?.Invoke(channel, note.noteNumber); } }; - MidiJack.MidiMaster.noteOffDelegate += (MidiJack.MidiChannel channel, int note) => + + midi.onWillNoteOff += (MidiNoteControl note) => { - if (noteOffDelegateProxy != null) - { - noteOffDelegateProxy.Invoke(channel, note); - } + noteOffDelegateProxy?.Invoke(channel, note.noteNumber); }; - MidiJack.MidiMaster.knobDelegate += (MidiJack.MidiChannel channel, int knobNo, float value) => + midi.onWillControlChange += (MidiValueControl control, float value) => { - KnobUpdated(channel, knobNo, value); + KnobUpdated(channel, control.controlNumber, value); }; } - public void KnobUpdated(MidiJack.MidiChannel channel, int knobNo, float value) + public void KnobUpdated(MidiChannel channel, int knobNo, float value) { if (knobDelegateProxy != null) { @@ -128,4 +183,4 @@ void Update() } } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Input/InputManager.cs b/Assets/Scripts/Input/InputManager.cs index b686522c..d9f60ebc 100644 --- a/Assets/Scripts/Input/InputManager.cs +++ b/Assets/Scripts/Input/InputManager.cs @@ -67,7 +67,7 @@ private void Start() }; midiCCWrapper.knobUpdateBoolDelegate += async (int knobNo, bool value) => { - MidiJack.MidiChannel channel = MidiJack.MidiChannel.Ch1; //仮でCh1 + MidiChannel channel = MidiChannel.Ch1; //仮でCh1 Debug.Log("MidiCC:" + channel + "/" + knobNo + "/" + value); var config = new KeyConfig(); @@ -88,7 +88,7 @@ private void Start() if (!doKeyConfig) CheckKey(config, value); }; - midiCCWrapper.knobDelegateProxy += (MidiJack.MidiChannel channel, int knobNo, float value) => + midiCCWrapper.knobDelegateProxy += (MidiChannel channel, int knobNo, float value) => { CheckKnobUpdated(channel, knobNo, value); }; @@ -147,14 +147,14 @@ private void Server_Received(object sender, DataReceivedEventArgs e) }, null); } - private string MidiName(MidiJack.MidiChannel channel, int note) + private string MidiName(MidiChannel channel, int note) { return $"MIDI Ch{(int)channel + 1} {note}"; } private float[] lastKnobUpdatedSendTime = new float[MidiCCWrapper.KNOBS]; - private async void CheckKnobUpdated(MidiJack.MidiChannel channel, int knobNo, float value) + private async void CheckKnobUpdated(MidiChannel channel, int knobNo, float value) { if (doKeySend == false) return; if (lastKnobUpdatedSendTime[knobNo] + 3f < Time.realtimeSinceStartup) @@ -182,16 +182,32 @@ private async void ControllerAction_KeyDown(object sender, OVRKeyEventArgs e) config.isTouch = e.IsTouch; if (e.IsAxis) { - if (config.keyIndex < 0) return; + //if (config.keyIndex < 0) return; スティック真ん中をタッチしたときは反応させることにしたのでコメントアウト if (e.IsLeft) { - if (isStick) lastStickLeftAxisPoint = config.keyIndex; - else lastTouchpadLeftAxisPoint = config.keyIndex; + if (isStick) + { + lastStickLeftAxisPoint = config.keyIndex; + isStickLeftTouchDown = true; + } + else + { + lastTouchpadLeftAxisPoint = config.keyIndex; + isTouchpadLeftTouchDown = true; + } } else { - if (isStick) lastStickRightAxisPoint = config.keyIndex; - else lastTouchpadRightAxisPoint = config.keyIndex; + if (isStick) + { + lastStickRightAxisPoint = config.keyIndex; + isStickRightTouchDown = true; + } + else + { + lastTouchpadRightAxisPoint = config.keyIndex; + isTouchpadRightTouchDown = true; + } } } if (doKeyConfig || doKeySend) await controlWPFWindow.server.SendCommandAsync(new PipeCommands.KeyDown { Config = config }); @@ -218,29 +234,48 @@ private async void ControllerAction_KeyUp(object sender, OVRKeyEventArgs e) if (doKeyConfig) { }// await server.SendCommandAsync(new PipeCommands.KeyUp { Config = config }); else CheckKey(config, false); config.keyIndex = newindex; - if (config.keyIndex < 0) return; + //if (config.keyIndex < 0) return; スティック真ん中をタッチしたときは反応させることにしたのでコメントアウト //新しいキーを押す if (doKeyConfig) await controlWPFWindow.server.SendCommandAsync(new PipeCommands.KeyDown { Config = config }); else CheckKey(config, true); } if (doKeyConfig || doKeySend) { }// await server.SendCommandAsync(new PipeCommands.KeyUp { Config = config }); if (!doKeyConfig) CheckKey(config, false); + if (e.IsAxis) + { + if (isStick) + { + if (e.IsLeft) isStickLeftTouchDown = false; + else isStickRightTouchDown = false; + } + else + { + if (e.IsLeft) isTouchpadLeftTouchDown = false; + else isTouchpadRightTouchDown = false; + } + } } private int lastTouchpadLeftAxisPoint = -1; private int lastTouchpadRightAxisPoint = -1; private int lastStickLeftAxisPoint = -1; private int lastStickRightAxisPoint = -1; + private bool isStickLeftTouchDown = false; + private bool isStickRightTouchDown = false; + private bool isTouchpadLeftTouchDown = false; + private bool isTouchpadRightTouchDown = false; private bool isSendingKey = false; //タッチパッドやアナログスティックの変動 private async void ControllerAction_AxisChanged(object sender, OVRKeyEventArgs e) { if (e.IsAxis == false) return; + //if (e.Axis == Vector3.zero) return; var keyName = e.Name; if (keyName.Contains("Trigger")) return; //トリガーは現時点ではアナログ入力無効 if (keyName.Contains("Position")) keyName = keyName.Replace("Position", "Touch"); //ポジションはいったんタッチと同じにする bool isStick = keyName.Contains("Stick"); + //Debug.Log($"ControllerAction_AxisChanged[{e.Name}] IsLeft:{e.IsLeft} isStick:{isStick} Axis:({e.Axis.x}, {e.Axis.y}, {e.Axis.z})"); var newindex = NearestPointIndex(e.IsLeft, e.Axis.x, e.Axis.y, isStick); if ((isStick ? (e.IsLeft ? lastStickLeftAxisPoint : lastStickRightAxisPoint) : (e.IsLeft ? lastTouchpadLeftAxisPoint : lastTouchpadRightAxisPoint)) != newindex) {//ドラッグで隣の領域に入った場合 @@ -254,7 +289,10 @@ private async void ControllerAction_AxisChanged(object sender, OVRKeyEventArgs e config.isTouch = true;// e.IsTouch; //ポジションはいったんタッチと同じにする //前のキーを離す if (doKeyConfig || doKeySend) { }// await server.SendCommandAsync(new PipeCommands.KeyUp { Config = config }); - if (!doKeyConfig) CheckKey(config, false); + if (isStick || ((e.IsLeft && isTouchpadLeftTouchDown) || (e.IsLeft == false && isTouchpadRightTouchDown))) + { + if (!doKeyConfig) CheckKey(config, false); + } config.keyIndex = newindex; //新しいキーを押す if (doKeyConfig || doKeySend) @@ -266,7 +304,14 @@ private async void ControllerAction_AxisChanged(object sender, OVRKeyEventArgs e isSendingKey = false; } } - if (!doKeyConfig) CheckKey(config, true); + if (newindex == -1 && isStick && ((e.IsLeft && isStickLeftTouchDown == false) || (e.IsLeft == false && isStickRightTouchDown == false))) + { + // スティックにタッチしておらず、センターに戻った時は何もしない + } + else if (isStick || ((e.IsLeft && isTouchpadLeftTouchDown) || (e.IsLeft == false && isTouchpadRightTouchDown))) + { //スティックか、タッチパッドに触れたまま移動したとき反応する(Axisが先に来るため触れた瞬間は反応しない) + if (!doKeyConfig) CheckKey(config, true); + } if (e.IsLeft) { if (isStick) lastStickLeftAxisPoint = newindex; @@ -418,7 +463,7 @@ private void CheckKey(KeyConfig config, bool isKeyDown) var doKeyActions = new List(); //手の操作時は左手と右手は分けて処理しないと、右がおしっぱで左を離したときに戻らなくなる - foreach (var downaction in Settings.Current.KeyActions?.OrderBy(d => d.KeyConfigs.Count()).Where(d => d.FaceAction == action.FaceAction && d.HandAction == action.HandAction && d.Hand == action.Hand && d.FunctionAction == action.FunctionAction)) + foreach (var downaction in Settings.Current.KeyActions?.OrderBy(d => d.KeyConfigs.Count()).Where(d => d.FaceAction == action.FaceAction && d.HandAction == action.HandAction && d.Hand == action.Hand && d.FunctionAction == action.FunctionAction && d.MotionAction == action.MotionAction)) {//キーの少ない順に実行して、同時押しと被ったとき同時押しを後から実行して上書きさせる //if (action.KeyConfigs.Count == CurrentKeyConfigs.Count) //{ //別々の機能を同時に押す場合もあるのでキーの数は見てはいけない diff --git a/Assets/ExternalPlugins/DVRSDK/DVRAvatar.meta b/Assets/Scripts/MotionPlayback.meta similarity index 77% rename from Assets/ExternalPlugins/DVRSDK/DVRAvatar.meta rename to Assets/Scripts/MotionPlayback.meta index 7b5b4879..c79a2621 100644 --- a/Assets/ExternalPlugins/DVRSDK/DVRAvatar.meta +++ b/Assets/Scripts/MotionPlayback.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 00a67b2f71730ff4fa3c37915e15435c +guid: 2d879886dcbc109438b2232990230275 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Scripts/MotionPlayback/BvhWriter.cs b/Assets/Scripts/MotionPlayback/BvhWriter.cs new file mode 100644 index 00000000..48caf166 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/BvhWriter.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace VMC +{ + /// + /// BVHファイルの書き出し(UniVRM/UniHumanoidにBVHエクスポータが無いため自前実装) + /// ボーン構成はVRMのHumanoidボーンを使用する + /// UniHumanoidのBvhImporterContextで読み込んだ際に元のモーションに戻るよう、 + /// インポータの逆変換(X反転・チャンネル順 Yrotation Xrotation Zrotation)で書き出す + /// + public class BvhWriter + { + //単位はセンチメートル(BVHの慣例) + private const float Scale = 100f; + + private class BvhJoint + { + public HumanBodyBones Bone; + public Transform Transform; + public BvhJoint Parent; + public List Children = new List(); + public Vector3 Offset; //レストポーズでの親ジョイントからの相対位置 + public Quaternion RestWorldRotation; //レスト(バインド)ポーズでのワールド回転 + public Quaternion CurrentDelta; //AddFrame内でのレストからのワールド差分回転(親の計算に使用) + } + + private readonly Animator animator; + private readonly Transform root; + private BvhJoint hipsJoint; + private readonly List jointOrder = new List(); + private readonly List frames = new List(); + + /// + /// animator: T-Poseのヒューマノイド(レストポーズがT-Poseであること) + /// + public BvhWriter(Animator animator, Transform root) + { + this.animator = animator; + this.root = root; + BuildHierarchy(); + } + + private void BuildHierarchy() + { + var boneMap = new Dictionary(); + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + var t = animator.GetBoneTransform(bone); + if (t == null) continue; + boneMap[bone] = new BvhJoint { Bone = bone, Transform = t }; + } + + //Humanoidの親子関係を構築(存在しない中間ボーンはスキップして最も近い祖先に繋ぐ) + foreach (var kv in boneMap) + { + if (kv.Key == HumanBodyBones.Hips) continue; + var parentJoint = FindParentJoint(kv.Key, boneMap); + if (parentJoint == null) continue; + kv.Value.Parent = parentJoint; + parentJoint.Children.Add(kv.Value); + } + + hipsJoint = boneMap[HumanBodyBones.Hips]; + hipsJoint.Offset = root.InverseTransformPoint(hipsJoint.Transform.position); + + //オフセット計算とジョイント順(Traverse順=チャンネル順)確定 + jointOrder.Clear(); + TraverseJoint(hipsJoint); + + //レスト(バインド)ポーズでのワールド回転を記録する。 + //BvhWriterはバインドポーズの状態で構築される前提(呼び出し側でRestoreBindPose済み) + foreach (var joint in jointOrder) + { + joint.RestWorldRotation = joint.Transform.rotation; + } + } + + private void TraverseJoint(BvhJoint joint) + { + jointOrder.Add(joint); + foreach (var child in joint.Children) + { + child.Offset = child.Transform.position - joint.Transform.position; + TraverseJoint(child); + } + } + + private BvhJoint FindParentJoint(HumanBodyBones bone, Dictionary boneMap) + { + var parentIndex = HumanTrait.GetParentBone((int)bone); + while (parentIndex >= 0) + { + var parentBone = (HumanBodyBones)parentIndex; + if (boneMap.TryGetValue(parentBone, out var joint)) return joint; + parentIndex = HumanTrait.GetParentBone(parentIndex); + } + return null; + } + + /// + /// 現在のスケルトンのポーズを1フレームとして記録する + /// + public void AddFrame() + { + var values = new List(); + foreach (var joint in jointOrder) + { + if (joint == hipsJoint) + { + //ROOTは位置チャンネル(ルート相対、X反転、cm) + var p = root.InverseTransformPoint(joint.Transform.position) * Scale; + values.Add(-p.x); + values.Add(p.y); + values.Add(p.z); + } + + //BVHはレスト回転が単位(identity)である前提のため、絶対ローカル回転ではなく + //「レストポーズからのワールド差分回転」を親ジョイント相対で書き出す。 + //D_j = W_j(f) * W_j(rest)^-1、L_j = D_parent^-1 * D_j + //(こうするとインポート時に各ボーンのレスト基準回転がクローンと一致し、姿勢が崩れない) + var delta = joint.Transform.rotation * Quaternion.Inverse(joint.RestWorldRotation); + joint.CurrentDelta = delta; + var parentDelta = joint.Parent != null ? joint.Parent.CurrentDelta : Quaternion.identity; + var q = Quaternion.Inverse(parentDelta) * delta; + + //X反転(インポータのReverseX()の逆変換 = 同じ変換) + q.ToAngleAxis(out var angle, out var axis); + var bvhRotation = float.IsNaN(axis.x) ? Quaternion.identity : Quaternion.AngleAxis(-angle, new Vector3(-axis.x, axis.y, axis.z)); + + //チャンネル順 Yrotation Xrotation Zrotation は + //Ry*Rx*Rz = Quaternion.Euler(x,y,z) と等価のため、eulerAnglesでそのまま分解できる + var euler = bvhRotation.eulerAngles; + values.Add(NormalizeAngle(euler.y)); + values.Add(NormalizeAngle(euler.x)); + values.Add(NormalizeAngle(euler.z)); + } + frames.Add(values.ToArray()); + } + + private static float NormalizeAngle(float angle) + { + //-180~180に正規化 + angle %= 360f; + if (angle > 180f) angle -= 360f; + if (angle < -180f) angle += 360f; + return angle; + } + + /// + /// BVH形式の文字列を生成する + /// + public string Write(float frameTime, int startFrame, int endFrame) + { + var sb = new StringBuilder(); + sb.AppendLine("HIERARCHY"); + WriteJoint(sb, hipsJoint, 0); + + startFrame = Mathf.Clamp(startFrame, 0, frames.Count - 1); + endFrame = Mathf.Clamp(endFrame, startFrame, frames.Count - 1); + var frameCount = endFrame - startFrame + 1; + + sb.AppendLine("MOTION"); + sb.AppendLine($"Frames: {frameCount}"); + sb.AppendLine($"Frame Time: {frameTime.ToString("0.########", CultureInfo.InvariantCulture)}"); + for (int i = startFrame; i <= endFrame; i++) + { + sb.AppendLine(string.Join(" ", frames[i].Select(v => v.ToString("0.####", CultureInfo.InvariantCulture)))); + } + return sb.ToString(); + } + + private void WriteJoint(StringBuilder sb, BvhJoint joint, int depth) + { + var indent = new string(' ', depth * 2); + var type = joint == hipsJoint ? "ROOT" : "JOINT"; + sb.AppendLine($"{indent}{type} {GetJointName(joint.Bone)}"); + sb.AppendLine($"{indent}{{"); + var childIndent = new string(' ', (depth + 1) * 2); + //オフセットはX反転・cm + var offset = joint.Offset * Scale; + sb.AppendLine($"{childIndent}OFFSET {(-offset.x).ToString("0.####", CultureInfo.InvariantCulture)} {offset.y.ToString("0.####", CultureInfo.InvariantCulture)} {offset.z.ToString("0.####", CultureInfo.InvariantCulture)}"); + if (joint == hipsJoint) + { + sb.AppendLine($"{childIndent}CHANNELS 6 Xposition Yposition Zposition Yrotation Xrotation Zrotation"); + } + else + { + sb.AppendLine($"{childIndent}CHANNELS 3 Yrotation Xrotation Zrotation"); + } + + if (joint.Children.Count == 0) + { + sb.AppendLine($"{childIndent}End Site"); + sb.AppendLine($"{childIndent}{{"); + //末端の長さは不明のため、親からのオフセットと同方向に短い終端を置く + var endOffset = joint.Offset.sqrMagnitude > 0.000001f ? joint.Offset.normalized * 0.05f * Scale : new Vector3(0, 0.05f * Scale, 0); + sb.AppendLine($"{childIndent} OFFSET {(-endOffset.x).ToString("0.####", CultureInfo.InvariantCulture)} {endOffset.y.ToString("0.####", CultureInfo.InvariantCulture)} {endOffset.z.ToString("0.####", CultureInfo.InvariantCulture)}"); + sb.AppendLine($"{childIndent}}}"); + } + else + { + foreach (var child in joint.Children) + { + WriteJoint(sb, child, depth + 1); + } + } + sb.AppendLine($"{indent}}}"); + } + + private static string GetJointName(HumanBodyBones bone) + { + //UniHumanoidのSkeletonEstimator等がボーン名から推定しやすいUnity Humanoid名を使用する + return bone.ToString(); + } + } +} diff --git a/Assets/Scripts/MotionPlayback/BvhWriter.cs.meta b/Assets/Scripts/MotionPlayback/BvhWriter.cs.meta new file mode 100644 index 00000000..365eda67 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/BvhWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42dd8c04361220e49ad67d30ad17d833 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MotionPlayback/LoadedMotion.cs b/Assets/Scripts/MotionPlayback/LoadedMotion.cs new file mode 100644 index 00000000..a28a2c48 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/LoadedMotion.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UniGLTF; +using UnityEngine; +using UniVRM10; + +namespace VMC +{ + /// + /// UniVRMで読み込んだモーションファイル(VRMA/BVH)のラッパー + /// legacy AnimationClipを手動サンプリングして任意時刻のポーズを取り出す + /// + public class LoadedMotion : IDisposable + { + public string FilePath { get; private set; } + public string Name { get; private set; } + public float Length { get; private set; } + public float FrameRate { get; private set; } + public int FrameCount { get; private set; } + public bool IsVrma { get; private set; } + + public GameObject Root { get; private set; } + public Animator Animator { get; private set; } + public Vrm10AnimationInstance VrmaInstance { get; private set; } //BVHの場合null + + private Animation animation; + private AnimationState animationState; + private HumanPoseHandler poseHandler; + + //表情・視線はUniVRMのVrmAnimationImporterがVrm10AnimationInstanceのfieldへSetCurveする方式のため、 + //Animation.Sample()では値が駆動されず(かつエディタ警告が出る)。glTFのVRMC_vrm_animationから直接カーブを読み、 + //自前で任意時刻を評価する(該当チャンネルはインポート前にglTFから除去して警告と二重処理を防ぐ)。 + private readonly Dictionary expressionCurves = new Dictionary(); + //VRMA仕様: 視線はlookAtノードのローカル「回転(quaternion)」で表す(Extrinsic ZXY, Y=yaw, X=pitch) + private AnimationCurve lookAtRotX, lookAtRotY, lookAtRotZ, lookAtRotW; + private bool hasLookAt; + private float currentTime; + + public bool HasLookAt => hasLookAt; + + public UnityMemoryMappedFile.MotionFileInfo ToInfo() + { + return new UnityMemoryMappedFile.MotionFileInfo + { + FilePath = FilePath, + Name = Name, + Length = Length, + FrameRate = FrameRate, + FrameCount = FrameCount, + IsVrma = IsVrma, + }; + } + + public static async Task LoadAsync(string path) + { + if (string.IsNullOrEmpty(path) || File.Exists(path) == false) + { + throw new FileNotFoundException(path); + } + + var motion = new LoadedMotion(); + motion.FilePath = path; + motion.Name = Path.GetFileNameWithoutExtension(path); + + try + { + if (Path.GetExtension(path).ToLower() == ".bvh") + { + motion.LoadBvh(path); + } + else + { + await motion.LoadVrmaAsync(path); + } + + //マニュアルサンプリングの準備 + motion.animation = motion.Root.GetComponent(); + if (motion.animation == null) + { + throw new InvalidDataException("No animation found in file"); + } + motion.animation.playAutomatically = false; + motion.animation.Stop(); + foreach (AnimationState state in motion.animation) + { + motion.animationState = state; + break; + } + if (motion.animationState == null) + { + throw new InvalidDataException("No animation found in file"); + } + motion.animationState.wrapMode = WrapMode.ClampForever; + motion.Length = motion.animationState.clip.length; + + if (motion.FrameRate <= 0f) motion.FrameRate = 30f; + if (motion.FrameCount <= 0) motion.FrameCount = Mathf.Max(1, Mathf.RoundToInt(motion.Length * motion.FrameRate) + 1); + + motion.Animator = motion.Root.GetComponent(); + if (motion.Animator == null || motion.Animator.avatar == null || motion.Animator.avatar.isValid == false) + { + throw new InvalidDataException("No humanoid avatar found in file"); + } + motion.poseHandler = new HumanPoseHandler(motion.Animator.avatar, motion.Animator.transform); + + motion.Sample(0f); + } + catch + { + //読み込み途中で生成したGameObjectが残らないように破棄する + motion.Dispose(); + throw; + } + + return motion; + } + + /// + /// 実体(Vrm10AnimationInstance等)を生成せず、軽量にメタ情報だけ読み取る。 + /// 起動時の一覧表示用(遅延読み込み)。VRMAの本読み込みで出るエディタ警告も回避できる。 + /// + public static UnityMemoryMappedFile.MotionFileInfo ReadInfo(string path) + { + if (string.IsNullOrEmpty(path) || File.Exists(path) == false) + { + throw new FileNotFoundException(path); + } + + var info = new UnityMemoryMappedFile.MotionFileInfo + { + FilePath = path, + Name = Path.GetFileNameWithoutExtension(path), + FrameRate = 30f, + }; + + if (Path.GetExtension(path).ToLower() == ".bvh") + { + var context = new UniHumanoid.BvhImporterContext(); + context.Parse(path, File.ReadAllText(path)); //Load()はしない(階層生成なし=軽量) + info.IsVrma = false; + var frameSec = context.Bvh.FrameTime.TotalSeconds; + info.FrameCount = context.Bvh.FrameCount; + if (frameSec > 0) + { + info.FrameRate = (float)(1.0 / frameSec); + info.Length = (float)((context.Bvh.FrameCount - 1) * frameSec); + } + } + else + { + using var data = new AutoGltfFileParser(path).Parse(); + info.IsVrma = true; + DetectVrmaTimes(data, out var frameRate, out var frameCount, out var length); + if (frameRate > 0) info.FrameRate = frameRate; + info.FrameCount = frameCount; + info.Length = length; + } + + if (info.FrameRate <= 0f) info.FrameRate = 30f; + if (info.FrameCount <= 0) info.FrameCount = Mathf.Max(1, Mathf.RoundToInt(info.Length * info.FrameRate) + 1); + return info; + } + + /// + /// VRMA(glTFアニメーション)のサンプラー時刻からフレームレート/フレーム数/長さを推定する + /// + private static void DetectVrmaTimes(GltfData data, out float frameRate, out int frameCount, out float length) + { + frameRate = 0f; + frameCount = 0; + length = 0f; + try + { + var gltfAnimation = data.GLTF.animations.FirstOrDefault(); + if (gltfAnimation != null && gltfAnimation.channels.Count > 0) + { + var sampler = gltfAnimation.samplers[gltfAnimation.channels[0].sampler]; + var times = data.GetArrayFromAccessor(sampler.input); + if (times.Length > 0) + { + frameCount = times.Length; + length = times[times.Length - 1]; + } + if (times.Length > 1) + { + var deltas = new List(); + for (int i = 1; i < times.Length; i++) + { + var delta = times[i] - times[i - 1]; + if (delta > 0f) deltas.Add(delta); + } + if (deltas.Count > 0) + { + deltas.Sort(); + var medianDelta = deltas[deltas.Count / 2]; + if (medianDelta > 0f) + { + frameRate = Mathf.Round(1f / medianDelta * 100f) / 100f; + } + } + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to detect frame rate: {ex}"); + } + } + + private void LoadBvh(string path) + { + var context = new UniHumanoid.BvhImporterContext(); + context.Parse(path, File.ReadAllText(path)); + context.Load(); + + Root = context.Root; + IsVrma = false; + + //フレームレートはファイルのFrame Timeから自動判定 + if (context.Bvh.FrameTime.TotalSeconds > 0) + { + FrameRate = (float)(1.0 / context.Bvh.FrameTime.TotalSeconds); + } + FrameCount = context.Bvh.FrameCount; + } + + private async Task LoadVrmaAsync(string path) + { + using var data = new AutoGltfFileParser(path).Parse(); + + //フレームレートはglTFアニメーションのサンプラー時刻から自動判定 + DetectVrmaTimes(data, out var detectedFrameRate, out var detectedFrameCount, out _); + if (detectedFrameRate > 0f) + { + FrameRate = detectedFrameRate; + FrameCount = detectedFrameCount; + } + + //表情・視線のカーブを自前で読み取り、対応チャンネルをglTFから除去してからインポートする + //(VrmAnimationImporterがVrm10AnimationInstanceへSetCurveする処理を回避=エディタ警告と視線チャンネルのダングリングを防ぐ) + BuildExpressionAndLookAtCurves(data); + + //UniVRM 0.131でVrmAnimationImporterのコンストラクタがGltfData→VrmAnimationDataに変更された + using var loader = new VrmAnimationImporter(new VrmAnimationData(data)); + var instance = await loader.LoadAsync(new RuntimeOnlyAwaitCaller()); + Root = instance.gameObject; + IsVrma = true; + + VrmaInstance = instance.GetComponent(); + if (VrmaInstance != null && VrmaInstance.BoxMan != null) + { + VrmaInstance.ShowBoxMan(false); + } + } + + /// + /// VRMC_vrm_animationの表情・視線を自前カーブとして読み取り、対応するアニメーションチャンネルをglTFから除去する。 + /// (VrmAnimationImporterはノードindexでチャンネルを探すため、チャンネルを消せばSetCurve処理自体が走らず警告も出ない) + /// + private void BuildExpressionAndLookAtCurves(GltfData data) + { + expressionCurves.Clear(); + lookAtRotX = lookAtRotY = lookAtRotZ = lookAtRotW = null; + hasLookAt = false; + + if (UniGLTF.Extensions.VRMC_vrm_animation.GltfDeserializer.TryGet(data.GLTF.extensions, out var vrma) == false) return; + var gltfAnimation = data.GLTF.animations.FirstOrDefault(); + if (gltfAnimation == null) return; + + var channelsToRemove = new List(); + + //表情 + if (vrma.Expressions != null) + { + var preset = vrma.Expressions.Preset; + if (preset != null) + { + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.happy), preset.Happy, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.angry), preset.Angry, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.sad), preset.Sad, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.relaxed), preset.Relaxed, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.surprised), preset.Surprised, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.aa), preset.Aa, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ih), preset.Ih, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ou), preset.Ou, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ee), preset.Ee, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.oh), preset.Oh, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blink), preset.Blink, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blinkLeft), preset.BlinkLeft, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blinkRight), preset.BlinkRight, channelsToRemove); + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.neutral), preset.Neutral, channelsToRemove); + } + if (vrma.Expressions.Custom != null) + { + foreach (var kv in vrma.Expressions.Custom) + { + AddExpressionCurve(data, gltfAnimation, ExpressionKey.CreateCustom(kv.Key), kv.Value, channelsToRemove); + } + } + } + + //視線(VRMA仕様: 注視点ノードのローカル回転チャンネル。translationではない) + if (vrma.LookAt != null && vrma.LookAt.Node.HasValue) + { + var channelIndex = FindRotationChannel(gltfAnimation, vrma.LookAt.Node.Value); + if (channelIndex >= 0) + { + var channel = gltfAnimation.channels[channelIndex]; + var sampler = gltfAnimation.samplers[channel.sampler]; + var input = data.GetArrayFromAccessor(sampler.input); + var output = data.FlatternFloatArrayFromAccessor(sampler.output); //VEC4(x,y,z,w) + lookAtRotX = new AnimationCurve(); + lookAtRotY = new AnimationCurve(); + lookAtRotZ = new AnimationCurve(); + lookAtRotW = new AnimationCurve(); + for (int j = 0; j < input.Length; j++) + { + var t = input[j]; + lookAtRotX.AddKey(new Keyframe(t, output[j * 4 + 0])); + lookAtRotY.AddKey(new Keyframe(t, output[j * 4 + 1])); + lookAtRotZ.AddKey(new Keyframe(t, output[j * 4 + 2])); + lookAtRotW.AddKey(new Keyframe(t, output[j * 4 + 3])); + } + hasLookAt = true; + channelsToRemove.Add(channelIndex); + } + } + + //後ろから除去してindexのずれを防ぐ + foreach (var idx in channelsToRemove.Distinct().OrderByDescending(x => x)) + { + gltfAnimation.channels.RemoveAt(idx); + } + } + + private void AddExpressionCurve(GltfData data, glTFAnimation gltfAnimation, ExpressionKey key, UniGLTF.Extensions.VRMC_vrm_animation.Expression expression, List channelsToRemove) + { + if (expression == null || expression.Node.HasValue == false) return; + var channelIndex = FindTranslationChannel(gltfAnimation, expression.Node.Value); + if (channelIndex < 0) return; + + var channel = gltfAnimation.channels[channelIndex]; + var sampler = gltfAnimation.samplers[channel.sampler]; + var input = data.GetArrayFromAccessor(sampler.input); + var output = data.FlatternFloatArrayFromAccessor(sampler.output); + var curve = new AnimationCurve(); + for (int j = 0; j < input.Length; j++) + { + //VRMAの表情はtranslationのX成分に重みが格納される(軸変換されない生の値) + curve.AddKey(new Keyframe(input[j], output[j * 3])); + } + expressionCurves[key] = curve; + channelsToRemove.Add(channelIndex); + } + + private static int FindTranslationChannel(glTFAnimation gltfAnimation, int node) + { + return FindChannel(gltfAnimation, node, "translation"); + } + + private static int FindRotationChannel(glTFAnimation gltfAnimation, int node) + { + return FindChannel(gltfAnimation, node, "rotation"); + } + + private static int FindChannel(glTFAnimation gltfAnimation, int node, string path) + { + for (int i = 0; i < gltfAnimation.channels.Count; i++) + { + var channel = gltfAnimation.channels[i]; + if (channel.target.node == node && channel.target.path == path) + { + return i; + } + } + return -1; + } + + /// + /// 指定時刻のポーズをスケルトンに反映する + /// + public void Sample(float time) + { + currentTime = Mathf.Clamp(time, 0f, Length); + if (animationState == null) return; + animationState.enabled = true; + animationState.weight = 1f; + animationState.time = currentTime; + animation.Sample(); + animationState.enabled = false; + } + + /// + /// 現在のスケルトンのポーズを取得する(Sample後に呼ぶ) + /// + public void GetHumanPose(ref HumanPose pose) + { + poseHandler.GetHumanPose(ref pose); + } + + /// + /// 現在の表情の値一覧を取得する(VRMAのみ / Sample後に呼ぶ) + /// + public IEnumerable> GetExpressionWeights() + { + foreach (var kv in expressionCurves) + { + yield return new KeyValuePair(kv.Key, kv.Value.Evaluate(currentTime)); + } + } + + /// + /// 現在の視線のyaw/pitch(度)を取得する(VRMAで視線情報がある場合のみ / Sample後に呼ぶ)。 + /// VRMA仕様: lookAtノードのローカル回転をExtrinsic ZXYのオイラー角に分解し、Y=yaw / X=pitch とする。 + /// + public bool TryGetLookAtYawPitch(out float yaw, out float pitch) + { + yaw = 0f; + pitch = 0f; + if (hasLookAt == false) return false; + + //glTFの生quaternion(エクスポート時にX反転済み)を評価し、X反転を戻してUnityのローカル回転へ + var q = new Quaternion( + lookAtRotX.Evaluate(currentTime), + -lookAtRotY.Evaluate(currentTime), + -lookAtRotZ.Evaluate(currentTime), + lookAtRotW.Evaluate(currentTime)); + if (q.x == 0f && q.y == 0f && q.z == 0f && q.w == 0f) return false; + q = Quaternion.Normalize(q); + + //UnityのeulerAnglesはZXY順(仕様のExtrinsic ZXYと一致)。Y=yaw, X=pitch。 + var e = q.eulerAngles; + yaw = Mathf.DeltaAngle(0f, e.y); + pitch = Mathf.DeltaAngle(0f, e.x); + return true; + } + + public void Dispose() + { + poseHandler?.Dispose(); + poseHandler = null; + if (Root != null) + { + UnityEngine.Object.Destroy(Root); + Root = null; + } + } + } +} diff --git a/Assets/Scripts/MotionPlayback/LoadedMotion.cs.meta b/Assets/Scripts/MotionPlayback/LoadedMotion.cs.meta new file mode 100644 index 00000000..089001b4 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/LoadedMotion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e347acaa7d60ffd4ebf7e1a12327a865 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MotionPlayback/MotionPlayer.cs b/Assets/Scripts/MotionPlayback/MotionPlayer.cs new file mode 100644 index 00000000..86f92258 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/MotionPlayer.cs @@ -0,0 +1,619 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; + +namespace VMC +{ + /// + /// モーションファイル(VRMA/BVH)の再生 + /// VirtualAvatar(MotionSource.MotionPlayback)経由でモデルに適用するため、 + /// VR機器(VRIK)やVMCProtocolより優先される + /// + public class MotionPlayer : MonoBehaviour + { + public enum PlayState + { + Stopped = 0, + Playing = 1, + Paused = 2, + PoseHold = 3, + } + + private ControlWPFWindow controlWPFWindow; + private FaceController faceController; + private System.Threading.SynchronizationContext context = null; + + private VirtualAvatar virtualAvatar; + private Vrm10Instance currentVrm10Instance; //視線(LookAt)適用用 + private bool lookAtApplied = false; //モーションで視線を適用中か(適用をやめた時に1回だけ戻すため) + + //遅延読み込み: 起動時はメタ情報(Info)だけ保持し、実体(LoadedMotion)は初回再生時に生成する + private class MotionEntry + { + public string FilePath; + public UnityMemoryMappedFile.MotionFileInfo Info; //軽量メタ(名前/長さ/FPS/フレーム数) + public LoadedMotion Loaded; //本読み込み後の実体(未読込はnull) + public bool IsLoading; + } + private readonly List entries = new List(); + + private PlayState state = PlayState.Stopped; + private int currentIndex = -1; + private float currentTime = 0f; + + private HumanPose humanPose = new HumanPose(); + private HumanPoseHandler cloneHandler; + private Animator cloneHandlerAnimator; //cloneHandler作成時のAnimator(モデル変更検出用) + + private float lastStatusSendTime = 0f; + private const string ExpressionPresetName = "MotionPlayer"; + + //設定ファイル未読み込み時はOnDeserializingが走らずnullのため、ここで初期化する + private List MotionFilePaths => Settings.Current.MotionPlayback_MotionFiles ?? (Settings.Current.MotionPlayback_MotionFiles = new List()); + + private void Awake() + { + context = System.Threading.SynchronizationContext.Current; + controlWPFWindow = GameObject.Find("ControlWPFWindow").GetComponent(); + controlWPFWindow.AdditionalSettingAction += ApplySettings; + VMCEvents.OnCurrentModelChanged += OnCurrentModelChanged; + VMCEvents.OnModelUnloading += OnModelUnloading; + } + + private void Start() + { + faceController = GameObject.Find("AnimationController").GetComponent(); + controlWPFWindow.server.ReceivedEvent += Server_Received; + + //VirtualAvatarはモデル変更時に親Transformの子を全て破棄するため、 + //モーションのスケルトンとは別の専用GameObjectを親にする + var avatarRoot = new GameObject("AvatarRoot").transform; + avatarRoot.SetParent(transform, false); + virtualAvatar = new VirtualAvatar(avatarRoot, MotionSource.MotionPlayback); + virtualAvatar.Enable = false; + virtualAvatar.IgnoreDefaultBone = false; + MotionManager.Instance.AddVirtualAvatar(virtualAvatar); + SetVirtualAvatarSetting(); + } + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(async s => + { + if (e.CommandType == typeof(PipeCommands.Motion_GetSetting)) + { + await controlWPFWindow.server.SendCommandAsync(CreateSettingCommand(), e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.Motion_SetSetting)) + { + var d = (PipeCommands.Motion_SetSetting)e.Data; + SetSetting(d); + } + else if (e.CommandType == typeof(PipeCommands.Motion_LoadFile)) + { + var d = (PipeCommands.Motion_LoadFile)e.Data; + var ret = new PipeCommands.Motion_ReturnLoadFile(); + try + { + var entry = await LoadMotionAsync(d.Path); + ret.Success = true; + ret.Info = entry.Info; + } + catch (Exception ex) + { + Debug.LogError($"Failed to load motion: {d.Path}\n{ex}"); + ret.Success = false; + ret.Error = ex.Message; + } + await controlWPFWindow.server.SendCommandAsync(ret, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.Motion_RemoveFile)) + { + var d = (PipeCommands.Motion_RemoveFile)e.Data; + RemoveMotion(d.Index); + } + else if (e.CommandType == typeof(PipeCommands.Motion_GetFileList)) + { + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.Motion_ReturnFileList + { + Files = entries.Select(m => m.Info).ToList() + }, e.RequestId); + } + else if (e.CommandType == typeof(PipeCommands.Motion_Play)) + { + var d = (PipeCommands.Motion_Play)e.Data; + Play(d.Index); + } + else if (e.CommandType == typeof(PipeCommands.Motion_Pause)) + { + Pause(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_Stop)) + { + Stop(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_Seek)) + { + var d = (PipeCommands.Motion_Seek)e.Data; + Seek(d.Seconds); + } + else if (e.CommandType == typeof(PipeCommands.Motion_FrameStep)) + { + var d = (PipeCommands.Motion_FrameStep)e.Data; + FrameStep(d.Delta); + } + else if (e.CommandType == typeof(PipeCommands.Motion_SetRepeatMode)) + { + var d = (PipeCommands.Motion_SetRepeatMode)e.Data; + Settings.Current.MotionPlayback_RepeatMode = d.RepeatMode; + } + }, null); + } + + private PipeCommands.Motion_SetSetting CreateSettingCommand() + { + return new PipeCommands.Motion_SetSetting + { + MotionFiles = new List(MotionFilePaths), + RepeatMode = Settings.Current.MotionPlayback_RepeatMode, + ApplyRootPosition = Settings.Current.MotionPlayback_ApplyRootPosition, + ApplyRootRotation = Settings.Current.MotionPlayback_ApplyRootRotation, + ApplySpine = Settings.Current.MotionPlayback_ApplySpine, + ApplyChest = Settings.Current.MotionPlayback_ApplyChest, + ApplyHead = Settings.Current.MotionPlayback_ApplyHead, + ApplyLeftArm = Settings.Current.MotionPlayback_ApplyLeftArm, + ApplyRightArm = Settings.Current.MotionPlayback_ApplyRightArm, + ApplyLeftHand = Settings.Current.MotionPlayback_ApplyLeftHand, + ApplyRightHand = Settings.Current.MotionPlayback_ApplyRightHand, + ApplyLeftLeg = Settings.Current.MotionPlayback_ApplyLeftLeg, + ApplyRightLeg = Settings.Current.MotionPlayback_ApplyRightLeg, + ApplyLeftFoot = Settings.Current.MotionPlayback_ApplyLeftFoot, + ApplyRightFoot = Settings.Current.MotionPlayback_ApplyRightFoot, + ApplyLeftFinger = Settings.Current.MotionPlayback_ApplyLeftFinger, + ApplyRightFinger = Settings.Current.MotionPlayback_ApplyRightFinger, + ApplyEye = Settings.Current.MotionPlayback_ApplyEye, + ApplyExpression = Settings.Current.MotionPlayback_ApplyExpression, + ApplyLookAt = Settings.Current.MotionPlayback_ApplyLookAt, + RecordFps = Settings.Current.MotionRecord_Fps, + RecordCountdown = Settings.Current.MotionRecord_CountdownSeconds, + RecordMotion = Settings.Current.MotionRecord_SaveMotion, + RecordExpressionPreset = Settings.Current.MotionRecord_SaveExpressionPreset, + RecordExpressionCustom = Settings.Current.MotionRecord_SaveExpressionCustom, + RecordLookAt = Settings.Current.MotionRecord_SaveLookAt, + }; + } + + private void SetSetting(PipeCommands.Motion_SetSetting setting) + { + Settings.Current.MotionPlayback_RepeatMode = setting.RepeatMode; + Settings.Current.MotionPlayback_ApplyRootPosition = setting.ApplyRootPosition; + Settings.Current.MotionPlayback_ApplyRootRotation = setting.ApplyRootRotation; + Settings.Current.MotionPlayback_ApplySpine = setting.ApplySpine; + Settings.Current.MotionPlayback_ApplyChest = setting.ApplyChest; + Settings.Current.MotionPlayback_ApplyHead = setting.ApplyHead; + Settings.Current.MotionPlayback_ApplyLeftArm = setting.ApplyLeftArm; + Settings.Current.MotionPlayback_ApplyRightArm = setting.ApplyRightArm; + Settings.Current.MotionPlayback_ApplyLeftHand = setting.ApplyLeftHand; + Settings.Current.MotionPlayback_ApplyRightHand = setting.ApplyRightHand; + Settings.Current.MotionPlayback_ApplyLeftLeg = setting.ApplyLeftLeg; + Settings.Current.MotionPlayback_ApplyRightLeg = setting.ApplyRightLeg; + Settings.Current.MotionPlayback_ApplyLeftFoot = setting.ApplyLeftFoot; + Settings.Current.MotionPlayback_ApplyRightFoot = setting.ApplyRightFoot; + Settings.Current.MotionPlayback_ApplyLeftFinger = setting.ApplyLeftFinger; + Settings.Current.MotionPlayback_ApplyRightFinger = setting.ApplyRightFinger; + Settings.Current.MotionPlayback_ApplyEye = setting.ApplyEye; + Settings.Current.MotionPlayback_ApplyExpression = setting.ApplyExpression; + Settings.Current.MotionPlayback_ApplyLookAt = setting.ApplyLookAt; + + //一時停止中など毎フレームの適用処理が動いていない場合でも、 + //適用をオフにした時点でモーションが動かした表情・視線を戻す + if (setting.ApplyLookAt == false) ResetLookAt(); + if (setting.ApplyExpression == false) ClearExpressions(); + + //記録設定はMotion_SetRecordSetting(MotionRecorder)で更新するためここでは適用しない + //(再生・記録の両ウインドウを同時に開いた際に古い値で上書きされるのを防ぐ) + + SetVirtualAvatarSetting(); + } + + private void SetVirtualAvatarSetting() + { + if (virtualAvatar == null) return; + virtualAvatar.ApplyRootPosition = Settings.Current.MotionPlayback_ApplyRootPosition; + virtualAvatar.ApplyRootRotation = Settings.Current.MotionPlayback_ApplyRootRotation; + virtualAvatar.ApplySpine = Settings.Current.MotionPlayback_ApplySpine; + virtualAvatar.ApplyChest = Settings.Current.MotionPlayback_ApplyChest; + virtualAvatar.ApplyHead = Settings.Current.MotionPlayback_ApplyHead; + virtualAvatar.ApplyLeftArm = Settings.Current.MotionPlayback_ApplyLeftArm; + virtualAvatar.ApplyRightArm = Settings.Current.MotionPlayback_ApplyRightArm; + virtualAvatar.ApplyLeftHand = Settings.Current.MotionPlayback_ApplyLeftHand; + virtualAvatar.ApplyRightHand = Settings.Current.MotionPlayback_ApplyRightHand; + virtualAvatar.ApplyLeftLeg = Settings.Current.MotionPlayback_ApplyLeftLeg; + virtualAvatar.ApplyRightLeg = Settings.Current.MotionPlayback_ApplyRightLeg; + virtualAvatar.ApplyLeftFoot = Settings.Current.MotionPlayback_ApplyLeftFoot; + virtualAvatar.ApplyRightFoot = Settings.Current.MotionPlayback_ApplyRightFoot; + virtualAvatar.ApplyLeftFinger = Settings.Current.MotionPlayback_ApplyLeftFinger; + virtualAvatar.ApplyRightFinger = Settings.Current.MotionPlayback_ApplyRightFinger; + virtualAvatar.ApplyEye = Settings.Current.MotionPlayback_ApplyEye; + } + + private async void ApplySettings(GameObject gameObject) + { + SetVirtualAvatarSetting(); + + //設定ファイルに保存されたモーションファイルを登録する(メタ情報のみ。実体は初回再生時に読み込む) + var files = Settings.Current.MotionPlayback_MotionFiles; + if (files == null) return; + var pathsToLoad = files.Where(p => entries.Any(m => m.FilePath == p) == false).ToList(); + foreach (var path in pathsToLoad) + { + try + { + await LoadMotionAsync(path); + } + catch (Exception ex) + { + Debug.LogError($"Failed to load motion: {path}\n{ex}"); + } + } + } + + /// + /// モーションファイルを一覧に登録する(メタ情報のみ読み取り、実体はまだ生成しない) + /// + private async Task LoadMotionAsync(string path) + { + var exist = entries.FirstOrDefault(m => m.FilePath == path); + if (exist != null) return exist; + + //メタ情報の読み取りはファイルパースを伴うため別スレッドで実行 + var info = await Task.Run(() => LoadedMotion.ReadInfo(path)); + + exist = entries.FirstOrDefault(m => m.FilePath == path); + if (exist != null) return exist; + + var entry = new MotionEntry { FilePath = path, Info = info }; + entries.Add(entry); + if (MotionFilePaths.Contains(path) == false) + { + MotionFilePaths.Add(path); + } + return entry; + } + + /// + /// 実体(LoadedMotion)を必要になった時点で生成する(遅延読み込み) + /// + private async Task EnsureLoadedAsync(MotionEntry entry) + { + if (entry.Loaded != null) return entry.Loaded; + if (entry.IsLoading) return null; + entry.IsLoading = true; + try + { + var motion = await LoadedMotion.LoadAsync(entry.FilePath); + motion.Root.transform.SetParent(transform, false); + entry.Loaded = motion; + entry.Info = motion.ToInfo(); //実体から得た正確なメタで更新 + return motion; + } + finally + { + entry.IsLoading = false; + } + } + + private void RemoveMotion(int index) + { + if (index < 0 || index >= entries.Count) return; + if (currentIndex == index) + { + Stop(); + currentIndex = -1; + } + var entry = entries[index]; + MotionFilePaths.Remove(entry.FilePath); + entries.RemoveAt(index); + entry.Loaded?.Dispose(); + if (currentIndex > index) currentIndex--; + } + + public void Play(int index) + { + if (index < 0 || index >= entries.Count) return; + if (state == PlayState.Paused && index == currentIndex) + { + //一時停止からの再開 + state = PlayState.Playing; + } + else + { + currentIndex = index; + currentTime = 0f; + state = PlayState.Playing; + } + virtualAvatar.Enable = true; + ApplyCurrentFrame(); + SendStatus(); + } + + public void PlayByPath(string path) + { + var index = entries.FindIndex(m => m.FilePath == path); + if (index < 0) return; + Play(index); + } + + public void Pause() + { + if (state != PlayState.Playing) return; + state = PlayState.Paused; + SendStatus(); + } + + public void Stop() + { + if (state == PlayState.Stopped) return; + state = PlayState.Stopped; + currentTime = 0f; + virtualAvatar.Enable = false; + ClearExpressions(); + ResetLookAt(); + SendStatus(); + } + + public void Seek(float seconds) + { + if (currentIndex < 0 || currentIndex >= entries.Count) return; + currentTime = Mathf.Clamp(seconds, 0f, entries[currentIndex].Info.Length); + if (state == PlayState.Stopped) + { + state = PlayState.Paused; + virtualAvatar.Enable = true; + } + ApplyCurrentFrame(); + SendStatus(); + } + + /// + /// 1フレーム進める/戻す(ファイルのフレームレートに従う) + /// + public void FrameStep(int delta) + { + if (currentIndex < 0 || currentIndex >= entries.Count) + { + if (entries.Count == 0) return; + currentIndex = 0; + } + var info = entries[currentIndex].Info; + if (state == PlayState.Playing) + { + state = PlayState.Paused; + } + if (state == PlayState.Stopped) + { + state = PlayState.Paused; + virtualAvatar.Enable = true; + } + var fps = info.FrameRate > 0 ? info.FrameRate : 30f; + currentTime = Mathf.Clamp(currentTime + delta / fps, 0f, info.Length); + ApplyCurrentFrame(); + SendStatus(); + } + + /// + /// モーションの1フレームを抜き出したポーズを適用する(ショートカットキー用) + /// + public async void ApplyPoseByPath(string path, int frame) + { + try + { + await ApplyPoseByPathAsync(path, frame); + } + catch (Exception ex) + { + Debug.LogError($"Failed to apply pose: {path}\n{ex}"); + } + } + + /// + /// ApplyPoseByPathの待機可能版(完了を待ちたい呼び出し元向け) + /// + public async Task ApplyPoseByPathAsync(string path, int frame) + { + var entry = await LoadMotionAsync(path); + currentIndex = entries.IndexOf(entry); + var fps = entry.Info.FrameRate > 0 ? entry.Info.FrameRate : 30f; + currentTime = Mathf.Clamp(frame / fps, 0f, entry.Info.Length); + state = PlayState.PoseHold; + virtualAvatar.Enable = true; + //実体を読み込んでから適用する + await EnsureLoadedAsync(entry); + ApplyCurrentFrame(); + SendStatus(); + } + + private void OnCurrentModelChanged(GameObject model) + { + currentVrm10Instance = model != null ? model.GetComponent() : null; + } + + private void OnModelUnloading(GameObject model) + { + ResetLookAt(); //モデル破棄前に視線の適用状態を解除しておく + currentVrm10Instance = null; + cloneHandler?.Dispose(); + cloneHandler = null; + cloneHandlerAnimator = null; + if (state != PlayState.Stopped) + { + state = PlayState.Stopped; + virtualAvatar.Enable = false; + ClearExpressions(); + } + } + + private void Update() + { + if (state == PlayState.Playing) + { + if (currentIndex < 0 || currentIndex >= entries.Count) + { + Stop(); + return; + } + var length = entries[currentIndex].Info.Length; + currentTime += Time.deltaTime; + if (currentTime >= length) + { + switch (Settings.Current.MotionPlayback_RepeatMode) + { + case 1: //1ファイルループ + currentTime = length > 0f ? currentTime % length : 0f; + break; + case 2: //リストのループ再生 + currentIndex = (currentIndex + 1) % entries.Count; + currentTime = 0f; + break; + default: //1ショット + Stop(); + return; + } + } + ApplyCurrentFrame(); + + if (Time.realtimeSinceStartup - lastStatusSendTime > 0.1f) + { + SendStatus(); + } + } + } + + private void ApplyCurrentFrame() + { + if (currentIndex < 0 || currentIndex >= entries.Count) return; + var entry = entries[currentIndex]; + + //遅延読み込み: 実体が未生成なら読み込みを開始し、このフレームはスキップ(読込完了後のフレームから適用) + if (entry.Loaded == null) + { + _ = EnsureLoadedAsync(entry); + return; + } + var motion = entry.Loaded; + + motion.Sample(currentTime); + + //ポーズをVirtualAvatarのクローンスケルトンへ転写する(HumanPose経由でリターゲット) + if (EnsureCloneHandler()) + { + motion.GetHumanPose(ref humanPose); + cloneHandler.SetHumanPose(ref humanPose); + } + + //表情(VRMAのみ) + //適用オフの場合はモーションのみ再生し、表情はVMCProtocol受信等の他の入力に任せる + if (faceController != null) + { + var weights = (motion.IsVrma && Settings.Current.MotionPlayback_ApplyExpression) + ? motion.GetExpressionWeights().ToArray() + : Array.Empty>(); + if (weights.Length > 0) + { + faceController.OverwritePresets(ExpressionPresetName, weights.Select(kv => kv.Key).ToArray(), weights.Select(kv => kv.Value).ToArray()); + } + else + { + //リストループで表情なしのモーションに切り替わった際等に前の表情が残らないようにする + ClearExpressions(); + } + } + + //視線(VRMAに視線情報がある場合のみ / 適用オフならVMCProtocol受信等の他の入力に任せる) + //SetYawPitchManuallyはLookAtTargetType!=SpecifiedTransformのときのみ有効(アイトラッキングと同じ挙動) + if (currentVrm10Instance != null && motion.IsVrma && Settings.Current.MotionPlayback_ApplyLookAt + && motion.HasLookAt && motion.TryGetLookAtYawPitch(out var yaw, out var pitch)) + { + try + { + currentVrm10Instance.Runtime.LookAt.SetYawPitchManually(yaw, pitch); + lookAtApplied = true; + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to apply lookat: {ex.Message}"); + } + } + else + { + //適用をやめた直後に、モーションで動かした視線が固定されたまま残らないように戻す + ResetLookAt(); + } + } + + /// + /// モーションで動かした視線を初期方向(正面)に戻す。 + /// VMCProtocol受信やアイトラッキングが視線を制御している場合に競合しないよう、 + /// モーションで視線を適用した後の1回だけ実行する。 + /// + private void ResetLookAt() + { + if (lookAtApplied == false) return; + lookAtApplied = false; + if (currentVrm10Instance == null) return; + try + { + currentVrm10Instance.Runtime.LookAt.SetYawPitchManually(0f, 0f); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to reset lookat: {ex.Message}"); + } + } + + private bool EnsureCloneHandler() + { + if (virtualAvatar?.animator == null || virtualAvatar.animator.avatar == null) return false; + if (cloneHandler == null || cloneHandlerAnimator != virtualAvatar.animator) + { + cloneHandler?.Dispose(); + cloneHandler = new HumanPoseHandler(virtualAvatar.animator.avatar, virtualAvatar.animator.transform); + cloneHandlerAnimator = virtualAvatar.animator; + } + return true; + } + + private void ClearExpressions() + { + faceController?.OverwritePresets(ExpressionPresetName, Array.Empty(), Array.Empty()); + } + + private async void SendStatus() + { + lastStatusSendTime = Time.realtimeSinceStartup; + var length = (currentIndex >= 0 && currentIndex < entries.Count) ? entries[currentIndex].Info.Length : 0f; + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.Motion_PlaybackStatus + { + Index = currentIndex, + Time = currentTime, + Length = length, + State = (int)state, + }); + } + + #region 自動テスト用フック + + /// 登録済みモーションのフレーム数 + internal int Test_GetFrameCount(string path) + { + var entry = entries.FirstOrDefault(m => m.FilePath == path); + return entry != null ? entry.Info.FrameCount : 0; + } + + #endregion + } +} diff --git a/Assets/Scripts/MotionPlayback/MotionPlayer.cs.meta b/Assets/Scripts/MotionPlayback/MotionPlayer.cs.meta new file mode 100644 index 00000000..4db76fd8 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/MotionPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3760c2243c1a0cf4493c226469a95d44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MotionPlayback/MotionRecorder.cs b/Assets/Scripts/MotionPlayback/MotionRecorder.cs new file mode 100644 index 00000000..6a9c2c7d --- /dev/null +++ b/Assets/Scripts/MotionPlayback/MotionRecorder.cs @@ -0,0 +1,696 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UniGLTF; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; + +namespace VMC +{ + /// + /// モデルのモーションを記録してVRMA/BVHに書き出す + /// Vrm10Instance(DefaultExecutionOrder 11000)の処理後の最終ポーズを記録するため12000 + /// + [DefaultExecutionOrder(12000)] + public class MotionRecorder : MonoBehaviour + { + public enum RecordState + { + Stopped = 0, + Countdown = 1, + Recording = 2, + Recorded = 3, + } + + private ControlWPFWindow controlWPFWindow; + private FaceController faceController; + private System.Threading.SynchronizationContext context = null; + + private RecordState state = RecordState.Stopped; + private float countdownRemain = 0f; + private float recordStartTime = 0f; + private float recordFps = 60f; + + //記録データ + private readonly List recordedMuscles = new List(); + private readonly List recordedBodyPositions = new List(); + private readonly List recordedBodyRotations = new List(); + private readonly List recordedExpressions = new List(); + private readonly List recordedLookAt = new List(); //x:yaw y:pitch + private List recordedExpressionKeys = new List(); + + private GameObject currentModel; + private Vrm10Instance currentVrm10Instance; + private HumanPoseHandler modelPoseHandler; + private Animator modelPoseHandlerAnimator; + private HumanPose humanPose = new HumanPose(); + + //プレビュー用 + private VirtualAvatar virtualAvatar; + private HumanPoseHandler previewHandler; + private Animator previewHandlerAnimator; + private bool previewPlaying = false; + private float previewTime = 0f; + private int previewStartFrame = 0; + private int previewEndFrame = 0; + private int previewCurrentFrame = 0; + + private float lastStatusSendTime = 0f; + private const string ExpressionPresetName = "MotionRecorder"; + + //VRMAのプリセットとして書き出せる表情(look系はVRMAのExpressionプリセットに存在しない) + private static readonly HashSet VrmaSupportedPresets = new HashSet + { + ExpressionPreset.happy, ExpressionPreset.angry, ExpressionPreset.sad, ExpressionPreset.relaxed, + ExpressionPreset.surprised, ExpressionPreset.aa, ExpressionPreset.ih, ExpressionPreset.ou, + ExpressionPreset.ee, ExpressionPreset.oh, ExpressionPreset.blink, ExpressionPreset.blinkLeft, + ExpressionPreset.blinkRight, ExpressionPreset.neutral, + }; + + private void Awake() + { + context = System.Threading.SynchronizationContext.Current; + controlWPFWindow = GameObject.Find("ControlWPFWindow").GetComponent(); + VMCEvents.OnCurrentModelChanged += OnCurrentModelChanged; + VMCEvents.OnModelUnloading += OnModelUnloading; + } + + private void Start() + { + faceController = GameObject.Find("AnimationController").GetComponent(); + controlWPFWindow.server.ReceivedEvent += Server_Received; + + //VirtualAvatarはモデル変更時に親Transformの子を全て破棄するため専用GameObjectを親にする + var avatarRoot = new GameObject("AvatarRoot").transform; + avatarRoot.SetParent(transform, false); + virtualAvatar = new VirtualAvatar(avatarRoot, MotionSource.MotionPlayback); + virtualAvatar.Enable = false; + virtualAvatar.IgnoreDefaultBone = false; + SetAllApplyFlags(virtualAvatar); + MotionManager.Instance.AddVirtualAvatar(virtualAvatar); + } + + private void SetAllApplyFlags(VirtualAvatar avatar) + { + avatar.ApplyRootPosition = true; + avatar.ApplyRootRotation = true; + avatar.ApplySpine = true; + avatar.ApplyChest = true; + avatar.ApplyHead = true; + avatar.ApplyLeftArm = true; + avatar.ApplyRightArm = true; + avatar.ApplyLeftHand = true; + avatar.ApplyRightHand = true; + avatar.ApplyLeftLeg = true; + avatar.ApplyRightLeg = true; + avatar.ApplyLeftFoot = true; + avatar.ApplyRightFoot = true; + avatar.ApplyLeftFinger = true; + avatar.ApplyRightFinger = true; + avatar.ApplyEye = true; + } + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(async s => + { + if (e.CommandType == typeof(PipeCommands.Motion_GetSetting)) + { + //ウインドウを開き直した際に記録済み状態を復元できるように現在の状態を通知する + SendRecordingStatus(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_SetRecordSetting)) + { + var d = (PipeCommands.Motion_SetRecordSetting)e.Data; + Settings.Current.MotionRecord_Fps = d.RecordFps; + Settings.Current.MotionRecord_CountdownSeconds = d.RecordCountdown; + Settings.Current.MotionRecord_SaveMotion = d.RecordMotion; + Settings.Current.MotionRecord_SaveExpressionPreset = d.RecordExpressionPreset; + Settings.Current.MotionRecord_SaveExpressionCustom = d.RecordExpressionCustom; + Settings.Current.MotionRecord_SaveLookAt = d.RecordLookAt; + } + else if (e.CommandType == typeof(PipeCommands.Motion_StartRecording)) + { + StartRecording(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_StopRecording)) + { + StopRecording(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_PreviewSeek)) + { + var d = (PipeCommands.Motion_PreviewSeek)e.Data; + PreviewSeek(d.Frame); + } + else if (e.CommandType == typeof(PipeCommands.Motion_PreviewPlay)) + { + var d = (PipeCommands.Motion_PreviewPlay)e.Data; + PreviewPlay(d.StartFrame, d.EndFrame); + } + else if (e.CommandType == typeof(PipeCommands.Motion_PreviewPause)) + { + previewPlaying = false; + SendPreviewStatus(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_PreviewStop)) + { + PreviewStop(); + } + else if (e.CommandType == typeof(PipeCommands.Motion_SaveRecording)) + { + var d = (PipeCommands.Motion_SaveRecording)e.Data; + var ret = new PipeCommands.Motion_ReturnSaveRecording(); + try + { + SaveRecording(d.Path, d.Format, d.StartFrame, d.EndFrame); + ret.Success = true; + } + catch (Exception ex) + { + Debug.LogError($"Failed to save recording: {ex}"); + ret.Success = false; + ret.Error = ex.Message; + } + await controlWPFWindow.server.SendCommandAsync(ret, e.RequestId); + } + }, null); + } + + private void OnCurrentModelChanged(GameObject model) + { + currentModel = model; + currentVrm10Instance = model != null ? model.GetComponent() : null; + modelPoseHandler?.Dispose(); + modelPoseHandler = null; + modelPoseHandlerAnimator = null; + } + + private void OnModelUnloading(GameObject model) + { + if (state == RecordState.Recording || state == RecordState.Countdown) + { + StopRecording(); + } + PreviewStop(); + currentModel = null; + currentVrm10Instance = null; + modelPoseHandler?.Dispose(); + modelPoseHandler = null; + modelPoseHandlerAnimator = null; + previewHandler?.Dispose(); + previewHandler = null; + previewHandlerAnimator = null; + } + + public void StartRecording() + { + if (state == RecordState.Recording || state == RecordState.Countdown) return; + if (currentModel == null) + { + Debug.LogWarning("MotionRecorder: No model loaded"); + return; + } + + PreviewStop(); + + recordFps = Mathf.Clamp(Settings.Current.MotionRecord_Fps, 1, 240); + countdownRemain = Mathf.Max(0, Settings.Current.MotionRecord_CountdownSeconds); + + recordedMuscles.Clear(); + recordedBodyPositions.Clear(); + recordedBodyRotations.Clear(); + recordedExpressions.Clear(); + recordedLookAt.Clear(); + recordedExpressionKeys.Clear(); + + if (countdownRemain > 0f) + { + state = RecordState.Countdown; + } + else + { + BeginCapture(); + } + SendRecordingStatus(); + } + + private void BeginCapture() + { + //記録する表情キー一覧を確定する + if (currentVrm10Instance != null) + { + foreach (var key in currentVrm10Instance.Runtime.Expression.ExpressionKeys) + { + if (key.Preset == ExpressionPreset.custom || VrmaSupportedPresets.Contains(key.Preset)) + { + recordedExpressionKeys.Add(key); + } + } + } + + recordStartTime = Time.time; + state = RecordState.Recording; + } + + public void StopRecording() + { + if (state == RecordState.Countdown) + { + state = RecordState.Stopped; + } + else if (state == RecordState.Recording) + { + state = recordedMuscles.Count > 0 ? RecordState.Recorded : RecordState.Stopped; + } + SendRecordingStatus(); + } + + private void Update() + { + if (state == RecordState.Countdown) + { + countdownRemain -= Time.deltaTime; + if (countdownRemain <= 0f) + { + BeginCapture(); + } + if (Time.realtimeSinceStartup - lastStatusSendTime > 0.1f) + { + SendRecordingStatus(); + } + } + + //プレビュー再生 + if (previewPlaying && recordedMuscles.Count > 0) + { + previewTime += Time.deltaTime; + var range = Mathf.Max(1, previewEndFrame - previewStartFrame + 1); + var frame = previewStartFrame + Mathf.FloorToInt(previewTime * recordFps) % range; + if (frame != previewCurrentFrame) + { + ApplyPreviewFrame(frame); + } + if (Time.realtimeSinceStartup - lastStatusSendTime > 0.1f) + { + SendPreviewStatus(); + } + } + } + + private void LateUpdate() + { + if (state == RecordState.Recording) + { + //フレームレートに合わせて記録する(処理落ち時は同じポーズを複数フレームに記録) + var expectedFrames = Mathf.FloorToInt((Time.time - recordStartTime) * recordFps) + 1; + if (recordedMuscles.Count < expectedFrames) + { + CaptureFrame(); + while (recordedMuscles.Count < expectedFrames) + { + DuplicateLastFrame(); + } + } + + if (Time.realtimeSinceStartup - lastStatusSendTime > 0.1f) + { + SendRecordingStatus(); + } + } + } + + private void CaptureFrame() + { + if (EnsureModelPoseHandler() == false) return; + + modelPoseHandler.GetHumanPose(ref humanPose); + recordedMuscles.Add((float[])humanPose.muscles.Clone()); + recordedBodyPositions.Add(humanPose.bodyPosition); + recordedBodyRotations.Add(humanPose.bodyRotation); + + //表情 + var expressionValues = new float[recordedExpressionKeys.Count]; + if (currentVrm10Instance != null) + { + var actualWeights = currentVrm10Instance.Runtime.Expression.ActualWeights; + for (int i = 0; i < recordedExpressionKeys.Count; i++) + { + if (actualWeights.TryGetValue(recordedExpressionKeys[i], out var weight)) + { + expressionValues[i] = weight; + } + } + } + recordedExpressions.Add(expressionValues); + + //視線 + if (currentVrm10Instance != null) + { + var lookAt = currentVrm10Instance.Runtime.LookAt; + recordedLookAt.Add(new Vector2(lookAt.Yaw, lookAt.Pitch)); + } + else + { + recordedLookAt.Add(Vector2.zero); + } + } + + private void DuplicateLastFrame() + { + recordedMuscles.Add(recordedMuscles[recordedMuscles.Count - 1]); + recordedBodyPositions.Add(recordedBodyPositions[recordedBodyPositions.Count - 1]); + recordedBodyRotations.Add(recordedBodyRotations[recordedBodyRotations.Count - 1]); + recordedExpressions.Add(recordedExpressions[recordedExpressions.Count - 1]); + recordedLookAt.Add(recordedLookAt[recordedLookAt.Count - 1]); + } + + private bool EnsureModelPoseHandler() + { + if (currentModel == null) return false; + var animator = currentModel.GetComponent(); + if (animator == null || animator.avatar == null) return false; + if (modelPoseHandler == null || modelPoseHandlerAnimator != animator) + { + modelPoseHandler?.Dispose(); + modelPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform); + modelPoseHandlerAnimator = animator; + } + return true; + } + + #region Preview + + private bool EnsurePreviewHandler() + { + if (virtualAvatar?.animator == null || virtualAvatar.animator.avatar == null) return false; + if (previewHandler == null || previewHandlerAnimator != virtualAvatar.animator) + { + previewHandler?.Dispose(); + previewHandler = new HumanPoseHandler(virtualAvatar.animator.avatar, virtualAvatar.animator.transform); + previewHandlerAnimator = virtualAvatar.animator; + } + return true; + } + + public void PreviewSeek(int frame) + { + if (state != RecordState.Recorded) return; + previewPlaying = false; + ApplyPreviewFrame(frame); + SendPreviewStatus(); + } + + public void PreviewPlay(int startFrame, int endFrame) + { + if (state != RecordState.Recorded) return; + previewStartFrame = Mathf.Clamp(startFrame, 0, recordedMuscles.Count - 1); + previewEndFrame = Mathf.Clamp(endFrame, previewStartFrame, recordedMuscles.Count - 1); + previewTime = 0f; + previewPlaying = true; + virtualAvatar.Enable = true; + ApplyPreviewFrame(previewStartFrame); + SendPreviewStatus(); + } + + public void PreviewStop() + { + previewPlaying = false; + if (virtualAvatar != null) + { + virtualAvatar.Enable = false; + } + faceController?.OverwritePresets(ExpressionPresetName, Array.Empty(), Array.Empty()); + SendPreviewStatus(); + } + + private void ApplyPreviewFrame(int frame) + { + if (recordedMuscles.Count == 0) return; + frame = Mathf.Clamp(frame, 0, recordedMuscles.Count - 1); + previewCurrentFrame = frame; + + virtualAvatar.Enable = true; + if (EnsurePreviewHandler()) + { + humanPose.muscles = recordedMuscles[frame]; + humanPose.bodyPosition = recordedBodyPositions[frame]; + humanPose.bodyRotation = recordedBodyRotations[frame]; + previewHandler.SetHumanPose(ref humanPose); + } + + if (recordedExpressionKeys.Count > 0 && faceController != null) + { + faceController.OverwritePresets(ExpressionPresetName, recordedExpressionKeys.ToArray(), recordedExpressions[frame]); + } + } + + #endregion + + #region Save + + private void SaveRecording(string path, int format, int startFrame, int endFrame) + { + if (state != RecordState.Recorded || recordedMuscles.Count == 0) + { + throw new InvalidOperationException("No recorded motion"); + } + if (virtualAvatar?.animator == null || virtualAvatar.animator.avatar == null) + { + throw new InvalidOperationException("No model loaded"); + } + + startFrame = Mathf.Clamp(startFrame, 0, recordedMuscles.Count - 1); + endFrame = Mathf.Clamp(endFrame, startFrame, recordedMuscles.Count - 1); + + //プレビューを止めてから書き出し用にスケルトンを使う + previewPlaying = false; + + try + { + if (format == 0) + { + SaveVrma(path, startFrame, endFrame); + } + else + { + SaveBvh(path, startFrame, endFrame); + } + } + finally + { + //書き出し中にスケルトンを動かしたため、プレビュー表示中だった場合は元のフレームに戻す + if (virtualAvatar.Enable) + { + ApplyPreviewFrame(previewCurrentFrame); + } + } + } + + private void ApplyFrameToSkeleton(int frame, bool applyMotion) + { + if (EnsurePreviewHandler() == false) return; + if (applyMotion) + { + humanPose.muscles = recordedMuscles[frame]; + humanPose.bodyPosition = recordedBodyPositions[frame]; + humanPose.bodyRotation = recordedBodyRotations[frame]; + previewHandler.SetHumanPose(ref humanPose); + } + else + { + //モーションを保存しない場合はクローンのバインドポーズ(VRMのTポーズ=アバターのレスト基準)にする。 + //マッスルゼロ姿勢はTポーズと異なるため、レストとして使うと再生時に全関節がずれる + virtualAvatar.RestoreBindPose(); + } + } + + private void SaveBvh(string path, int startFrame, int endFrame) + { + var animator = virtualAvatar.animator; + //オフセットとレスト回転をアバターのレスト基準(バインドTポーズ)で取得するため、バインドポーズに戻してから階層を構築する + virtualAvatar.RestoreBindPose(); + var writer = new BvhWriter(animator, animator.transform); + for (int i = startFrame; i <= endFrame; i++) + { + ApplyFrameToSkeleton(i, true); + writer.AddFrame(); + } + File.WriteAllText(path, writer.Write(1f / recordFps, 0, endFrame - startFrame)); + } + + private void SaveVrma(string path, int startFrame, int endFrame) + { + var animator = virtualAvatar.animator; + var exportRoot = animator.gameObject; + + var saveMotion = Settings.Current.MotionRecord_SaveMotion; + var savePreset = Settings.Current.MotionRecord_SaveExpressionPreset; + var saveCustom = Settings.Current.MotionRecord_SaveExpressionCustom; + var saveLookAt = Settings.Current.MotionRecord_SaveLookAt; + + //表情・視線用の一時ノードを作成する + var expressionNodes = new Dictionary(); //recordedExpressionKeysのindex -> node + Transform lookAtNode = null; + var tempNodes = new List(); + try + { + //視線ノードは表情ノードより「先」に作る(=glTFノードindexが表情より小さくなる)。 + //公式VrmAnimationImporterは表情ノードをRemoveAtで削除しindexを詰めるが視線チャンネルは補正しないため、 + //視線を表情より後(高index)に置くと削除で視線チャンネルのtarget.nodeがずれて公式実装で壊れる。 + //(表情/視線ノードはexportRoot直下=ルート除外によりトップレベル扱いになりchildrenは汚さない) + if (saveLookAt) + { + var node = new GameObject("VMC_LookAtTarget"); + node.transform.SetParent(exportRoot.transform, false); + tempNodes.Add(node); + lookAtNode = node.transform; + } + + for (int i = 0; i < recordedExpressionKeys.Count; i++) + { + var key = recordedExpressionKeys[i]; + var isCustom = key.Preset == ExpressionPreset.custom; + if (isCustom && saveCustom == false) continue; + if (isCustom == false && savePreset == false) continue; + + var node = new GameObject($"VMC_Expression_{(isCustom ? key.Name : key.Preset.ToString())}"); + node.transform.SetParent(exportRoot.transform, false); + tempNodes.Add(node); + expressionNodes[i] = node.transform; + } + + //バインドポーズ(VRMのTポーズ=アバターのレスト基準)に戻してからエクスポータを準備する。 + //Prepare時のノードのローカル回転がVRMAのレストとして書き出され、再インポート時のアバターTポーズになるため、 + //VRMのTポーズと一致させる必要がある(マッスルゼロ姿勢だと全関節がずれる) + virtualAvatar.RestoreBindPose(); + + var data = new ExportingGltfData(); + using (var exporter = new VMCVrmAnimationExporter(data, new GltfExportSettings())) + { + exporter.Prepare(exportRoot); + exporter.Export(vrma => + { + //Humanoidボーンを登録する + var map = new Dictionary(); + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + var t = animator.GetBoneTransform(bone); + if (t == null) continue; + map.Add(bone, t); + } + + vrma.SetPositionBoneAndParent(map[HumanBodyBones.Hips], exportRoot.transform); + + foreach (var kv in map) + { + var vrmBone = Vrm10HumanoidBoneSpecification.ConvertFromUnityBone(kv.Key); + var parent = GetParentBone(map, vrmBone) ?? exportRoot.transform; + vrma.AddRotationBoneAndParent(kv.Key, kv.Value, parent); + } + + //表情を登録する + int currentFrame = startFrame; + foreach (var kv in expressionNodes) + { + var keyIndex = kv.Key; + vrma.AddExpression(recordedExpressionKeys[keyIndex], kv.Value, () => recordedExpressions[currentFrame][keyIndex]); + } + + //視線を登録する + if (lookAtNode != null) + { + vrma.SetLookAt(lookAtNode, exportRoot.transform); + } + + //全フレームをサンプリングする + var frameTime = TimeSpan.FromSeconds(1.0 / recordFps); + var time = default(TimeSpan); + for (int i = startFrame; i <= endFrame; i++, time += frameTime) + { + currentFrame = i; + ApplyFrameToSkeleton(i, saveMotion); + if (lookAtNode != null) + { + //VRMA仕様: 視線はノードのローカル回転(Extrinsic ZXY, Y=yaw, X=pitch)で表す。 + //recordedLookAt=(yaw, pitch)。UnityのQuaternion.Euler(x,y,z)はZ→X→Y適用=Extrinsic ZXYと一致。 + lookAtNode.localRotation = Quaternion.Euler(recordedLookAt[i].y, recordedLookAt[i].x, 0f); + } + vrma.AddFrame(time); + } + }); + } + + File.WriteAllBytes(path, data.ToGlbBytes()); + } + finally + { + foreach (var node in tempNodes) + { + DestroyImmediate(node); + } + //スケルトンをバインドポーズに戻す + virtualAvatar.RestoreBindPose(); + } + } + + private static Transform GetParentBone(Dictionary map, Vrm10HumanoidBones bone) + { + while (true) + { + if (bone == Vrm10HumanoidBones.Hips) + { + break; + } + var parentBone = Vrm10HumanoidBoneSpecification.GetDefine(bone).ParentBone.Value; + var unityParentBone = Vrm10HumanoidBoneSpecification.ConvertToUnityBone(parentBone); + if (map.TryGetValue(unityParentBone, out var found)) + { + return found; + } + bone = parentBone; + } + return null; + } + + #endregion + + private async void SendRecordingStatus() + { + lastStatusSendTime = Time.realtimeSinceStartup; + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.Motion_RecordingStatus + { + State = (int)state, + Time = state == RecordState.Recording ? Time.time - recordStartTime : recordedMuscles.Count / recordFps, + Countdown = Mathf.Max(0f, countdownRemain), + FrameCount = recordedMuscles.Count, + Fps = recordFps, + }); + } + + private async void SendPreviewStatus() + { + lastStatusSendTime = Time.realtimeSinceStartup; + await controlWPFWindow.server.SendCommandAsync(new PipeCommands.Motion_PreviewStatus + { + Frame = previewCurrentFrame, + Playing = previewPlaying, + }); + } + + #region 自動テスト用フック + + internal RecordState Test_State => state; + + internal int Test_RecordedFrameCount => recordedMuscles.Count; + + internal float Test_RecordFps => recordFps; + + internal void Test_SaveRecording(string path, int format, int startFrame, int endFrame) + => SaveRecording(path, format, startFrame, endFrame); + + #endregion + } +} diff --git a/Assets/Scripts/MotionPlayback/MotionRecorder.cs.meta b/Assets/Scripts/MotionPlayback/MotionRecorder.cs.meta new file mode 100644 index 00000000..53184b37 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/MotionRecorder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3900098eb9f45d439d90dad747330c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs b/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs new file mode 100644 index 00000000..13a61310 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UniGLTF; +using UnityEngine; +using UniVRM10; + +namespace VMC +{ + /// + /// VRM Animation(.vrma)のエクスポート + /// UniVRMのVrmAnimationExporterはHumanoidボーンのみ対応のため、 + /// 表情(Expression)と視線(LookAt)の書き出しを追加した拡張版 + /// (座標系変換等の仕様はUniVRMのVrmAnimationExporter/VrmAnimationImporterに準拠) + /// + public class VMCVrmAnimationExporter : gltfExporter + { + public VMCVrmAnimationExporter( + ExportingGltfData data, + GltfExportSettings settings) + : base(data, settings) + { + settings.InverseAxis = Axes.X; + } + + readonly List m_times = new List(); + + class PositionExporter + { + public List Values = new List(); + public Transform Node; + readonly Transform m_root; + + public PositionExporter(Transform bone, Transform root) + { + Node = bone; + m_root = root; + } + + public void Add() + { + var p = m_root.worldToLocalMatrix.MultiplyPoint(Node.position); + // reverse-X + Values.Add(new Vector3(-p.x, p.y, p.z)); + } + } + PositionExporter m_position; + public void SetPositionBoneAndParent(Transform bone, Transform parent) + { + m_position = new PositionExporter(bone, parent); + } + + class RotationExporter + { + public List Values = new List(); + public readonly Transform Node; + public Transform m_parent; + + public RotationExporter(Transform bone, Transform parent) + { + Node = bone; + m_parent = parent; + } + + public void Add() + { + var q = Quaternion.Inverse(m_parent.rotation) * Node.rotation; + // reverse-X + Values.Add(new Quaternion(q.x, -q.y, -q.z, q.w)); + } + } + readonly Dictionary m_rotations = new Dictionary(); + public void AddRotationBoneAndParent(HumanBodyBones bone, Transform transform, Transform parent) + { + m_rotations.Add(bone, new RotationExporter(transform, parent)); + } + + /// + /// 表情の重みチャンネル + /// VRMAの仕様では表情はノードのX方向のtranslationとして記録される + /// (VrmAnimationImporterは軸変換せずaccessorの生の値を重みとして読むため、変換なしで書き込む) + /// + class ExpressionExporter + { + public List Values = new List(); + public readonly ExpressionKey Key; + public readonly Transform Node; + public Func GetWeight; + + public ExpressionExporter(ExpressionKey key, Transform node, Func getWeight) + { + Key = key; + Node = node; + GetWeight = getWeight; + } + + public void Add() + { + Values.Add(new Vector3(GetWeight(), 0, 0)); + } + } + readonly List m_expressions = new List(); + public void AddExpression(ExpressionKey key, Transform node, Func getWeight) + { + m_expressions.Add(new ExpressionExporter(key, node, getWeight)); + } + + /// + /// 視線(LookAt)ノード + /// VRMA仕様では視線はノードの「ローカル回転」で表す(Extrinsic ZXY, Y=yaw, X=pitch)。translationではない。 + /// + RotationExporter m_lookAt; + public void SetLookAt(Transform node, Transform parent) + { + m_lookAt = new RotationExporter(node, parent); + } + + public void AddFrame(TimeSpan time) + { + m_times.Add((float)time.TotalSeconds); + m_position.Add(); + foreach (var kv in m_rotations) + { + kv.Value.Add(); + } + foreach (var expression in m_expressions) + { + expression.Add(); + } + m_lookAt?.Add(); + } + + public void Export(Action addFrames) + { + base.Export(); + + addFrames(this); + + // + // export + // + var gltfAnimation = new glTFAnimation + { + }; + _data.Gltf.animations.Add(gltfAnimation); + + // Nodes には 右手左手変換後のコピーが入っているため名前で逆引きする + var names = Nodes.Select(x => x.name).ToList(); + + // time values + var input = _data.ExtendBufferAndGetAccessorIndex(m_times.ToArray()); + + void AddChannel(int outputAccessor, string nodeName, string path) + { + var sampler = gltfAnimation.samplers.Count; + gltfAnimation.samplers.Add(new glTFAnimationSampler + { + input = input, + output = outputAccessor, + interpolation = "LINEAR", + }); + + gltfAnimation.channels.Add(new glTFAnimationChannel + { + sampler = sampler, + target = new glTFAnimationTarget + { + node = names.IndexOf(nodeName), + path = path, + }, + }); + } + + { + var output = _data.ExtendBufferAndGetAccessorIndex(m_position.Values.ToArray()); + AddChannel(output, m_position.Node.name, "translation"); + } + + foreach (var kv in m_rotations) + { + var output = _data.ExtendBufferAndGetAccessorIndex(kv.Value.Values.ToArray()); + AddChannel(output, kv.Value.Node.name, "rotation"); + } + + foreach (var expression in m_expressions) + { + var output = _data.ExtendBufferAndGetAccessorIndex(expression.Values.ToArray()); + AddChannel(output, expression.Node.name, "translation"); + } + + if (m_lookAt != null) + { + var output = _data.ExtendBufferAndGetAccessorIndex(m_lookAt.Values.ToArray()); + AddChannel(output, m_lookAt.Node.name, "rotation"); + } + + // VRMC_vrm_animation + var vrmAnimation = VrmAnimationUtil.Create(m_rotations.ToDictionary(kv => kv.Key, kv => kv.Value.Node), names); + + // 表情 + if (m_expressions.Count > 0) + { + vrmAnimation.Expressions = new UniGLTF.Extensions.VRMC_vrm_animation.Expressions + { + Preset = new UniGLTF.Extensions.VRMC_vrm_animation.Preset(), + }; + foreach (var expression in m_expressions) + { + var node = new UniGLTF.Extensions.VRMC_vrm_animation.Expression { Node = names.IndexOf(expression.Node.name) }; + switch (expression.Key.Preset) + { + case ExpressionPreset.happy: vrmAnimation.Expressions.Preset.Happy = node; break; + case ExpressionPreset.angry: vrmAnimation.Expressions.Preset.Angry = node; break; + case ExpressionPreset.sad: vrmAnimation.Expressions.Preset.Sad = node; break; + case ExpressionPreset.relaxed: vrmAnimation.Expressions.Preset.Relaxed = node; break; + case ExpressionPreset.surprised: vrmAnimation.Expressions.Preset.Surprised = node; break; + case ExpressionPreset.aa: vrmAnimation.Expressions.Preset.Aa = node; break; + case ExpressionPreset.ih: vrmAnimation.Expressions.Preset.Ih = node; break; + case ExpressionPreset.ou: vrmAnimation.Expressions.Preset.Ou = node; break; + case ExpressionPreset.ee: vrmAnimation.Expressions.Preset.Ee = node; break; + case ExpressionPreset.oh: vrmAnimation.Expressions.Preset.Oh = node; break; + case ExpressionPreset.blink: vrmAnimation.Expressions.Preset.Blink = node; break; + case ExpressionPreset.blinkLeft: vrmAnimation.Expressions.Preset.BlinkLeft = node; break; + case ExpressionPreset.blinkRight: vrmAnimation.Expressions.Preset.BlinkRight = node; break; + case ExpressionPreset.neutral: vrmAnimation.Expressions.Preset.Neutral = node; break; + case ExpressionPreset.custom: + if (vrmAnimation.Expressions.Custom == null) + { + vrmAnimation.Expressions.Custom = new Dictionary(); + } + vrmAnimation.Expressions.Custom[expression.Key.Name] = node; + break; + } + } + } + + // 視線 + if (m_lookAt != null) + { + vrmAnimation.LookAt = new UniGLTF.Extensions.VRMC_vrm_animation.LookAt + { + Node = names.IndexOf(m_lookAt.Node.name), + }; + } + + UniGLTF.Extensions.VRMC_vrm_animation.GltfSerializer.SerializeTo( + ref _data.Gltf.extensions + , vrmAnimation); + } + } +} diff --git a/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs.meta b/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs.meta new file mode 100644 index 00000000..c351fbf4 --- /dev/null +++ b/Assets/Scripts/MotionPlayback/VMCVrmAnimationExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f0d13b20df1cf9428e8c7b3074940b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts.meta b/Assets/Scripts/Plugin.meta similarity index 77% rename from Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts.meta rename to Assets/Scripts/Plugin.meta index 4b59a616..11dfb16b 100644 --- a/Assets/ExternalPlugins/DVRSDK/DVRAvatar/Scripts.meta +++ b/Assets/Scripts/Plugin.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ddda8d60d46c0b14a9d3eb94ec3b09a3 +guid: 24ddac6529dabd742b17ac65e148973e folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Scripts/Plugin/PluginHost.cs b/Assets/Scripts/Plugin/PluginHost.cs new file mode 100644 index 00000000..039ccc19 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginHost.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; +using VMC.Plugin; + +namespace VMC +{ + /// + /// IPluginHost の本体側実装。 + /// プラグインへ渡す窓口をここに集約し、プラグインが Assembly-CSharp を + /// 直接参照しなくて済むようにする。 + /// + public class PluginHost : IPluginHost + { + private readonly FaceControlAdapter faceControl; + private readonly MotionSourceFactory motionSource; + private readonly PluginIpc ipc; + + private GameObject currentModel; + + public PluginHost(ControlWPFWindow controlWPFWindow, FaceController faceController) + { + faceControl = new FaceControlAdapter(faceController); + motionSource = new MotionSourceFactory(); + ipc = new PluginIpc(controlWPFWindow); + + VMCEvents.OnCurrentModelChanged += model => currentModel = model; + VMCEvents.OnModelUnloading += model => currentModel = null; + } + + public IFaceControl FaceControl => faceControl; + public IMotionSourceFactory MotionSource => motionSource; + public IPluginIpc Ipc => ipc; + public GameObject CurrentModel => currentModel; + + public event Action SettingsApplied; + + /// 本体の設定適用が終わったときに ControlWPFWindow から呼ばれる + internal void RaiseSettingsApplied() => SettingsApplied?.Invoke(); + + public IPluginSettings GetSettings(string pluginId) => new PluginSettingsStore(pluginId); + } + + /// FaceController を IFaceControl として公開するアダプタ + internal class FaceControlAdapter : IFaceControl + { + private readonly FaceController faceController; + private Vrm10Instance vrm10Instance; + + public FaceControlAdapter(FaceController faceController) + { + this.faceController = faceController; + VMCEvents.OnCurrentModelChanged += model => + vrm10Instance = model != null ? model.GetComponent() : null; + VMCEvents.OnModelUnloading += _ => vrm10Instance = null; + } + + public event Action BeforeApply + { + add { faceController.BeforeApply += value; } + remove { faceController.BeforeApply -= value; } + } + + public void SetBlink_L(float value) => faceController.SetBlink_L(value); + public void SetBlink_R(float value) => faceController.SetBlink_R(value); + + public void MixPresets(string presetName, string[] keys, float[] values) + => faceController.MixPresets(presetName, keys, values); + + public void SetLookAtPosition(Vector3 worldPosition) + { + if (vrm10Instance == null) return; + //LookAtTarget未使用時のみ有効。ボーン/Expressionどちらの目線タイプもRuntimeが処理する + var lookAt = vrm10Instance.Runtime.LookAt; + var (yaw, pitch) = lookAt.CalculateYawPitchFromLookAtPosition(worldPosition); + lookAt.SetYawPitchManually(yaw, pitch); + } + + public bool ExternalEyelidControlEnabled + { + get => faceController.ExternalEyelidControlEnabled; + set => faceController.ExternalEyelidControlEnabled = value; + } + } + + /// VirtualAvatar の生成と MotionManager への登録を仲介する + internal class MotionSourceFactory : IMotionSourceFactory + { + public VirtualAvatar Create(Transform boneParentTransform) + { + var virtualAvatar = new VirtualAvatar(boneParentTransform, MotionSource.ExternalDevice) + { + Enable = false, + }; + MotionManager.Instance.AddVirtualAvatar(virtualAvatar); + return virtualAvatar; + } + + public void Remove(VirtualAvatar virtualAvatar) + { + virtualAvatar.Enable = false; + MotionManager.Instance?.RemoveVirtualAvatar(virtualAvatar); + } + } + + /// コントロールパネルとの通信をプラグインへ中継する + internal class PluginIpc : IPluginIpc + { + private readonly ControlWPFWindow controlWPFWindow; + private readonly System.Threading.SynchronizationContext context; + + public PluginIpc(ControlWPFWindow controlWPFWindow) + { + this.controlWPFWindow = controlWPFWindow; + context = System.Threading.SynchronizationContext.Current; + } + + public event EventHandler Received + { + add { controlWPFWindow.server.ReceivedEvent += value; } + remove { controlWPFWindow.server.ReceivedEvent -= value; } + } + + public Task SendCommandAsync(object command, string requestId = null) + => controlWPFWindow.server.SendCommandAsync(command, requestId); + + public void Post(Action action) => context.Post(_ => action(), null); + } +} diff --git a/Assets/Scripts/Plugin/PluginHost.cs.meta b/Assets/Scripts/Plugin/PluginHost.cs.meta new file mode 100644 index 00000000..da7df1d3 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginHost.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 948e00c10c29a584fa61c643c3d93615 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Plugin/PluginManager.cs b/Assets/Scripts/Plugin/PluginManager.cs new file mode 100644 index 00000000..8cb9319f --- /dev/null +++ b/Assets/Scripts/Plugin/PluginManager.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; +using VMC.Plugin; + +namespace VMC +{ + /// + /// 公式プラグイン(Plugins/配下)のローダ。 + /// + /// ユーザー製作のMod(Mods/配下・ModManager)とは意図的に分けている: + /// - Mod はコントロールパネル接続後・プレリリース版のみ読み込まれるが、 + /// プラグインは設定の適用より前に読み込む必要がある + /// - Mod を読み込むと VRoid Hub 連携が無効化されるが、公式プラグインは対象外 + /// - Mod は属性で識別するのに対し、プラグインは IVMCPlugin の実装で識別する + /// + public class PluginManager : MonoBehaviour + { + /// + /// プラグインの置き場所。ビルド版ではexeの隣、エディタではリポジトリ直下を指す。 + /// Awakeの実行順に依らず使えるよう都度求める。 + /// + private static string PluginsPath => Path.GetFullPath(Application.dataPath + "/../Plugins/"); + + private readonly List loadedPlugins = new List(); + + public IReadOnlyList LoadedPlugins => loadedPlugins; + + public class LoadedPlugin + { + public string Id; + public string DisplayName; + public string Version; + public string AssemblyPath; + public IVMCPlugin Instance; + } + + /// + /// Plugins/ 以下を走査してプラグインを読み込む。 + /// ControlWPFWindow の初期化中(設定の適用より前)に一度だけ呼ばれる。 + /// + public void LoadPlugins(IPluginHost host) + { + if (Directory.Exists(PluginsPath) == false) + { + //初回起動時に置き場所が分かるよう、空でも作っておく + try { Directory.CreateDirectory(PluginsPath); } catch { } + return; + } + + Debug.Log("Start Loading Plugins"); + + //プラグインごとのフォルダ(直下のDLLも一応拾う) + var directories = new List { PluginsPath }; + directories.AddRange(Directory.GetDirectories(PluginsPath, "*", SearchOption.TopDirectoryOnly)); + + foreach (var directory in directories) + { + //ネイティブDLLはDllImportの探索パスに入らないため、先に絶対パスで読み込んでおく。 + //ネイティブDLLは native/ サブフォルダに置く決まりなので、 + //ここで拾う直下の *.dll はマネージドDLLだけになる + NativeLibraryLoader.PreloadFrom(directory); + + foreach (var dllFile in Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly)) + { + LoadPluginAssembly(dllFile, host); + } + } + + Debug.Log($"Loaded {loadedPlugins.Count} plugin(s)"); + } + + private void LoadPluginAssembly(string dllFile, IPluginHost host) + { + Type[] pluginTypes; + try + { + var assembly = Assembly.LoadFrom(dllFile); + pluginTypes = assembly.GetTypes() + .Where(x => x.IsPublic && x.IsAbstract == false && typeof(IVMCPlugin).IsAssignableFrom(x)) + .ToArray(); + } + catch (BadImageFormatException) + { + //ネイティブDLLなので無視してよい + return; + } + catch (ReflectionTypeLoadException ex) + { + //SDKの依存が欠けている場合など。他のプラグインは読み込めるようにして続行する + Debug.LogError($"[Plugin] 型の読み込みに失敗しました: {dllFile}"); + foreach (var loaderException in ex.LoaderExceptions.Take(3)) + { + Debug.LogError($"[Plugin] {loaderException.Message}"); + } + return; + } + catch (Exception ex) + { + Debug.LogError($"[Plugin] 読み込みに失敗しました: {dllFile} ({ex.Message})"); + return; + } + + foreach (var type in pluginTypes) + { + try + { + if (typeof(MonoBehaviour).IsAssignableFrom(type) == false) + { + Debug.LogError($"[Plugin] {type.FullName} は MonoBehaviour を継承していないため読み込めません"); + continue; + } + + var plugin = (IVMCPlugin)gameObject.AddComponent(type); + + if (loadedPlugins.Any(d => d.Id == plugin.Id)) + { + Debug.LogError($"[Plugin] ID '{plugin.Id}' が重複しているため読み込みを中止しました: {dllFile}"); + Destroy((MonoBehaviour)plugin); + continue; + } + + //受信したコマンドを型解決できるよう、Initializeより前に登録しておく + UnityMemoryMappedFile.PipeCommands.RegisterPluginCommandTypes(plugin.CommandTypes); + + plugin.Initialize(host); + + loadedPlugins.Add(new LoadedPlugin + { + Id = plugin.Id, + DisplayName = plugin.DisplayName, + Version = plugin.Version, + AssemblyPath = dllFile, + Instance = plugin, + }); + + Debug.Log($"[Plugin] {plugin.DisplayName} {plugin.Version} を読み込みました"); + } + catch (Exception ex) + { + //1つのプラグインの失敗で本体が起動しなくなるのは避ける + Debug.LogError($"[Plugin] 初期化に失敗しました: {type.FullName}"); + Debug.LogException(ex); + } + } + } + } +} diff --git a/Assets/Scripts/Plugin/PluginManager.cs.meta b/Assets/Scripts/Plugin/PluginManager.cs.meta new file mode 100644 index 00000000..aa9aa97e --- /dev/null +++ b/Assets/Scripts/Plugin/PluginManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c333a259b6b39a342aa55d3232f39e68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Plugin/PluginSettingsMigration.cs b/Assets/Scripts/Plugin/PluginSettingsMigration.cs new file mode 100644 index 00000000..fb906072 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginSettingsMigration.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC +{ + /// + /// mocopi / VIVE / Tobii が本体機能だった頃の設定を、プラグインの設定領域へ移す。 + /// + /// 旧フィールド(Settings.mocopi_* 等)は設定ファイルの互換のため残してある。 + /// 移行後はプラグイン側の値が正となり、旧フィールドは読み書きされない + /// (設定ファイル内の値はそのまま保持され、古いバージョンでも読める)。 + /// + /// 本体側にプラグイン固有の知識が残るのはこのファイルだけで、 + /// 役目は設定ファイル1つにつき一度きりの移行に限られる。 + /// + internal static class PluginSettingsMigration + { + private const string MigratedKey = "_migrated/DevicePlugins"; + + public static void Migrate(Settings settings) + { + if (settings == null) return; + if (settings.PluginSettings == null) settings.PluginSettings = new Dictionary(); + if (settings.PluginSettings.ContainsKey(MigratedKey)) return; + + try + { + MigrateMocopi(settings); + MigrateViveSR(settings); + MigrateTobii(settings); + } + catch (Exception ex) + { + //移行に失敗しても起動は続ける(プラグイン側の既定値で動く) + Debug.LogWarning($"[Plugin] 旧設定の移行に失敗しました: {ex.Message}"); + } + + settings.PluginSettings[MigratedKey] = "true"; + } + + private static void Set(Settings settings, string pluginId, string key, T value) + { + settings.PluginSettings[pluginId + "/" + key] = sh_akira.Json.Serializer.Serialize(value); + } + + private static void MigrateMocopi(Settings s) + { + //mocopiプラグインは設定を mocopi_SetSetting の形そのままで1キーに保存する + Set(s, "mocopi", "Setting", new MocopiSetting + { + enable = s.mocopi_Enable, + port = s.mocopi_Port, + ApplyRootPosition = s.mocopi_ApplyRootPosition, + ApplyRootRotation = s.mocopi_ApplyRootRotation, + ApplyChest = s.mocopi_ApplyChest, + ApplySpine = s.mocopi_ApplySpine, + ApplyHead = s.mocopi_ApplyHead, + ApplyLeftArm = s.mocopi_ApplyLeftArm, + ApplyRightArm = s.mocopi_ApplyRightArm, + ApplyLeftHand = s.mocopi_ApplyLeftHand, + ApplyRightHand = s.mocopi_ApplyRightHand, + ApplyLeftLeg = s.mocopi_ApplyLeftLeg, + ApplyRightLeg = s.mocopi_ApplyRightLeg, + ApplyLeftFoot = s.mocopi_ApplyLeftFoot, + ApplyRightFoot = s.mocopi_ApplyRightFoot, + CorrectHipBone = s.mocopi_CorrectHipBone, + }); + } + + private static void MigrateViveSR(Settings s) + { + const string id = "ViveSR"; + Set(s, id, "EyeEnable", s.EyeTracking_ViveProEyeEnable); + Set(s, id, "EyeScaleHorizontal", s.EyeTracking_ViveProEyeScaleHorizontal); + Set(s, id, "EyeScaleVertical", s.EyeTracking_ViveProEyeScaleVertical); + Set(s, id, "EyeOffsetHorizontal", s.EyeTracking_ViveProEyeOffsetHorizontal); + Set(s, id, "EyeOffsetVertical", s.EyeTracking_ViveProEyeOffsetVertical); + Set(s, id, "UseEyelidMovements", s.EyeTracking_ViveProEyeUseEyelidMovements); + Set(s, id, "LipEnable", s.LipTracking_ViveEnable); + if (s.LipShapesToBlendShapeMap != null) + { + Set(s, id, "LipShapesToBlendShapeMap", s.LipShapesToBlendShapeMap); + } + } + + private static void MigrateTobii(Settings s) + { + const string id = "Tobii"; + Set(s, id, "ScaleHorizontal", s.EyeTracking_TobiiScaleHorizontal); + Set(s, id, "ScaleVertical", s.EyeTracking_TobiiScaleVertical); + Set(s, id, "OffsetHorizontal", s.EyeTracking_TobiiOffsetHorizontal); + Set(s, id, "OffsetVertical", s.EyeTracking_TobiiOffsetVertical); + Set(s, id, "CenterX", s.EyeTracking_TobiiCenterX); + Set(s, id, "CenterY", s.EyeTracking_TobiiCenterY); + + var position = s.EyeTracking_TobiiPosition; + if (position != null) + { + //プラグイン側が持つ形(TobiiPlugin.StoredTransform)に合わせて書き出す。 + //旧データは親なしのTransformなので、localの値がそのままワールド値になる + Set(s, id, "MonitorPosition", new TobiiMonitorPosition + { + px = position.localPosition.x, + py = position.localPosition.y, + pz = position.localPosition.z, + rx = position.localRotation.x, + ry = position.localRotation.y, + rz = position.localRotation.z, + rw = position.localRotation.w, + }); + } + } + + /// TobiiPlugin.StoredTransform と同じ形 + [Serializable] + private class TobiiMonitorPosition + { + public float px, py, pz; + public float rx, ry, rz, rw; + } + + /// + /// VMC.Plugin.Commands.mocopi_SetSetting と同じ形。 + /// JSONはメンバー名で対応付けるので、名前を揃えておけば読み込める。 + /// + [Serializable] + private class MocopiSetting + { + public bool enable { get; set; } + public int port { get; set; } + public bool ApplyRootPosition { get; set; } + public bool ApplyRootRotation { get; set; } + public bool ApplyChest { get; set; } + public bool ApplySpine { get; set; } + public bool ApplyHead { get; set; } + public bool ApplyLeftArm { get; set; } + public bool ApplyRightArm { get; set; } + public bool ApplyLeftHand { get; set; } + public bool ApplyRightHand { get; set; } + public bool ApplyLeftLeg { get; set; } + public bool ApplyRightLeg { get; set; } + public bool ApplyLeftFoot { get; set; } + public bool ApplyRightFoot { get; set; } + public bool CorrectHipBone { get; set; } + } + } +} diff --git a/Assets/Scripts/Plugin/PluginSettingsMigration.cs.meta b/Assets/Scripts/Plugin/PluginSettingsMigration.cs.meta new file mode 100644 index 00000000..4201dc50 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginSettingsMigration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9189e2795b544eb42b0f25e4b4ead1e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Plugin/PluginSettingsStore.cs b/Assets/Scripts/Plugin/PluginSettingsStore.cs new file mode 100644 index 00000000..d60690a5 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginSettingsStore.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine; +using VMC.Plugin; + +namespace VMC +{ + /// + /// プラグイン設定を本体の設定ファイル(プロファイル)へ保存する実装。 + /// + /// Settings.Current.PluginSettings に "プラグインID/キー" → JSON文字列 の形で持つ。 + /// Settings.Current はプロファイル切り替えで差し替わるため、値はキャッシュせず + /// 毎回 Settings.Current を見に行く。 + /// + internal class PluginSettingsStore : IPluginSettings + { + private readonly string prefix; + + public PluginSettingsStore(string pluginId) + { + prefix = pluginId + "/"; + } + + private static System.Collections.Generic.Dictionary Store + { + get + { + if (Settings.Current.PluginSettings == null) + { + Settings.Current.PluginSettings = new System.Collections.Generic.Dictionary(); + } + return Settings.Current.PluginSettings; + } + } + + public T Get(string key, T defaultValue = default) + { + if (Store.TryGetValue(prefix + key, out var json) == false) return defaultValue; + if (string.IsNullOrEmpty(json)) return defaultValue; + try + { + return sh_akira.Json.Serializer.Deserialize(json); + } + catch (Exception ex) + { + //壊れた値で起動できなくなるのは避けたいので、既定値へフォールバックする + Debug.LogWarning($"[Plugin] 設定の読み込みに失敗しました: {prefix + key} ({ex.Message})"); + return defaultValue; + } + } + + public void Set(string key, T value) + { + try + { + Store[prefix + key] = sh_akira.Json.Serializer.Serialize(value); + } + catch (Exception ex) + { + Debug.LogError($"[Plugin] 設定の保存に失敗しました: {prefix + key} ({ex.Message})"); + } + } + } +} diff --git a/Assets/Scripts/Plugin/PluginSettingsStore.cs.meta b/Assets/Scripts/Plugin/PluginSettingsStore.cs.meta new file mode 100644 index 00000000..ecc3eb42 --- /dev/null +++ b/Assets/Scripts/Plugin/PluginSettingsStore.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 202c268e1d3d5234281f31497a210728 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/PostProcessingManager.cs b/Assets/Scripts/PostProcessingManager.cs index 7404a685..4a9f72d8 100644 --- a/Assets/Scripts/PostProcessingManager.cs +++ b/Assets/Scripts/PostProcessingManager.cs @@ -113,6 +113,23 @@ public void Apply(Settings d) vg.color.overrideState = true; vg.color.value = new Color(d.PPS_Vignette_Color_r, d.PPS_Vignette_Color_g, d.PPS_Vignette_Color_b, d.PPS_Vignette_Color_a); + var ao = sp.GetSetting(); + if (ao == null) + { + ao = sp.AddSettings(); + } + ao.active = true; + ao.enabled.overrideState = true; + ao.enabled.value = d.PPS_AO_Enable; + ao.mode.overrideState = true; + ao.mode.value = d.PPS_AO_IsScalable ? AmbientOcclusionMode.ScalableAmbientObscurance : AmbientOcclusionMode.MultiScaleVolumetricObscurance; + ao.intensity.overrideState = true; + ao.intensity.value = d.PPS_AO_Intensity; + ao.thicknessModifier.overrideState = true; + ao.thicknessModifier.value = d.PPS_AO_Thickness; + ao.color.overrideState = true; + ao.color.value = new Color(d.PPS_AO_Color_r, d.PPS_AO_Color_g, d.PPS_AO_Color_b, d.PPS_AO_Color_a); + var ca = sp.GetSetting(); if (ca == null) { diff --git a/Assets/Scripts/Setting/CommonSettings.cs b/Assets/Scripts/Setting/CommonSettings.cs new file mode 100644 index 00000000..7863e587 --- /dev/null +++ b/Assets/Scripts/Setting/CommonSettings.cs @@ -0,0 +1,53 @@ +using sh_akira; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace VMC +{ + + [Serializable] + public class CommonSettings + { + public string LoadSettingFilePathOnStart = ""; //起動時に読み込む設定ファイルパス + [OptionalField] + public bool LaunchSteamVROnStartup = true; //起動時にSteamVRを初期化する + + //初期値 + [OnDeserializing()] + internal void OnDeserializingMethod(StreamingContext context) + { + LoadSettingFilePathOnStart = ""; + LaunchSteamVROnStartup = true; + } + + + public static CommonSettings Current = new CommonSettings(); + + //共通設定の書き込み + public static void Save() + { + string path = Path.GetFullPath(Application.dataPath + "/../Settings/common.json"); + var directoryName = Path.GetDirectoryName(path); + if (Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); + File.WriteAllText(path, Json.Serializer.ToReadable(Json.Serializer.Serialize(Current))); + } + + //共通設定の読み込み + public static void Load() + { + string path = Path.GetFullPath(Application.dataPath + "/../Settings/common.json"); + if (!File.Exists(path)) + { + return; + } + Current = Json.Serializer.Deserialize(File.ReadAllText(path)); //設定を読み込み + } + } + +} diff --git a/Assets/Scripts/Setting/CommonSettings.cs.meta b/Assets/Scripts/Setting/CommonSettings.cs.meta new file mode 100644 index 00000000..acca0d46 --- /dev/null +++ b/Assets/Scripts/Setting/CommonSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59b4f75ddd85abe4f8425df72a3e659a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Setting/Settings.cs b/Assets/Scripts/Setting/Settings.cs index da9fa518..e396282c 100644 --- a/Assets/Scripts/Setting/Settings.cs +++ b/Assets/Scripts/Setting/Settings.cs @@ -1,616 +1,1102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using UnityEngine; -using UnityMemoryMappedFile; -using Valve.VR; - -namespace VMC -{ - [Serializable] - public class StoreTransform - { - public Vector3 localPosition; - public Vector3 position; - public Quaternion localRotation; - public Quaternion rotation; - public Vector3 localScale; - - public StoreTransform() { } - public StoreTransform(Transform orig) : this() - { - localPosition = orig.localPosition; - position = orig.position; - localRotation = orig.localRotation; - rotation = orig.rotation; - localScale = orig.localScale; - } - - public static StoreTransform Create(Transform orig) - { - if (orig == null) return null; - return new StoreTransform(orig); - } - - public void SetPosition(Transform orig) - { - localPosition = orig.position; - position = orig.position; - } - - public void SetPosition(Vector3 orig) - { - localPosition = orig; - position = orig; - } - - public void SetRotation(Transform orig) - { - localRotation = orig.localRotation; - rotation = orig.rotation; - } - - public void SetPositionAndRotation(Transform orig) - { - SetPosition(orig); - SetRotation(orig); - } - - public Transform ToLocalTransform(Transform saveto) - { - saveto.localPosition = localPosition; - saveto.localRotation = localRotation; - saveto.localScale = localScale; - return saveto; - } - - public Transform ToWorldTransform(Transform saveto) - { - saveto.position = position; - saveto.rotation = rotation; - saveto.localScale = localScale; - return saveto; - } - } - - [Serializable] - public class LookTargetSettings - { - public Vector3 Offset; - public float Distance; - public static LookTargetSettings Create(CameraMouseControl target) - { - return new LookTargetSettings { Offset = target.LookOffset, Distance = target.CameraDistance }; - } - public void Set(CameraMouseControl target) - { - Offset = target.LookOffset; Distance = target.CameraDistance; - } - public void ApplyTo(CameraMouseControl target) - { - target.LookOffset = Offset; target.CameraDistance = Distance; - } - public void ApplyTo(Camera camera) - { - var target = camera.GetComponent(); - if (target != null) { target.LookOffset = Offset; target.CameraDistance = Distance; } - } - } - - [Serializable] - public class Settings - { - public static Settings Current = new Settings(); - - [OptionalField] - public string AAA_0 = null; - [OptionalField] - public string AAA_1 = null; - [OptionalField] - public string AAA_2 = null; - [OptionalField] - public string AAA_3 = null; - [OptionalField] - public string AAA_SavedVersion = null; - public string VRMPath = null; - public StoreTransform headTracker = null; - public StoreTransform bodyTracker = null; - public StoreTransform leftHandTracker = null; - public StoreTransform rightHandTracker = null; - public StoreTransform leftFootTracker = null; - public StoreTransform rightFootTracker = null; - [OptionalField] - public StoreTransform leftElbowTracker = null; - [OptionalField] - public StoreTransform rightElbowTracker = null; - [OptionalField] - public StoreTransform leftKneeTracker = null; - [OptionalField] - public StoreTransform rightKneeTracker = null; - public Color BackgroundColor; - public Color CustomBackgroundColor; - public bool IsTransparent; - public bool HideBorder; - public bool IsTopMost; - public StoreTransform FreeCameraTransform = null; - public LookTargetSettings FrontCameraLookTargetSettings = null; - public LookTargetSettings BackCameraLookTargetSettings = null; - [OptionalField] - public StoreTransform PositionFixedCameraTransform = null; - [OptionalField] - public CameraTypes? CameraType = null; - [OptionalField] - public bool ShowCameraGrid = false; - [OptionalField] - public bool CameraMirrorEnable = false; - [OptionalField] - public bool WindowClickThrough; - [OptionalField] - public bool LipSyncEnable; - [OptionalField] - public string LipSyncDevice; - [OptionalField] - public float LipSyncGain; - [OptionalField] - public bool LipSyncMaxWeightEnable; - [OptionalField] - public float LipSyncWeightThreashold; - [OptionalField] - public bool LipSyncMaxWeightEmphasis; - [OptionalField] - public bool AutoBlinkEnable = false; - [OptionalField] - public float BlinkTimeMin = 1.0f; - [OptionalField] - public float BlinkTimeMax = 10.0f; - [OptionalField] - public float CloseAnimationTime = 0.06f; - [OptionalField] - public float OpenAnimationTime = 0.03f; - [OptionalField] - public float ClosingTime = 0.1f; - [OptionalField] - public string DefaultFace = "通常(NEUTRAL)"; - - [OptionalField] - public bool IsOculus; - [OptionalField] - public bool LeftCenterEnable; - [OptionalField] - public bool RightCenterEnable; - [OptionalField] - public List LeftTouchPadPoints; - [OptionalField] - public List RightTouchPadPoints; - [OptionalField] - public List LeftThumbStickPoints; - [OptionalField] - public List RightThumbStickPoints; - [OptionalField] - public List KeyActions = null; - [OptionalField] - public float LeftHandRotation = 0; //unused - [OptionalField] - public float RightHandRotation = 0; //unused - [OptionalField] - public float LeftHandPositionX; - [OptionalField] - public float LeftHandPositionY; - [OptionalField] - public float LeftHandPositionZ; - [OptionalField] - public float LeftHandRotationX; - [OptionalField] - public float LeftHandRotationY; - [OptionalField] - public float LeftHandRotationZ; - [OptionalField] - public float RightHandPositionX; - [OptionalField] - public float RightHandPositionY; - [OptionalField] - public float RightHandPositionZ; - [OptionalField] - public float RightHandRotationX; - [OptionalField] - public float RightHandRotationY; - [OptionalField] - public float RightHandRotationZ; - [OptionalField] - public int SwivelOffset; - - [OptionalField] - public Tuple Head = Tuple.Create(ETrackedDeviceClass.HMD, default(string)); - [OptionalField] - public Tuple LeftHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); - [OptionalField] - public Tuple RightHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); - [OptionalField] - public Tuple Pelvis = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple LeftFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple RightFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple LeftElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple RightElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple LeftKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - [OptionalField] - public Tuple RightKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - - [OptionalField] - public float LeftHandTrackerOffsetToBottom = 0.02f; - [OptionalField] - public float LeftHandTrackerOffsetToBodySide = 0.05f; - [OptionalField] - public float RightHandTrackerOffsetToBottom = 0.02f; - [OptionalField] - public float RightHandTrackerOffsetToBodySide = 0.05f; - - [OptionalField] - public bool EnableNormalMapFix = true; - [OptionalField] - public bool DeleteHairNormalMap = true; - - [OptionalField] - public bool WebCamEnabled = false; - [OptionalField] - public bool WebCamResize = false; - [OptionalField] - public bool WebCamMirroring = false; - [OptionalField] - public int WebCamBuffering = 0; - - [OptionalField] - public float CameraFOV = 60.0f; - [OptionalField] - public float CameraSmooth = 0.0f; - - [OptionalField] - public Color LightColor; - [OptionalField] - public float LightRotationX; - [OptionalField] - public float LightRotationY; - - [OptionalField] - public int ScreenWidth = 0; - [OptionalField] - public int ScreenHeight = 0; - [OptionalField] - public int ScreenRefreshRate = 0; - - //EyeTracking - [OptionalField] - public float EyeTracking_TobiiScaleHorizontal; - [OptionalField] - public float EyeTracking_TobiiScaleVertical; - [OptionalField] - public float EyeTracking_TobiiOffsetHorizontal; - [OptionalField] - public float EyeTracking_TobiiOffsetVertical; - [OptionalField] - public StoreTransform EyeTracking_TobiiPosition; - [OptionalField] - public float EyeTracking_TobiiCenterX; - [OptionalField] - public float EyeTracking_TobiiCenterY; - [OptionalField] - public float EyeTracking_ViveProEyeScaleHorizontal; - [OptionalField] - public float EyeTracking_ViveProEyeScaleVertical; - [OptionalField] - public float EyeTracking_ViveProEyeOffsetHorizontal; - [OptionalField] - public float EyeTracking_ViveProEyeOffsetVertical; - [OptionalField] - public bool EyeTracking_ViveProEyeUseEyelidMovements; - [OptionalField] - public bool EyeTracking_ViveProEyeEnable; - - //ExternalMotionSender - [OptionalField] - public bool ExternalMotionSenderEnable; - [OptionalField] - public string ExternalMotionSenderAddress; - [OptionalField] - public int ExternalMotionSenderPort; - [OptionalField] - public int ExternalMotionSenderPeriodStatus; - [OptionalField] - public int ExternalMotionSenderPeriodRoot; - [OptionalField] - public int ExternalMotionSenderPeriodBone; - [OptionalField] - public int ExternalMotionSenderPeriodBlendShape; - [OptionalField] - public int ExternalMotionSenderPeriodCamera; - [OptionalField] - public int ExternalMotionSenderPeriodDevices; - [OptionalField] - public bool ExternalMotionSenderResponderEnable; - [OptionalField] - public bool ExternalMotionReceiverEnable; - [OptionalField] - public List ExternalMotionReceiverEnableList; - [OptionalField] - public int ExternalMotionReceiverPort; - [OptionalField] - public List ExternalMotionReceiverPortList; - [OptionalField] - public bool ExternalMotionReceiverRequesterEnable; - [OptionalField] - public string ExternalMotionSenderOptionString; - [OptionalField] - public List MidiCCBlendShape; - [OptionalField] - public bool MidiEnable; - [OptionalField] - public Dictionary LipShapesToBlendShapeMap; - [OptionalField] - public bool LipTracking_ViveEnable; - - [OptionalField] - public bool ExternalBonesReceiverEnable; - - [OptionalField] - public bool EnableSkeletal; - - [OptionalField] - public bool TrackingFilterEnable; - [OptionalField] - public bool TrackingFilterHmdEnable; - [OptionalField] - public bool TrackingFilterControllerEnable; - [OptionalField] - public bool TrackingFilterTrackerEnable; - - [OptionalField] - public bool FixKneeRotation; - - [OptionalField] - public bool FixElbowRotation; - - [OptionalField] - public bool HandleControllerAsTracker; - - [OptionalField] - public int AntiAliasing; - - [OptionalField] - public bool VirtualMotionTrackerEnable; - [OptionalField] - public int VirtualMotionTrackerNo; - - - [OptionalField] - public bool PPS_Enable; - [OptionalField] - public bool PPS_Bloom_Enable; - [OptionalField] - public float PPS_Bloom_Intensity; - [OptionalField] - public float PPS_Bloom_Threshold; - - [OptionalField] - public bool PPS_DoF_Enable; - [OptionalField] - public float PPS_DoF_FocusDistance; - [OptionalField] - public float PPS_DoF_Aperture; - [OptionalField] - public float PPS_DoF_FocusLength; - [OptionalField] - public int PPS_DoF_MaxBlurSize; - - [OptionalField] - public bool PPS_CG_Enable; - [OptionalField] - public float PPS_CG_Temperature; - [OptionalField] - public float PPS_CG_Saturation; - [OptionalField] - public float PPS_CG_Contrast; - [OptionalField] - public float PPS_CG_Gamma; - - [OptionalField] - public bool PPS_Vignette_Enable; - [OptionalField] - public float PPS_Vignette_Intensity; - [OptionalField] - public float PPS_Vignette_Smoothness; - [OptionalField] - public float PPS_Vignette_Roundness; - - [OptionalField] - public bool PPS_CA_Enable; - [OptionalField] - public float PPS_CA_Intensity; - [OptionalField] - public bool PPS_CA_FastMode; - - [OptionalField] - public float PPS_Bloom_Color_a; - [OptionalField] - public float PPS_Bloom_Color_r; - [OptionalField] - public float PPS_Bloom_Color_g; - [OptionalField] - public float PPS_Bloom_Color_b; - - [OptionalField] - public float PPS_CG_ColorFilter_a; - [OptionalField] - public float PPS_CG_ColorFilter_r; - [OptionalField] - public float PPS_CG_ColorFilter_g; - [OptionalField] - public float PPS_CG_ColorFilter_b; - - [OptionalField] - public float PPS_Vignette_Color_a; - [OptionalField] - public float PPS_Vignette_Color_r; - [OptionalField] - public float PPS_Vignette_Color_g; - [OptionalField] - public float PPS_Vignette_Color_b; - - [OptionalField] - public bool TurnOffAmbientLight; - - //初期値 - [OnDeserializing()] - internal void OnDeserializingMethod(StreamingContext context) - { - AAA_0 = "========================================"; - AAA_1 = " Virtual Motion Capture Setting File"; - AAA_2 = " See more : vmc.info"; - AAA_3 = "========================================"; - - AAA_SavedVersion = null; - - BlinkTimeMin = 1.0f; - BlinkTimeMax = 10.0f; - CloseAnimationTime = 0.06f; - OpenAnimationTime = 0.03f; - ClosingTime = 0.1f; - DefaultFace = "通常(NEUTRAL)"; - - Head = Tuple.Create(ETrackedDeviceClass.HMD, default(string)); - LeftHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); - RightHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); - Pelvis = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - LeftFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - RightFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - LeftElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - RightElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - LeftKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - RightKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); - - LeftHandTrackerOffsetToBottom = 0.02f; - LeftHandTrackerOffsetToBodySide = 0.05f; - RightHandTrackerOffsetToBottom = 0.02f; - RightHandTrackerOffsetToBodySide = 0.05f; - - PositionFixedCameraTransform = null; - - EnableNormalMapFix = true; - DeleteHairNormalMap = true; - - CameraMirrorEnable = false; - - WebCamEnabled = false; - WebCamResize = false; - WebCamMirroring = false; - WebCamBuffering = 0; - - CameraFOV = 60.0f; - CameraSmooth = 0f; - - LightColor = Color.white; - LightRotationX = 130; - LightRotationY = 43; - - ScreenWidth = 0; - ScreenHeight = 0; - ScreenRefreshRate = 0; - - EyeTracking_TobiiScaleHorizontal = 0.5f; - EyeTracking_TobiiScaleVertical = 0.2f; - EyeTracking_ViveProEyeScaleHorizontal = 2.0f; - EyeTracking_ViveProEyeScaleVertical = 1.5f; - EyeTracking_ViveProEyeUseEyelidMovements = false; - EyeTracking_ViveProEyeEnable = false; - - EnableSkeletal = true; - - ExternalMotionSenderEnable = false; - ExternalMotionSenderAddress = "127.0.0.1"; - ExternalMotionSenderPort = 39539; - ExternalMotionSenderPeriodStatus = 1; - ExternalMotionSenderPeriodRoot = 1; - ExternalMotionSenderPeriodBone = 1; - ExternalMotionSenderPeriodBlendShape = 1; - ExternalMotionSenderPeriodCamera = 1; - ExternalMotionSenderPeriodDevices = 1; - ExternalMotionSenderOptionString = ""; - ExternalMotionSenderResponderEnable = false; - - ExternalMotionReceiverEnable = false; - ExternalMotionReceiverEnableList = null; - ExternalMotionReceiverPort = 39540; - ExternalMotionReceiverPortList = null; - ExternalMotionReceiverRequesterEnable = true; - - MidiCCBlendShape = new List(Enumerable.Repeat(default(string), MidiCCWrapper.KNOBS)); - MidiEnable = false; - - LipShapesToBlendShapeMap = new Dictionary(); - LipTracking_ViveEnable = false; - - TrackingFilterEnable = true; - TrackingFilterHmdEnable = true; - TrackingFilterControllerEnable = true; - TrackingFilterTrackerEnable = true; - - FixKneeRotation = true; - FixElbowRotation = true; - - HandleControllerAsTracker = false; - - AntiAliasing = 2; - - VirtualMotionTrackerEnable = false; - VirtualMotionTrackerNo = 50; - - PPS_Enable = false; - PPS_Bloom_Enable = false; - PPS_Bloom_Intensity = 2.7f; - PPS_Bloom_Threshold = 0.5f; - - PPS_DoF_Enable = false; - PPS_DoF_FocusDistance = 1.65f; - PPS_DoF_Aperture = 16f; - PPS_DoF_FocusLength = 16.4f; - PPS_DoF_MaxBlurSize = 3; - - PPS_CG_Enable = false; - PPS_CG_Temperature = 0f; - PPS_CG_Saturation = 0f; - PPS_CG_Contrast = 0f; - PPS_CG_Gamma = 0f; - - PPS_Vignette_Enable = false; - PPS_Vignette_Intensity = 0.65f; - PPS_Vignette_Smoothness = 0.35f; - PPS_Vignette_Roundness = 1f; - - PPS_CA_Enable = false; - PPS_CA_Intensity = 1f; - PPS_CA_FastMode = false; - - PPS_Bloom_Color_a = 1f; - PPS_Bloom_Color_r = 1f; - PPS_Bloom_Color_g = 1f; - PPS_Bloom_Color_b = 1f; - - PPS_CG_ColorFilter_a = 1f; - PPS_CG_ColorFilter_r = 1f; - PPS_CG_ColorFilter_g = 1f; - PPS_CG_ColorFilter_b = 1f; - - PPS_Vignette_Color_a = 1f; - PPS_Vignette_Color_r = 0f; - PPS_Vignette_Color_g = 0f; - PPS_Vignette_Color_b = 0f; - - TurnOffAmbientLight = false; - ExternalBonesReceiverEnable = false; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using UnityEngine; +using UnityMemoryMappedFile; +using Valve.VR; + +namespace VMC +{ + [Serializable] + public class StoreTransform + { + public Vector3 localPosition; + public Vector3 position; + public Quaternion localRotation; + public Quaternion rotation; + public Vector3 localScale; + + public StoreTransform() { } + public StoreTransform(Transform orig) : this() + { + localPosition = orig.localPosition; + position = orig.position; + localRotation = orig.localRotation; + rotation = orig.rotation; + localScale = orig.localScale; + } + + public static StoreTransform Create(Transform orig) + { + if (orig == null) return null; + return new StoreTransform(orig); + } + + public void SetPosition(Transform orig) + { + localPosition = orig.position; + position = orig.position; + } + + public void SetPosition(Vector3 orig) + { + localPosition = orig; + position = orig; + } + + public void SetRotation(Transform orig) + { + localRotation = orig.localRotation; + rotation = orig.rotation; + } + + public void SetPositionAndRotation(Transform orig) + { + SetPosition(orig); + SetRotation(orig); + } + + public Transform ToLocalTransform(Transform saveto) + { + saveto.localPosition = localPosition; + saveto.localRotation = localRotation; + saveto.localScale = localScale; + return saveto; + } + + public Transform ToWorldTransform(Transform saveto) + { + saveto.position = position; + saveto.rotation = rotation; + saveto.localScale = localScale; + return saveto; + } + } + + /// + /// キャリブレーション実行時の1つのトラッカーの姿勢(トラッキング機器から報告される生のローカル姿勢) + /// + [Serializable] + public class CalibrationTrackerPose + { + public string Name; //シリアル番号等のデバイス名(TrackingPointの識別子) + public Vector3 Position; + public Quaternion Rotation; + } + + /// + /// キャリブレーション実行時のトラッカー姿勢一式。 + /// 別のアバターを読み込んだ時にこの姿勢を再現してキャリブレーションを再実行することで、 + /// 再度Tポーズを取らなくても同じ基準でキャリブレーションできる。 + /// + [Serializable] + public class CalibrationSnapshot + { + public int CalibrateType; //PipeCommands.CalibrateType (未知の値でも壊れないようint) + public List Poses = new List(); + } + + [Serializable] + public class LookTargetSettings + { + public Vector3 Offset; + public float Distance; + public static LookTargetSettings Create(CameraMouseControl target) + { + return new LookTargetSettings { Offset = target.LookOffset, Distance = target.CameraDistance }; + } + public void Set(CameraMouseControl target) + { + Offset = target.LookOffset; Distance = target.CameraDistance; + } + public void ApplyTo(CameraMouseControl target) + { + target.LookOffset = Offset; target.CameraDistance = Distance; + } + public void ApplyTo(Camera camera) + { + var target = camera.GetComponent(); + if (target != null) { target.LookOffset = Offset; target.CameraDistance = Distance; } + } + } + + [Serializable] + public class VMCProtocolReceiverSettings + { + public bool Enable = false; + public int Port = 0; + public int DelayMs = 0; + + public string Name = "Receiver"; + + public bool ApplyRootRotation = true; + public bool ApplyRootPosition = true; + public bool ApplySpine = true; + public bool ApplyChest = true; + public bool ApplyHead = true; + public bool ApplyLeftArm = true; + public bool ApplyRightArm = true; + public bool ApplyLeftHand = true; + public bool ApplyRightHand = true; + public bool ApplyLeftLeg = true; + public bool ApplyRightLeg = true; + public bool ApplyLeftFoot = true; + public bool ApplyRightFoot = true; + public bool ApplyEye = false; + public bool ApplyLeftFinger = true; + public bool ApplyRightFinger = true; + + public bool FixHandBone = true; + public bool UseBonePosition = false; + + /// + /// 送信元が正規化(ControlRig)ボーン姿勢を送ってくる場合にtrueにする。 + /// 仕様の推奨はオリジナル(非正規化)ボーンなので既定はfalse。 + /// + [OptionalField] + public bool UseNormalizedBone = false; + [OptionalField] + public bool CorrectHipBone = false; + [OptionalField] + public bool IgnoreDefaultBone = true; + + public bool ApplyBlendShape = true; + public bool ApplyLookAt = true; + public bool ApplyTracker = true; + public bool ApplyCamera = true; + public bool ApplyLight = true; + public bool ApplyMidi = true; + public bool ApplyStatus = true; + public bool ApplyControl = true; + public bool ApplySetting = true; + [OptionalField] + public bool ApplyControllerInput = true; + [OptionalField] + public bool ApplyKeyboardInput = false; + + + //初期値 + [OnDeserializing()] + internal void OnDeserializingMethod(StreamingContext context) + { + Name = "Receiver"; + + ApplyRootRotation = true; + ApplyRootPosition = true; + ApplySpine = true; + ApplyChest = true; + ApplyHead = true; + ApplyLeftArm = true; + ApplyRightArm = true; + ApplyLeftHand = true; + ApplyRightHand = true; + ApplyLeftLeg = true; + ApplyRightLeg = true; + ApplyLeftFoot = true; + ApplyRightFoot = true; + ApplyLeftFinger = true; + ApplyRightFinger = true; + + FixHandBone = true; + IgnoreDefaultBone = true; + UseNormalizedBone = false; + + ApplyBlendShape = true; + ApplyLookAt = true; + ApplyTracker = true; + ApplyCamera = true; + ApplyLight = true; + ApplyMidi = true; + ApplyStatus = true; + ApplyControl = true; + ApplySetting = true; + ApplyControllerInput = true; + ApplyKeyboardInput = false; + } + + public VMCProtocolReceiverSettings Import(PipeCommands.SetVMCProtocolReceiverSetting setting) + { + Enable = setting.Enable; + Port = setting.Port; + DelayMs = setting.DelayMs; + + Name = setting.Name; + + ApplyRootRotation = setting.ApplyRootRotation; + ApplyRootPosition = setting.ApplyRootPosition; + ApplySpine = setting.ApplySpine; + ApplyChest = setting.ApplyChest; + ApplyHead = setting.ApplyHead; + ApplyLeftArm = setting.ApplyLeftArm; + ApplyRightArm = setting.ApplyRightArm; + ApplyLeftHand = setting.ApplyLeftHand; + ApplyRightHand = setting.ApplyRightHand; + ApplyLeftLeg = setting.ApplyLeftLeg; + ApplyRightLeg = setting.ApplyRightLeg; + ApplyLeftFoot = setting.ApplyLeftFoot; + ApplyRightFoot = setting.ApplyRightFoot; + ApplyEye = setting.ApplyEye; + ApplyLeftFinger = setting.ApplyLeftFinger; + ApplyRightFinger = setting.ApplyRightFinger; + FixHandBone = setting.CorrectHandBone; + UseBonePosition = setting.UseBonePosition; + CorrectHipBone = setting.CorrectHipBone; + IgnoreDefaultBone = setting.IgnoreDefaultBone; + + ApplyBlendShape = setting.ApplyBlendShape; + ApplyLookAt = setting.ApplyLookAt; + ApplyTracker = setting.ApplyTracker; + ApplyCamera = setting.ApplyCamera; + ApplyLight = setting.ApplyLight; + ApplyMidi = setting.ApplyMidi; + ApplyStatus = setting.ApplyStatus; + ApplyControl = setting.ApplyControl; + ApplySetting = setting.ApplySetting; + ApplyControllerInput = setting.ApplyControllerInput; + ApplyKeyboardInput = setting.ApplyKeyboardInput; + UseNormalizedBone = setting.UseNormalizedBone; + + return this; + } + + public PipeCommands.SetVMCProtocolReceiverSetting Export(int index) + { + var setting = new PipeCommands.SetVMCProtocolReceiverSetting + { + + Index = index, + Enable = Enable, + Port = Port, + Name = Name, + + ApplyRootRotation = ApplyRootRotation, + ApplyRootPosition = ApplyRootPosition, + ApplySpine = ApplySpine, + ApplyChest = ApplyChest, + ApplyHead = ApplyHead, + ApplyLeftArm = ApplyLeftArm, + ApplyRightArm = ApplyRightArm, + ApplyLeftHand = ApplyLeftHand, + ApplyRightHand = ApplyRightHand, + ApplyLeftLeg = ApplyLeftLeg, + ApplyRightLeg = ApplyRightLeg, + ApplyLeftFoot = ApplyLeftFoot, + ApplyRightFoot = ApplyRightFoot, + ApplyEye = ApplyEye, + ApplyLeftFinger = ApplyLeftFinger, + ApplyRightFinger = ApplyRightFinger, + + DelayMs = DelayMs, + + CorrectHandBone = FixHandBone, + CorrectHipBone = CorrectHipBone, + UseBonePosition = UseBonePosition, + IgnoreDefaultBone = IgnoreDefaultBone, + + ApplyBlendShape = ApplyBlendShape, + ApplyLookAt = ApplyLookAt, + ApplyTracker = ApplyTracker, + ApplyCamera = ApplyCamera, + ApplyLight = ApplyLight, + ApplyMidi = ApplyMidi, + ApplyStatus = ApplyStatus, + ApplyControl = ApplyControl, + ApplySetting = ApplySetting, + ApplyControllerInput = ApplyControllerInput, + ApplyKeyboardInput = ApplyKeyboardInput, + UseNormalizedBone = UseNormalizedBone, + }; + return setting; + } + } + + [Serializable] + public class Settings + { + public static Settings Current = new Settings(); + + [OptionalField] + public string AAA_0 = null; + [OptionalField] + public string AAA_1 = null; + [OptionalField] + public string AAA_2 = null; + [OptionalField] + public string AAA_3 = null; + [OptionalField] + public string AAA_SavedVersion = null; + public string VRMPath = null; + public StoreTransform headTracker = null; + public StoreTransform bodyTracker = null; + public StoreTransform leftHandTracker = null; + public StoreTransform rightHandTracker = null; + public StoreTransform leftFootTracker = null; + public StoreTransform rightFootTracker = null; + [OptionalField] + public StoreTransform leftElbowTracker = null; + [OptionalField] + public StoreTransform rightElbowTracker = null; + [OptionalField] + public StoreTransform leftKneeTracker = null; + [OptionalField] + public StoreTransform rightKneeTracker = null; + [OptionalField] + public StoreTransform chestTracker = null; + public Color BackgroundColor; + public Color CustomBackgroundColor; + public bool IsTransparent; + public bool HideBorder; + public bool IsTopMost; + public StoreTransform FreeCameraTransform = null; + public LookTargetSettings FrontCameraLookTargetSettings = null; + public LookTargetSettings BackCameraLookTargetSettings = null; + [OptionalField] + public StoreTransform PositionFixedCameraTransform = null; + [OptionalField] + public CameraTypes? CameraType = null; + [OptionalField] + public bool ShowCameraGrid = false; + [OptionalField] + public bool CameraMirrorEnable = false; + [OptionalField] + public bool WindowClickThrough; + [OptionalField] + public bool LipSyncEnable; + [OptionalField] + public string LipSyncDevice; + [OptionalField] + public float LipSyncGain; + [OptionalField] + public bool LipSyncMaxWeightEnable; + [OptionalField] + public float LipSyncWeightThreashold; + [OptionalField] + public bool LipSyncMaxWeightEmphasis; + [OptionalField] + public bool AutoBlinkEnable = false; + [OptionalField] + public float BlinkTimeMin = 1.0f; + [OptionalField] + public float BlinkTimeMax = 10.0f; + [OptionalField] + public float CloseAnimationTime = 0.06f; + [OptionalField] + public float OpenAnimationTime = 0.03f; + [OptionalField] + public float ClosingTime = 0.1f; + [OptionalField] + public string DefaultFace = "通常(NEUTRAL)"; + + [OptionalField] + public bool IsOculus; + [OptionalField] + public bool LeftCenterEnable; + [OptionalField] + public bool RightCenterEnable; + [OptionalField] + public List LeftTouchPadPoints; + [OptionalField] + public List RightTouchPadPoints; + [OptionalField] + public List LeftThumbStickPoints; + [OptionalField] + public List RightThumbStickPoints; + [OptionalField] + public List KeyActions = null; + [OptionalField] + public float LeftHandRotation = 0; //unused + [OptionalField] + public float RightHandRotation = 0; //unused + [OptionalField] + public float LeftHandPositionX; + [OptionalField] + public float LeftHandPositionY; + [OptionalField] + public float LeftHandPositionZ; + [OptionalField] + public float LeftHandRotationX; + [OptionalField] + public float LeftHandRotationY; + [OptionalField] + public float LeftHandRotationZ; + [OptionalField] + public float RightHandPositionX; + [OptionalField] + public float RightHandPositionY; + [OptionalField] + public float RightHandPositionZ; + [OptionalField] + public float RightHandRotationX; + [OptionalField] + public float RightHandRotationY; + [OptionalField] + public float RightHandRotationZ; + [OptionalField] + public int SwivelOffset; + + [OptionalField] + public Tuple Head = Tuple.Create(ETrackedDeviceClass.HMD, default(string)); + [OptionalField] + public Tuple LeftHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); + [OptionalField] + public Tuple RightHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); + [OptionalField] + public Tuple Pelvis = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple LeftFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple RightFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple LeftElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple RightElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple LeftKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple RightKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + [OptionalField] + public Tuple Chest = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + + [OptionalField] + public float LeftHandTrackerOffsetToBottom = 0.02f; + [OptionalField] + public float LeftHandTrackerOffsetToBodySide = 0.05f; + [OptionalField] + public float RightHandTrackerOffsetToBottom = 0.02f; + [OptionalField] + public float RightHandTrackerOffsetToBodySide = 0.05f; + + [OptionalField] + public bool WebCamEnabled = false; + [OptionalField] + public bool WebCamResize = false; + [OptionalField] + public bool WebCamMirroring = false; + [OptionalField] + public int WebCamBuffering = 0; + + [OptionalField] + public float CameraFOV = 60.0f; + [OptionalField] + public float CameraSmooth = 0.0f; + + [OptionalField] + public Color LightColor; + [OptionalField] + public float LightRotationX; + [OptionalField] + public float LightRotationY; + + [OptionalField] + public int ScreenWidth = 0; + [OptionalField] + public int ScreenHeight = 0; + [OptionalField] + public int ScreenRefreshRate = 0; + + //EyeTracking + [OptionalField] + public float EyeTracking_TobiiScaleHorizontal; + [OptionalField] + public float EyeTracking_TobiiScaleVertical; + [OptionalField] + public float EyeTracking_TobiiOffsetHorizontal; + [OptionalField] + public float EyeTracking_TobiiOffsetVertical; + [OptionalField] + public StoreTransform EyeTracking_TobiiPosition; + [OptionalField] + public float EyeTracking_TobiiCenterX; + [OptionalField] + public float EyeTracking_TobiiCenterY; + [OptionalField] + public float EyeTracking_ViveProEyeScaleHorizontal; + [OptionalField] + public float EyeTracking_ViveProEyeScaleVertical; + [OptionalField] + public float EyeTracking_ViveProEyeOffsetHorizontal; + [OptionalField] + public float EyeTracking_ViveProEyeOffsetVertical; + [OptionalField] + public bool EyeTracking_ViveProEyeUseEyelidMovements; + [OptionalField] + public bool EyeTracking_ViveProEyeEnable; + + //ExternalMotionSender + [OptionalField] + public bool ExternalMotionSenderEnable; + [OptionalField] + public string ExternalMotionSenderAddress; + [OptionalField] + public int ExternalMotionSenderPort; + [OptionalField] + public int ExternalMotionSenderPeriodStatus; + [OptionalField] + public int ExternalMotionSenderPeriodRoot; + [OptionalField] + public int ExternalMotionSenderPeriodBone; + [OptionalField] + public int ExternalMotionSenderPeriodBlendShape; + [OptionalField] + public int ExternalMotionSenderPeriodCamera; + [OptionalField] + public int ExternalMotionSenderPeriodDevices; + [OptionalField] + public bool ExternalMotionSenderResponderEnable; + [OptionalField] + public bool ExternalMotionReceiverEnable; + [OptionalField] + public List ExternalMotionReceiverEnableList; + [OptionalField] + public int ExternalMotionReceiverPort; + [OptionalField] + public List ExternalMotionReceiverPortList; + [OptionalField] + public List ExternalMotionReceiverDelayMsList; + [OptionalField] + public bool ExternalMotionReceiverRequesterEnable; + [OptionalField] + public string ExternalMotionSenderOptionString; + + /// + /// 正規化(ControlRig)ボーン姿勢を送信する。 + /// VMCProtocolの仕様ではオリジナル(非正規化)ボーンが推奨で、 + /// 正規化ボーンの送信は「既定で無効のオプション」と定められているため既定はfalse。 + /// + [OptionalField] + public bool ExternalMotionSenderUseNormalizedBone = false; + + /// + /// 表情をVRM1.0形式の名称(happy/aa等)でも送信する。 + /// 仕様ではVRM0.x形式の送信が必須で、VRM1.0形式はオプション。 + /// + [OptionalField] + public bool ExternalMotionSenderSendVRM1Expression = false; + [OptionalField] + public List MidiCCBlendShape; + [OptionalField] + public bool MidiEnable; + [OptionalField] + public Dictionary LipShapesToBlendShapeMap; + [OptionalField] + public bool LipTracking_ViveEnable; + + [OptionalField] + public bool ExternalBonesReceiverEnable; + + [OptionalField] + public List VMCProtocolReceiverSettingsList; + + [OptionalField] + public bool EnableSkeletal; + + [OptionalField] + public bool TrackingFilterEnable; + [OptionalField] + public bool TrackingFilterHmdEnable; + [OptionalField] + public bool TrackingFilterControllerEnable; + [OptionalField] + public bool TrackingFilterTrackerEnable; + + [OptionalField] + public bool FixKneeRotation; + + [OptionalField] + public bool FixElbowRotation; + + [OptionalField] + public bool HandleControllerAsTracker; + + [OptionalField] + public bool TrackerReassignmentWhenChestAvailable; + + [OptionalField] + public int AntiAliasing; + + [OptionalField] + public bool VirtualMotionTrackerEnable; + [OptionalField] + public int VirtualMotionTrackerNo; + + + [OptionalField] + public bool PPS_Enable; + [OptionalField] + public bool PPS_Bloom_Enable; + [OptionalField] + public float PPS_Bloom_Intensity; + [OptionalField] + public float PPS_Bloom_Threshold; + + [OptionalField] + public bool PPS_DoF_Enable; + [OptionalField] + public float PPS_DoF_FocusDistance; + [OptionalField] + public float PPS_DoF_Aperture; + [OptionalField] + public float PPS_DoF_FocusLength; + [OptionalField] + public int PPS_DoF_MaxBlurSize; + + [OptionalField] + public bool PPS_CG_Enable; + [OptionalField] + public float PPS_CG_Temperature; + [OptionalField] + public float PPS_CG_Saturation; + [OptionalField] + public float PPS_CG_Contrast; + [OptionalField] + public float PPS_CG_Gamma; + + [OptionalField] + public bool PPS_Vignette_Enable; + [OptionalField] + public float PPS_Vignette_Intensity; + [OptionalField] + public float PPS_Vignette_Smoothness; + [OptionalField] + public float PPS_Vignette_Roundness; + + [OptionalField] + public bool PPS_AO_Enable; + [OptionalField] + public bool PPS_AO_IsScalable; + [OptionalField] + public float PPS_AO_Intensity; + [OptionalField] + public float PPS_AO_Thickness; + + [OptionalField] + public bool PPS_CA_Enable; + [OptionalField] + public float PPS_CA_Intensity; + [OptionalField] + public bool PPS_CA_FastMode; + + [OptionalField] + public float PPS_Bloom_Color_a; + [OptionalField] + public float PPS_Bloom_Color_r; + [OptionalField] + public float PPS_Bloom_Color_g; + [OptionalField] + public float PPS_Bloom_Color_b; + + [OptionalField] + public float PPS_CG_ColorFilter_a; + [OptionalField] + public float PPS_CG_ColorFilter_r; + [OptionalField] + public float PPS_CG_ColorFilter_g; + [OptionalField] + public float PPS_CG_ColorFilter_b; + + [OptionalField] + public float PPS_Vignette_Color_a; + [OptionalField] + public float PPS_Vignette_Color_r; + [OptionalField] + public float PPS_Vignette_Color_g; + [OptionalField] + public float PPS_Vignette_Color_b; + + [OptionalField] + public float PPS_AO_Color_a; + [OptionalField] + public float PPS_AO_Color_r; + [OptionalField] + public float PPS_AO_Color_g; + [OptionalField] + public float PPS_AO_Color_b; + + [OptionalField] + public bool TurnOffAmbientLight; + + [OptionalField] + public bool mocopi_Enable; + [OptionalField] + public int mocopi_Port; + [OptionalField] + public bool mocopi_ApplyRootPosition; + [OptionalField] + public bool mocopi_ApplyRootRotation; + [OptionalField] + public bool mocopi_ApplyChest; + [OptionalField] + public bool mocopi_ApplySpine; + [OptionalField] + public bool mocopi_ApplyHead; + [OptionalField] + public bool mocopi_ApplyLeftArm; + [OptionalField] + public bool mocopi_ApplyRightArm; + [OptionalField] + public bool mocopi_ApplyLeftHand; + [OptionalField] + public bool mocopi_ApplyRightHand; + [OptionalField] + public bool mocopi_ApplyLeftLeg; + [OptionalField] + public bool mocopi_ApplyRightLeg; + [OptionalField] + public bool mocopi_ApplyLeftFoot; + [OptionalField] + public bool mocopi_ApplyRightFoot; + [OptionalField] + public bool mocopi_CorrectHipBone; + + /// + /// プラグイン(Plugins/配下)の設定。"プラグインID/キー" → JSON文字列。 + /// プロファイル切り替えでプラグインの設定も一緒に切り替わるよう、ここに保存する。 + /// + [OptionalField] + public Dictionary PluginSettings; + + //モーション再生 + [OptionalField] + public List MotionPlayback_MotionFiles; + [OptionalField] + public int MotionPlayback_RepeatMode; + [OptionalField] + public bool MotionPlayback_ApplyRootPosition; + [OptionalField] + public bool MotionPlayback_ApplyRootRotation; + [OptionalField] + public bool MotionPlayback_ApplySpine; + [OptionalField] + public bool MotionPlayback_ApplyChest; + [OptionalField] + public bool MotionPlayback_ApplyHead; + [OptionalField] + public bool MotionPlayback_ApplyLeftArm; + [OptionalField] + public bool MotionPlayback_ApplyRightArm; + [OptionalField] + public bool MotionPlayback_ApplyLeftHand; + [OptionalField] + public bool MotionPlayback_ApplyRightHand; + [OptionalField] + public bool MotionPlayback_ApplyLeftLeg; + [OptionalField] + public bool MotionPlayback_ApplyRightLeg; + [OptionalField] + public bool MotionPlayback_ApplyLeftFoot; + [OptionalField] + public bool MotionPlayback_ApplyRightFoot; + [OptionalField] + public bool MotionPlayback_ApplyLeftFinger; + [OptionalField] + public bool MotionPlayback_ApplyRightFinger; + [OptionalField] + public bool MotionPlayback_ApplyEye; + //VRMAに含まれる表情・視線を再生時に適用するか + //(オフにするとモーションのみ再生し、表情はVMCProtocol受信等の他の入力に任せられる) + [OptionalField] + public bool MotionPlayback_ApplyExpression; + [OptionalField] + public bool MotionPlayback_ApplyLookAt; + + //モーション記録 + [OptionalField] + public int MotionRecord_Fps; + [OptionalField] + public int MotionRecord_CountdownSeconds; + [OptionalField] + public bool MotionRecord_SaveMotion; + [OptionalField] + public bool MotionRecord_SaveExpressionPreset; + [OptionalField] + public bool MotionRecord_SaveExpressionCustom; + [OptionalField] + public bool MotionRecord_SaveLookAt; + + + [OptionalField] + public bool EnableOverrideBodyHeight; + [OptionalField] + public float OverrideBodyHeight; + [OptionalField] + public float PelvisOffsetAdjustY; + [OptionalField] + public float PelvisOffsetAdjustZ; + + [OptionalField] + public bool UnityChildWindowEnable; + + [OptionalField] + public int WristRotationFix_UpperArmWeight = 200; // /1000 + [OptionalField] + public int WristRotationFix_ForearmWeight = 570; // /1000 + [OptionalField] + public int WristRotationFix_MaxAccumulatedTwist = 300; + + //キャリブレーション自動再適用 + //キャリブレーション実行時のトラッカー姿勢を記録しておき、別のアバターを読み込んだ時に + //同じトラッカー姿勢でキャリブレーションを再実行することで、再度Tポーズを取る手間を省く + [OptionalField] + public bool EnableAutoCalibrationOnModelLoad = true; + [OptionalField] + public CalibrationSnapshot LastCalibrationSnapshot = null; + + //初期値 + [OnDeserializing()] + internal void OnDeserializingMethod(StreamingContext context) + { + AAA_0 = "========================================"; + AAA_1 = " Virtual Motion Capture Setting File"; + AAA_2 = " See more : vmc.info"; + AAA_3 = "========================================"; + + AAA_SavedVersion = null; + + BlinkTimeMin = 1.0f; + BlinkTimeMax = 10.0f; + CloseAnimationTime = 0.06f; + OpenAnimationTime = 0.03f; + ClosingTime = 0.1f; + DefaultFace = "通常(NEUTRAL)"; + + Head = Tuple.Create(ETrackedDeviceClass.HMD, default(string)); + LeftHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); + RightHand = Tuple.Create(ETrackedDeviceClass.Controller, default(string)); + Pelvis = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + LeftFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + RightFoot = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + LeftElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + RightElbow = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + LeftKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + RightKnee = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + Chest = Tuple.Create(ETrackedDeviceClass.GenericTracker, default(string)); + + LeftHandTrackerOffsetToBottom = 0.02f; + LeftHandTrackerOffsetToBodySide = 0.05f; + RightHandTrackerOffsetToBottom = 0.02f; + RightHandTrackerOffsetToBodySide = 0.05f; + + PositionFixedCameraTransform = null; + + CameraMirrorEnable = false; + + WebCamEnabled = false; + WebCamResize = false; + WebCamMirroring = false; + WebCamBuffering = 0; + + CameraFOV = 60.0f; + CameraSmooth = 0f; + + LightColor = Color.white; + LightRotationX = 130; + LightRotationY = 43; + + ScreenWidth = 0; + ScreenHeight = 0; + ScreenRefreshRate = 0; + + EyeTracking_TobiiScaleHorizontal = 0.5f; + EyeTracking_TobiiScaleVertical = 0.2f; + EyeTracking_ViveProEyeScaleHorizontal = 2.0f; + EyeTracking_ViveProEyeScaleVertical = 1.5f; + EyeTracking_ViveProEyeUseEyelidMovements = false; + EyeTracking_ViveProEyeEnable = false; + + EnableSkeletal = true; + + ExternalMotionSenderEnable = false; + ExternalMotionSenderAddress = "127.0.0.1"; + ExternalMotionSenderPort = 39539; + ExternalMotionSenderPeriodStatus = 1; + ExternalMotionSenderPeriodRoot = 1; + ExternalMotionSenderPeriodBone = 1; + ExternalMotionSenderPeriodBlendShape = 1; + ExternalMotionSenderPeriodCamera = 1; + ExternalMotionSenderPeriodDevices = 1; + ExternalMotionSenderOptionString = ""; + //仕様上、正規化ボーンの送信は「既定で無効のオプション」 + ExternalMotionSenderUseNormalizedBone = false; + ExternalMotionSenderSendVRM1Expression = false; + ExternalMotionSenderResponderEnable = false; + + ExternalMotionReceiverEnable = false; + ExternalMotionReceiverEnableList = null; + ExternalMotionReceiverPort = 39540; + ExternalMotionReceiverPortList = null; + ExternalMotionReceiverDelayMsList = null; + ExternalMotionReceiverRequesterEnable = true; + + MidiCCBlendShape = new List(Enumerable.Repeat(default(string), MidiCCWrapper.KNOBS)); + MidiEnable = false; + + LipShapesToBlendShapeMap = new Dictionary(); + LipTracking_ViveEnable = false; + + TrackingFilterEnable = true; + TrackingFilterHmdEnable = true; + TrackingFilterControllerEnable = true; + TrackingFilterTrackerEnable = true; + + FixKneeRotation = true; + FixElbowRotation = true; + + HandleControllerAsTracker = false; + + TrackerReassignmentWhenChestAvailable = false; + + AntiAliasing = 2; + + VirtualMotionTrackerEnable = false; + VirtualMotionTrackerNo = 50; + + PPS_Enable = false; + PPS_Bloom_Enable = false; + PPS_Bloom_Intensity = 2.7f; + PPS_Bloom_Threshold = 0.5f; + + PPS_DoF_Enable = false; + PPS_DoF_FocusDistance = 1.65f; + PPS_DoF_Aperture = 16f; + PPS_DoF_FocusLength = 16.4f; + PPS_DoF_MaxBlurSize = 3; + + PPS_CG_Enable = false; + PPS_CG_Temperature = 0f; + PPS_CG_Saturation = 0f; + PPS_CG_Contrast = 0f; + PPS_CG_Gamma = 0f; + + PPS_Vignette_Enable = false; + PPS_Vignette_Intensity = 0.65f; + PPS_Vignette_Smoothness = 0.35f; + PPS_Vignette_Roundness = 1f; + + PPS_AO_Enable = false; + PPS_AO_IsScalable = false; + PPS_AO_Intensity = 0f; + PPS_AO_Thickness = 0f; + + PPS_CA_Enable = false; + PPS_CA_Intensity = 1f; + PPS_CA_FastMode = false; + + PPS_Bloom_Color_a = 1f; + PPS_Bloom_Color_r = 1f; + PPS_Bloom_Color_g = 1f; + PPS_Bloom_Color_b = 1f; + + PPS_CG_ColorFilter_a = 1f; + PPS_CG_ColorFilter_r = 1f; + PPS_CG_ColorFilter_g = 1f; + PPS_CG_ColorFilter_b = 1f; + + PPS_Vignette_Color_a = 1f; + PPS_Vignette_Color_r = 0f; + PPS_Vignette_Color_g = 0f; + PPS_Vignette_Color_b = 0f; + + PPS_AO_Color_a = 0f; + PPS_AO_Color_r = 0f; + PPS_AO_Color_g = 0f; + PPS_AO_Color_b = 0f; + + TurnOffAmbientLight = false; + ExternalBonesReceiverEnable = false; + + VMCProtocolReceiverSettingsList = new List(); + + mocopi_Enable = true; + mocopi_Port = 12351; + mocopi_ApplyRootPosition = true; + mocopi_ApplyRootRotation = true; + mocopi_ApplyChest = true; + mocopi_ApplySpine = true; + mocopi_ApplyHead = true; + mocopi_ApplyLeftArm = true; + mocopi_ApplyRightArm = true; + mocopi_ApplyLeftHand = true; + mocopi_ApplyRightHand = true; + mocopi_ApplyLeftLeg = true; + mocopi_ApplyRightLeg = true; + mocopi_ApplyLeftFoot = true; + mocopi_ApplyRightFoot = true; + mocopi_CorrectHipBone = false; + + MotionPlayback_MotionFiles = new List(); + MotionPlayback_RepeatMode = 0; + MotionPlayback_ApplyRootPosition = true; + MotionPlayback_ApplyRootRotation = true; + MotionPlayback_ApplySpine = true; + MotionPlayback_ApplyChest = true; + MotionPlayback_ApplyHead = true; + MotionPlayback_ApplyLeftArm = true; + MotionPlayback_ApplyRightArm = true; + MotionPlayback_ApplyLeftHand = true; + MotionPlayback_ApplyRightHand = true; + MotionPlayback_ApplyLeftLeg = true; + MotionPlayback_ApplyRightLeg = true; + MotionPlayback_ApplyLeftFoot = true; + MotionPlayback_ApplyRightFoot = true; + MotionPlayback_ApplyLeftFinger = true; + MotionPlayback_ApplyRightFinger = true; + MotionPlayback_ApplyEye = true; + MotionPlayback_ApplyExpression = true; + MotionPlayback_ApplyLookAt = true; + + MotionRecord_Fps = 60; + MotionRecord_CountdownSeconds = 3; + MotionRecord_SaveMotion = true; + MotionRecord_SaveExpressionPreset = true; + MotionRecord_SaveExpressionCustom = true; + MotionRecord_SaveLookAt = true; + + EnableOverrideBodyHeight = false; + OverrideBodyHeight = 1.7f; + PelvisOffsetAdjustY = 0; + PelvisOffsetAdjustZ = 0; + + UnityChildWindowEnable = false; + + WristRotationFix_UpperArmWeight = 200; + WristRotationFix_ForearmWeight = 570; + WristRotationFix_MaxAccumulatedTwist = 300; + + EnableAutoCalibrationOnModelLoad = true; + LastCalibrationSnapshot = null; + } + + /// + /// 指定したバージョンより前の設定ファイルかどうか(指定バージョンは含まない) + /// + /// + /// + /// + public bool IsSettingVersionBefore(int major, int minor) + { + if (major < 0 || minor < 0 || (major == 0 && minor < 48)) + throw new ArgumentOutOfRangeException(nameof(minor), "over 0.48 only"); + + if (string.IsNullOrWhiteSpace(AAA_SavedVersion)) + { + //before 0.47 _SaveVersion is null. + return major > 0 || (major == 0 && minor > 47); + } + else + { + var split = AAA_SavedVersion.Replace("v", "").Split('.'); + int pmajor, pminor; + if (split.Length == 2 && int.TryParse(split[0], out pmajor) && int.TryParse(split[1], out pminor)) + { + return major > pmajor || (major == pmajor && minor > pminor); + } + else + { + // parse failed + return false; + } + } + } + } +} diff --git a/Assets/Scripts/SteamVRWrapper2.0/SteamVR2Input.cs b/Assets/Scripts/SteamVRWrapper2.0/SteamVR2Input.cs index ee2a6637..4cf090ff 100644 --- a/Assets/Scripts/SteamVRWrapper2.0/SteamVR2Input.cs +++ b/Assets/Scripts/SteamVRWrapper2.0/SteamVR2Input.cs @@ -97,16 +97,17 @@ public class SteamVRActions public List default_bindings; } - private Dictionary LastPositions = new Dictionary(); + private Dictionary<(ulong ulRestrictedToDevice, ulong handle), (string name, Vector3 axis)> LastPositions = new Dictionary<(ulong ulRestrictedToDevice, ulong handle), (string name, Vector3 axis)>(); - private Vector3 GetLastPosition(string shortName) + private Vector3 GetLastPosition(string shortName, bool isLeft) { Vector3 axis = Vector3.zero; var partname = shortName.Substring("Touch".Length); - var key = LastPositions.Keys.FirstOrDefault(d => d.Contains(partname)); - if (key != null) + var startsWith = isLeft ? "Left" : "Right"; + var value = LastPositions.Values.FirstOrDefault(d => d.name.StartsWith(startsWith) && d.name.Contains(partname)); + if (value.name != null) { - axis = LastPositions[key]; + axis = value.axis; } return axis; } @@ -215,16 +216,18 @@ void Update() Debug.Log($"[SteamVR] GetDigitalActionData IsKeyDown ({action.name}): {err} handle: {action.handle}"); bool isTouch = action.ShortName.StartsWith("Touch") && action.ShortName.Contains("Trigger") == false; - Vector3 axis = isTouch ? GetLastPosition(action.ShortName) : Vector3.zero; - KeyDownEvent?.Invoke(this, new OVRKeyEventArgs(action.ShortName, axis, actionset.IsLeft != handSwap, axis != Vector3.zero, isTouch)); + bool isStick = action.ShortName.Contains("Stick"); + Vector3 axis = isTouch ? GetLastPosition(action.ShortName, actionset.IsLeft) : Vector3.zero; + KeyDownEvent?.Invoke(this, new OVRKeyEventArgs(action.ShortName, axis, actionset.IsLeft != handSwap, axis != Vector3.zero || isStick, isTouch)); } if (IsKeyUp(action.digitalActionData)) { Debug.Log($"[SteamVR] GetDigitalActionData IsKeyUp ({action.name}): {err} handle: {action.handle}"); bool isTouch = action.ShortName.StartsWith("Touch") && action.ShortName.Contains("Trigger") == false; - Vector3 axis = isTouch ? GetLastPosition(action.ShortName) : Vector3.zero; - KeyUpEvent?.Invoke(this, new OVRKeyEventArgs(action.ShortName, axis, actionset.IsLeft != handSwap, axis != Vector3.zero, isTouch)); + bool isStick = action.ShortName.Contains("Stick"); + Vector3 axis = isTouch ? GetLastPosition(action.ShortName, actionset.IsLeft) : Vector3.zero; + KeyUpEvent?.Invoke(this, new OVRKeyEventArgs(action.ShortName, axis, actionset.IsLeft != handSwap, axis != Vector3.zero || isStick, isTouch)); } } else if (action.type == "vector1" || action.type == "vector2" || action.type == "vector3") @@ -237,11 +240,20 @@ void Update() Debug.LogWarning($"[SteamVR] GetAnalogActionData error ({action.name}): {err} handle: {action.handle}"); continue; } - //Debug.Log($"[SteamVR] GetAnalogActionData Position:{action.analogActionData.x},{action.analogActionData.y} ({action.name}): {err} handle: {action.handle}"); + if (action.analogActionData.bActive == false || action.ShortName.Contains("Grip")) //QuestのGripアナログは捨てる + { + continue; + } + bool isStick = action.ShortName.Contains("Stick"); var axis = new Vector3(action.analogActionData.x, action.analogActionData.y, action.analogActionData.z); - if (axis != Vector3.zero) + var startsWith = actionset.IsLeft ? "Left" : "Right"; + var name = startsWith + action.name; + // 初めてか、axisがゼロじゃない時か、前回がゼロ以外だったら1発ゼロは取得する(Oculusでスティックを倒したまま指を離したとき対策) + // ここで名前ではなくhandleで取り回さないとLeftHandとRightHand以外のActionSetの分でゼロが入って狂う + var key = (actionset.ulRestrictedToDevice, action.handle); + if (axis != Vector3.zero || (isStick && LastPositions.ContainsKey(key) == true && LastPositions[key].axis != Vector3.zero)) { - LastPositions[action.name] = axis; + LastPositions[key] = (name, axis); AxisChangedEvent?.Invoke(this, new OVRKeyEventArgs(action.ShortName, axis, actionset.IsLeft != handSwap, true, false)); } } diff --git a/Assets/Scripts/Tracking/OpenVRTrackerManager.cs b/Assets/Scripts/Tracking/OpenVRTrackerManager.cs index 289b080b..a4582dff 100644 --- a/Assets/Scripts/Tracking/OpenVRTrackerManager.cs +++ b/Assets/Scripts/Tracking/OpenVRTrackerManager.cs @@ -15,6 +15,9 @@ public class OpenVRTrackerManager : MonoBehaviour private bool isOVRConnected = false; + public Action OpenVREventAction = null; + public bool isDashboardActivated = false; + private void Awake() { Instance = this; @@ -25,8 +28,16 @@ private void Start() Setup(); } + private void OnDestroy() + { + Close(); + } + private bool Setup() { + CommonSettings.Load(); + if (CommonSettings.Current.LaunchSteamVROnStartup == false) return false; + var error = EVRInitError.None; openVR = OpenVR.Init(ref error, EVRApplicationType.VRApplication_Overlay); @@ -195,6 +206,16 @@ public bool IsSafeMode() return en; } + public bool IsDashboardVisible() + { + if (openVR == null) + { + return false; + } + + return OpenVR.Overlay?.IsDashboardVisible() ?? false; + } + //コントローラ状態を調べる public void GetControllerSerial(out string LeftHandSerial, out string RightHandSerial) { @@ -227,6 +248,7 @@ public void GetControllerSerial(out string LeftHandSerial, out string RightHandS private void Close() { + isOVRConnected = false; openVR = null; OpenVR.Shutdown(); } @@ -237,6 +259,12 @@ private void Update() { PollingVREvents(); GetAllDevicePose(); + + bool dashboardVisible = IsDashboardVisible(); + if (isDashboardActivated != dashboardVisible) { + isDashboardActivated = dashboardVisible; + OpenVREventAction?.Invoke(); + } } } } diff --git a/Assets/Scripts/Tracking/TrackingPointManager.cs b/Assets/Scripts/Tracking/TrackingPointManager.cs index 0d9af61c..d5118acf 100644 --- a/Assets/Scripts/Tracking/TrackingPointManager.cs +++ b/Assets/Scripts/Tracking/TrackingPointManager.cs @@ -23,10 +23,34 @@ private void Awake() private Dictionary ControllerTrackingPoints = new Dictionary(); private Dictionary TrackerTrackingPoints = new Dictionary(); + /// + /// キャリブレーション再現用の姿勢上書き。 + /// 設定されている間、対象のトラッキングポイントは実機の入力ではなくここで指定した姿勢で更新される。 + /// (キャリブレーションは複数フレームにまたがるため、実機入力で上書きされないようにする) + /// + private Dictionary overridePoses = null; + + public void SetPoseOverride(Dictionary poses) + { + overridePoses = poses; + } + + public void ClearPoseOverride() + { + overridePoses = null; + } + public TrackingPoint ApplyPoint(string name, ETrackedDeviceClass deviceClass, Vector3 position, Quaternion rotation, bool isOK) { //ignore "LIV Virtual Camera" + //キャリブレーション再現中は記録済みの姿勢で固定する + if (overridePoses != null && overridePoses.TryGetValue(name, out var overridePose)) + { + position = overridePose.position; + rotation = overridePose.rotation; + } + if (AllTrackingPoints.TryGetValue(name, out var trackingPoint) == false) { trackingPoint = new TrackingPoint(name, deviceClass); @@ -152,10 +176,20 @@ public TrackingPoint(string name, ETrackedDeviceClass deviceClass) /// /// /// 移動していたらtrue + /// + /// 最後に適用されたローカル姿勢(トラッキング機器から報告された生の値)。 + /// キャリブレーション時の姿勢を記録して後から再現するために保持する。 + /// + public Vector3 LastLocalPosition { get; private set; } + public Quaternion LastLocalRotation { get; private set; } = Quaternion.identity; + public bool SetPositionAndRotationLocal(Vector3 position, Quaternion rotation) { bool moved = false; + LastLocalPosition = position; + LastLocalRotation = rotation; + if (Vector3.Distance(lastMovedPosition, position) > 0.1f) { moved = true; diff --git a/Assets/Scripts/Tracking/VMCProtocolTrackerManager.cs b/Assets/Scripts/Tracking/VMCProtocolTrackerManager.cs index 8ab26592..7fc6c399 100644 --- a/Assets/Scripts/Tracking/VMCProtocolTrackerManager.cs +++ b/Assets/Scripts/Tracking/VMCProtocolTrackerManager.cs @@ -8,7 +8,7 @@ namespace VMC public class VMCProtocolTrackerManager : MonoBehaviour { public ControlWPFWindow controlWPFWindow; - private ExternalReceiverForVMC[] externalReceivers => controlWPFWindow.externalMotionReceivers; + private List externalReceivers => controlWPFWindow.externalMotionReceivers; private Dictionary allDeviceInfo = new Dictionary(); @@ -18,13 +18,14 @@ private void Update() { foreach (var externalReceiver in externalReceivers) { + foreach (var c in externalReceiver.virtualHmdFiltered) + { + UpdateDeviceInfo(c.Key, c.Value, ETrackedDeviceClass.HMD); + } foreach (var c in externalReceiver.virtualControllerFiltered) { UpdateDeviceInfo(c.Key, c.Value, ETrackedDeviceClass.Controller); } - } - foreach (var externalReceiver in externalReceivers) - { foreach (var t in externalReceiver.virtualTrackerFiltered) { UpdateDeviceInfo(t.Key, t.Value, ETrackedDeviceClass.GenericTracker); diff --git a/Assets/Scripts/Utils/NativeMethods.cs b/Assets/Scripts/Utils/NativeMethods.cs index cde7977f..3a8cba26 100644 --- a/Assets/Scripts/Utils/NativeMethods.cs +++ b/Assets/Scripts/Utils/NativeMethods.cs @@ -33,6 +33,16 @@ public struct RECT public int top; public int right; public int bottom; + + public int width => right - left; + public int height => bottom - top; + } + + public enum PROCESS_DPI_AWARENESS + { + Process_DPI_Unaware = 0, + Process_System_DPI_Aware = 1, + Process_Per_Monitor_DPI_Aware = 2 } [DllImport("user32.dll")] @@ -74,7 +84,12 @@ public static bool IsWindowActive() [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); [DllImport("user32.dll")] - public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + [DllImport("Shcore.dll")] + public static extern int SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); + [DllImport("user32.dll")] + public static extern bool SetProcessDPIAware(); + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); public static readonly IntPtr HWND_TOP = new IntPtr(0); @@ -104,8 +119,9 @@ public enum SetWindowPosFlags : uint public static RECT GetUnityWindowClientPosition() { RECT r; GetClientRect(GetUnityWindowHandle(), out r); return r; } public static void SetUnityWindowPosition(int x, int y) => SetWindowPos(GetUnityWindowHandle(), IntPtr.Zero, x, y, 0, 0, SetWindowPosFlags.IgnoreResize); public static void SetUnityWindowSize(int width, int height) => SetWindowPos(GetUnityWindowHandle(), IntPtr.Zero, 0, 0, width, height, SetWindowPosFlags.IgnoreMove); + public static void SetUnityWindowFrameChanged() => SetWindowPos(GetUnityWindowHandle(), IntPtr.Zero, 0, 0, 0, 0, SetWindowPosFlags.IgnoreMoveAndResize | SetWindowPosFlags.IgnoreZOrder | SetWindowPosFlags.FrameChanged | SetWindowPosFlags.ShowWindow); public static void SetUnityWindowTopMost(bool enable) => SetWindowPos(GetUnityWindowHandle(), enable ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SetWindowPosFlags.IgnoreMoveAndResize); - public static void SetUnityWindowTitle(string title) => SetWindowText(GetUnityWindowHandle(), title); + public static void SetUnityWindowTitle(string title) => SetWindowText(GetUnityWindowHandle(), title); [DllImport("Dwmapi.dll")] public static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref DwmMargin margins); @@ -118,6 +134,7 @@ public static void SetDwmTransparent(bool enable) public const int GWL_STYLE = -16; public const uint WS_POPUP = 0x80000000; public const uint WS_VISIBLE = 0x10000000; + public const uint WS_CLIPCHILDREN = 0x02000000; public const int GWL_EXSTYLE = -20; public const uint WS_EX_LAYERED = 0x00080000; public const uint WS_EX_TRANSPARENT = 0x00000020; diff --git a/Assets/Scripts/VRoidSDKConnector.cs b/Assets/Scripts/VRoidSDKConnector.cs new file mode 100644 index 00000000..249f39df --- /dev/null +++ b/Assets/Scripts/VRoidSDKConnector.cs @@ -0,0 +1,566 @@ +// VRoid SDK(Assets/VRoidSDK)が同梱されている場合のみコンパイルする。 +// SDK未同梱でクローンした場合はEditorスクリプト(VRoidSDKDefineConfigurator)がVMC_VROIDSDKを未定義にし、 +// このファイル全体が除外されてビルドが通る。SDK参照はこの1ファイルに閉じている。 +#if VMC_VROIDSDK +using Pixiv.VroidSdk; +using Pixiv.VroidSdk.Api; +using Pixiv.VroidSdk.Api.DataModel; +using Pixiv.VroidSdk.Browser; +using Pixiv.VroidSdk.Cache; +using Pixiv.VroidSdk.Cache.DataModel; +using Pixiv.VroidSdk.Cache.Migrate; +using Pixiv.VroidSdk.IO; +using Pixiv.VroidSdk.Networking.Drivers; +using Pixiv.VroidSdk.Oauth; +using Pixiv.VroidSdk.Unity.Crypt; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using UniGLTF; +using UniGLTF.Extensions.VRMC_vrm; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; +using VMCMod; +using VRoidSDK.Examples.Core.Model; + +namespace VMC +{ + public class VRoidSDKConnector : MonoBehaviour + { + [SerializeField] + private ControlWPFWindow controlWPFWindow; + [SerializeField] + private ModManager modManager; + + private ApiModel _model; + private Client _oauthClient; + private DefaultApi _api; + private IManualCodeRegistrable _browser; + + private MemoryMappedFileServer server; + + private List _characterModels; + + //各フィルタの次ページ取得用リンク(カーソルページネーション) + private Dictionary _nextLinks = new Dictionary(); + + private System.Threading.SynchronizationContext context = null; + + void Awake() + { + modManager.OnBeforeModLoad += () => + { + if (server != null) + { + server.ReceivedEvent -= Server_Received; + } + controlWPFWindow = null; + server = null; + DestroyImmediate(gameObject); + }; + } + + // Use this for initialization + void Start() + { + context = System.Threading.SynchronizationContext.Current; + } + + // Update is called once per frame + void Update() + { + if (server == null) + { + if (controlWPFWindow != null) + { + server = controlWPFWindow.server; + if (server != null) + { + server.ReceivedEvent += Server_Received; + } + } + } + } + private ISdkConfig LoadConfigFromTextAsset() + { + var asset = Resources.Load("credential.json"); + if (asset == null) + { + throw new NullReferenceException("You have to place the credential.json.bytes in any of the Resources folders"); + } + + try + { + return OauthProvider.CreateSdkConfig(asset.text); + } + catch (SerializationException) + { + Debug.LogError($"Could not parse textAsset: {asset.text}"); + throw; + } + } + + private void Server_Received(object sender, DataReceivedEventArgs e) + { + context.Post(s => + { + if (e.CommandType == typeof(PipeCommands.VRoidSDK_StartAuthenticate)) + { + StartAuthenticate(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_RegisterCode)) + { + var d = (PipeCommands.VRoidSDK_RegisterCode)e.Data; + RegisterCode(d.Code); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_DoLogin)) + { + DoLogin(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_Logout)) + { + Logout(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_RequestAccountCharacterModels)) + { + RequestAccountCharacterModels(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_RequestHearts)) + { + RequestHearts(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_RequestRecommend)) + { + RequestRecommend(); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_RequestMoreModels)) + { + var d = (PipeCommands.VRoidSDK_RequestMoreModels)e.Data; + RequestMoreModels(d.ModelFilter); + } + else if (e.CommandType == typeof(PipeCommands.VRoidSDK_LoadModel)) + { + var d = (PipeCommands.VRoidSDK_LoadModel)e.Data; + LoadModel(d.id); + } + }, null); + } + + public void RequestAccountCharacterModels() + { + // VRoid Hubにて、自身が制作し登録したキャラクターモデルの一覧を取得(次ページ用リンク付き) + _api.GetAccountCharacterModels( + 10, // 最初の10件を取得 + (models, link) => GetModels_OnSuccess(models, link, PipeCommands.ModelFilters.Account, append: false), + GetModels_OnError + ); + } + + public void RequestHearts() + { + // VRoid Hubにて、ハートしたキャラクターモデルの一覧を取得 (※ 利用条件次第では含まれないものもある) + _api.GetHearts( + 10, + (models, link) => GetModels_OnSuccess(models, link, PipeCommands.ModelFilters.Heart, append: false), + GetModels_OnError + ); + } + + public void RequestRecommend() + { + // スタッフピック(おすすめ)の一覧を取得。旧実装のID固定バッチから正式なページ対応APIへ変更 + _api.GetStaffPicks( + 10, + (staffPicks, link) => GetModels_OnSuccess(staffPicks.Select(x => x.character_model).ToList(), link, PipeCommands.ModelFilters.Recommend, append: false), + GetModels_OnError + ); + } + + /// + /// スクロール最下部で、指定フィルタ(通常は一番下のグループ)の次ページを取得して末尾へ追記する + /// + public void RequestMoreModels(PipeCommands.ModelFilters filter) + { + if (_nextLinks.TryGetValue(filter, out var link) == false) return; + if (link?.next == null) return; + + if (filter == PipeCommands.ModelFilters.Recommend) + { + link.next.RequestLink>( + (staffPicks, nextLink) => GetModels_OnSuccess(staffPicks.Select(x => x.character_model).ToList(), nextLink, filter, append: true), + GetModels_OnError); + } + else + { + link.next.RequestLink>( + (models, nextLink) => GetModels_OnSuccess(models, nextLink, filter, append: true), + GetModels_OnError); + } + } + + /* 正常にキャラクターの情報が取得できた時の処理。VRM1.0モデルも含めて全て返す */ + private async void GetModels_OnSuccess(List characterModels, ApiLinksFormat link, PipeCommands.ModelFilters modelFilter, bool append) + { + _characterModels.AddRange(characterModels); + _nextLinks[modelFilter] = link; + await server.SendCommandAsync(new PipeCommands.VRoidSDK_ReturnModels + { + ModelFilter = modelFilter, + Models = ConvertCharacterModelListToPipe(characterModels), + Append = append, + HasNext = link != null && link.next != null, + }); + } + + /* 通信エラーなどのエラーが発生した時の処理 */ + private async void GetModels_OnError(ApiErrorFormat errorFormat) + { + await server.SendCommandAsync(new PipeCommands.VRoidSDK_Error { Message = $"Code:{errorFormat.code} Message:{errorFormat.message}" }); + } + + public async void StartAuthenticate() + { + // 認証処理用インスタンスの初期化 + if (_oauthClient == null) + { + var config = LoadConfigFromTextAsset(); + var driver = new HttpClientDriver(context); + _oauthClient = OauthProvider.CreateOauthClient(config, driver); + _browser = BrowserProvider.Create(_oauthClient, config); + _api = new DefaultApi(_oauthClient); + _model = new ApiModel(_oauthClient.IsAccountFileExist()); + _characterModels = new List(); + _nextLinks = new Dictionary(); + //VRM1.0のダウンロード/バージョン処理に正しく対応したSDK組み込みのModelLoaderを使用する + ModelLoader.Initialize(config, _api, Application.productName); + } + + if (_model.IsAuthorized()) + { + GetAccountInfo((account) => + { + _model.CurrentUser = account; + _model.Active = false; + AfterAuthentication(true); + }, async (error) => + { + // Get this error code if you could not get access token. + // It will open browser to re-authorize. + if (error.code == "AUTHORIZED_ERROR") + { + _model.ClearUserInfo(); + _oauthClient.ReleaseAuthorizedAccount(); + } + else + { + _model.ApiError = error; + await server.SendCommandAsync(new PipeCommands.VRoidSDK_Error { Message = error.ToString() }); + } + }); + return; + } + + if (!_oauthClient.IsAccountFileExist()) + { + // open login modal. + _model.Active = true; + _model.AuthorizationState = ApiModel.State.AUTHORIZATION_CODE_REQUESTED; + await server.SendCommandAsync(new PipeCommands.VRoidSDK_NeedLogin { }); + } + } + + /// + /// 保存されている認証情報を破棄してログイン前の状態に戻す。 + /// 次回はブラウザでのアプリケーション連携からやり直しになる。 + /// + public async void Logout() + { + //まだ一度も認証処理を開始していない場合は何もしない + if (_oauthClient == null) return; + + _model?.ClearUserInfo(); + _oauthClient.ReleaseAuthorizedAccount(); //保存済みのアカウント情報(トークン)を削除する + + //別アカウントでログインし直した時に前のアカウントの一覧が残らないようにする + _characterModels?.Clear(); + _nextLinks?.Clear(); + + await server.SendCommandAsync(new PipeCommands.VRoidSDK_NeedLogin { }); + } + + public void DoLogin() + { + // このアプリケーションでは初めての認証である + // ブラウザを開き、VRoid Hubサイト上にてアプリケーション連携の許可を得てから認証処理する + // Open a browser and enter the code if it is not authorized. + _oauthClient.Login(_browser, (_) => + { + // Close login modal. + _model.AuthorizationState = ApiModel.State.AUTHORIZED; + GetAccountInfo((account) => + { + _model.CurrentUser = account; + AfterAuthentication(true); + }, (error) => + { + _model.ApiError = error; + AfterAuthentication(false); + }); + }, (e) => + { + _model.AuthorizationState = ApiModel.State.CONNECTION_FAILED; + AfterAuthentication(false); + }); + //コードを入力(Register)されたらAfterAuthenticfation + } + private void GetAccountInfo(Action onGetAccount, Action onFailed) + { + if (_model.CurrentUser != null) + { + onGetAccount(_model.CurrentUser); + return; + } + + _api.GetAccount(onGetAccount, onFailed); + } + + public void RegisterCode(string code) + { + _browser?.OnRegisterCode(code); + } + + + // 認証完了後の処理 + private async void AfterAuthentication(bool isSuccess) + { + //isSuccessがtrueの時はログイン完了、falseの時はRegisterCodeを呼ばないといけない + await server.SendCommandAsync(new PipeCommands.VRoidSDK_EndAuthenticate { IsSuccess = isSuccess }); + } + + public async void LoadModel(string id) + { + float lowprogress = 0.0f; + try + { + var characterObj = await ModelLoader.LoadVrmAsync( + characterModel: _characterModels.First(d => d.id == id), // CharacterModel#id を渡す + onProgress: async (float progress) => + { + progress = (int)(progress * 10) / 10f; + if (lowprogress != progress) + { + lowprogress = progress; + // VRMファイルがキャッシュされておらずダウンロードが必要な場合に、進捗状況が0.0〜1.0の間で通知される + await server.SendCommandAsync(new PipeCommands.VRoidSDK_ModelDownloadProgress { progress = progress }); + } + } + ); + + // UniVRMでデシリアライズされたVRMファイルのGameObjectが返される + await server.SendCommandAsync(new PipeCommands.VRoidSDK_ModelLoadComplete { }); + controlWPFWindow.LoadNewModel(characterObj); + } + catch (ModelLoadFailException error) + { + // 実行中にエラーが発生した場合、呼び出される + await server.SendCommandAsync(new PipeCommands.VRoidSDK_Error { Message = error.Message }); + } + } + + #region PipeConverters + + private List ConvertCharacterModelListToPipe(List characterModels) + { + var list = new List(); + foreach (var model in characterModels) + { + list.Add(ConvertCharacterModelToPipe(model)); + } + return list; + } + + private PipeCommands.WebImage ConvertWebImageToPipe(WebImage source) + { + return new PipeCommands.WebImage + { + height = source.height, + url = source.url, + url2x = source.url2x, + width = source.width, + }; + } + + private PipeCommands.PortraitImage ConvertPortraitImageToPipe(PortraitImage source) + { + return new PipeCommands.PortraitImage + { + original = ConvertWebImageToPipe(source.original), + sq150 = ConvertWebImageToPipe(source.sq150), + sq300 = ConvertWebImageToPipe(source.sq300), + sq600 = ConvertWebImageToPipe(source.sq600), + w300 = ConvertWebImageToPipe(source.w300), + w600 = ConvertWebImageToPipe(source.w600), + }; + } + + private PipeCommands.FullBodyImage ConvertFullBodyImageToPipe(FullBodyImage source) + { + return new PipeCommands.FullBodyImage + { + original = ConvertWebImageToPipe(source.original), + w300 = ConvertWebImageToPipe(source.w300), + w600 = ConvertWebImageToPipe(source.w600), + }; + } + + private PipeCommands.CharacterLicense ConvertCharacterLicenseToPipe(CharacterLicense source) + { + if (source == null) return default(PipeCommands.CharacterLicense); + return new PipeCommands.CharacterLicense + { + characterization_allowed_user = source.characterization_allowed_user, + corporate_commercial_use = source.corporate_commercial_use, + credit = source.credit, + modification = source.modification, + personal_commercial_use = source.personal_commercial_use, + redistribution = source.redistribution, + sexual_expression = source.sexual_expression, + violent_expression = source.violent_expression, + }; + } + + private PipeCommands.Tag ConvertTagToPipe(Tag source) + { + return new PipeCommands.Tag + { + en_name = source.en_name, + ja_name = source.ja_name, + locale = source.locale, + name = source.name, + }; + } + + private List ConvertTagListToPipe(List source) + { + var list = new List(); + foreach (var t in source) + { + list.Add(ConvertTagToPipe(t)); + } + return list; + } + + private PipeCommands.AgeLimit ConvertAgeLimitToPipe(AgeLimit source) + { + return new PipeCommands.AgeLimit + { + is_adult = source.is_adult, + is_r15 = source.is_r15, + is_r18 = source.is_r18, + }; + } + + private PipeCommands.UserIcon ConvertUserIconToPipe(UserIcon source) + { + return new PipeCommands.UserIcon + { + sq170 = ConvertWebImageToPipe(source.sq170), + sq50 = ConvertWebImageToPipe(source.sq50), + }; + } + + private PipeCommands.User ConvertUserToPipe(User source) + { + return new PipeCommands.User + { + icon = ConvertUserIconToPipe(source.icon), + id = source.id, + name = source.name, + pixiv_user_id = source.pixiv_user_id, + }; + } + + private PipeCommands.Character ConvertCharacterToPipe(Character source) + { + return new PipeCommands.Character + { + created_at = source.created_at, + id = source.id, + is_private = source.is_private, + name = source.name, + published_at = source.published_at, + user = ConvertUserToPipe(source.user), + }; + } + + private PipeCommands.CharacterVersion ConvertCharacterVersionToPipe(CharacterModelVersion source) + { + return new PipeCommands.CharacterVersion + { + created_at = source.created_at, + id = source.id, + }; + } + + private PipeCommands.CharacterModel ConvertCharacterModelToPipe(CharacterModel source) + { + var specVersion = source.getVRMVersion(); + return new PipeCommands.CharacterModel + { + age_limit = ConvertAgeLimitToPipe(source.age_limit), + character = ConvertCharacterToPipe(source.character), + created_at = source.created_at, + download_count = source.download_count, + full_body_image = ConvertFullBodyImageToPipe(source.full_body_image), + heart_count = source.heart_count, + id = source.id, + is_downloadable = source.is_downloadable, + is_hearted = source.is_hearted, + is_private = source.is_private, + latest_character_model_version = ConvertCharacterVersionToPipe(source.latest_character_model_version), + license = ConvertCharacterLicenseToPipe(source.license), + spec_version = specVersion, + license_vrm10 = ConvertCharacterLicenseVRM10ToPipe(source, specVersion), + name = source.name, + portrait_image = ConvertPortraitImageToPipe(source.portrait_image), + published_at = source.published_at, + tags = ConvertTagListToPipe(source.tags), + usage_count = source.usage_count, + view_count = source.view_count, + }; + } + + /// + /// VRM1.0モデルのライセンスを正規化(SDKのWhat*()→EnumLicenseの文字列)してPipe構造体へ変換する + /// + private PipeCommands.CharacterLicenseVRM10 ConvertCharacterLicenseVRM10ToPipe(CharacterModel source, string specVersion) + { + if (specVersion != "1.0") return default(PipeCommands.CharacterLicenseVRM10); + var vrmMeta = source.latest_character_model_version.vrm_meta; + if (vrmMeta == null) return default(PipeCommands.CharacterLicenseVRM10); + + var l = new CharacterLicenseVRM10(vrmMeta); + return new PipeCommands.CharacterLicenseVRM10 + { + avatar_user = l.WhatCanUseAvatarByOtherUser().ToString(), + violence = l.WhatCanUseViolence().ToString(), + sexuality = l.WhatCanUseSexuality().ToString(), + political_religious = l.WhatCanUseReligionOrPolitical().ToString(), + antisocial_hate = l.WhatCanUseAntisocialOrHatred().ToString(), + personal_commercial = l.WhatCanUseCommercial().ToString(), + corporate_commercial = l.WhatCanUseCorporate().ToString(), + redistribution = l.WhatRedistribution().ToString(), + modification = l.WhatModification().ToString(), + credit = l.WhatShowCredit().ToString(), + }; + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/Assets/Scripts/VRoidSDKConnector.cs.meta b/Assets/Scripts/VRoidSDKConnector.cs.meta new file mode 100644 index 00000000..7213500e --- /dev/null +++ b/Assets/Scripts/VRoidSDKConnector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2f7a0c5fd055af4083cbe934b512f19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests.meta b/Assets/Tests.meta new file mode 100644 index 00000000..3d553b8b --- /dev/null +++ b/Assets/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1fea23eaff1b8c24aa5912e4f2d64acb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/Editor.meta b/Assets/Tests/Editor.meta similarity index 77% rename from Assets/ExternalPlugins/DVRSDK/Editor.meta rename to Assets/Tests/Editor.meta index d2ad67d0..2da801a5 100644 --- a/Assets/ExternalPlugins/DVRSDK/Editor.meta +++ b/Assets/Tests/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5133160a0dd0ad844aef90e7f9abef87 +guid: 4c9675b54f675864fa3a012334e5be72 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Tests/Editor/VMCTestEditorMenu.cs b/Assets/Tests/Editor/VMCTestEditorMenu.cs new file mode 100644 index 00000000..45d04f0e --- /dev/null +++ b/Assets/Tests/Editor/VMCTestEditorMenu.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VMC.Tests; + +namespace VMC.Tests.EditorTools +{ + /// + /// Unity Editor から自動テストを起動するメニュー。 + /// 実行要求を一時ファイルに書いてから再生モードに入る + /// (ドメインリロードで静的変数が消えるためファイル経由で渡す)。 + /// + public static class VMCTestEditorMenu + { + private const string MenuRoot = "VMC/自動テスト/"; + + [MenuItem(MenuRoot + "全シナリオを実行", priority = 0)] + public static void RunAll() => Run(updateGolden: false); + + [MenuItem(MenuRoot + "全シナリオを実行(ゴールデンを更新)", priority = 1)] + public static void RunAllAndUpdateGolden() + { + if (EditorUtility.DisplayDialog("ゴールデンの更新", + "現在の実行結果で期待値(ゴールデン)を上書きします。\n差分の検出は行われません。よろしいですか?", + "更新する", "キャンセル") == false) + { + return; + } + Run(updateGolden: true); + } + + [MenuItem(MenuRoot + "VRM0.xのみ実行", priority = 20)] + public static void RunVrm0() => Run(updateGolden: false, models: new[] { VMCTestModels.Vrm0 }); + + [MenuItem(MenuRoot + "VRM1.0のみ実行", priority = 21)] + public static void RunVrm10() => Run(updateGolden: false, models: new[] { VMCTestModels.Vrm10 }); + + [MenuItem(MenuRoot + "設定ファイルを開く", priority = 40)] + public static void OpenConfig() + { + var config = VMCTestConfig.Load(); + var path = VMCTestConfig.ResolvePath(VMCTestConfig.DefaultConfigPath); + if (File.Exists(path) == false) config.Save(VMCTestConfig.DefaultConfigPath); + EditorUtility.RevealInFinder(path); + EditorUtility.OpenWithDefaultApp(path); + } + + [MenuItem(MenuRoot + "結果フォルダを開く", priority = 41)] + public static void OpenResults() + { + var directory = VMCTestConfig.Load().ResolvedOutputDirectory; + Directory.CreateDirectory(directory); + EditorUtility.RevealInFinder(directory + Path.DirectorySeparatorChar); + } + + [MenuItem(MenuRoot + "ゴールデンフォルダを開く", priority = 42)] + public static void OpenGolden() + { + var directory = VMCTestConfig.Load().ResolvedGoldenDirectory; + Directory.CreateDirectory(directory); + EditorUtility.RevealInFinder(directory + Path.DirectorySeparatorChar); + } + + private static void Run(bool updateGolden, string[] models = null, string[] scenarios = null) + { + if (EditorApplication.isPlaying) + { + Debug.LogError("[VMCTest] 再生モードを終了してから実行してください"); + return; + } + + var config = VMCTestConfig.Load(); + var missing = new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 } + .Where(d => models == null || models.Contains(d)) + .Where(d => config.GetModelPath(d) == null) + .ToList(); + + if (missing.Count > 0) + { + Debug.LogWarning($"[VMCTest] VRMが見つからないためスキップされます: {string.Join(", ", missing)}\n" + + $"{VMCTestConfig.ResolvePath(VMCTestConfig.DefaultConfigPath)} にパスを設定してください"); + } + + new VMCTestRequest + { + Scenarios = scenarios?.ToList() ?? new System.Collections.Generic.List(), + Models = models?.ToList() ?? new System.Collections.Generic.List(), + UpdateGolden = updateGolden, + QuitWhenFinished = false, + }.Save(); + + EditorApplication.EnterPlaymode(); + } + } +} diff --git a/Assets/Tests/Editor/VMCTestEditorMenu.cs.meta b/Assets/Tests/Editor/VMCTestEditorMenu.cs.meta new file mode 100644 index 00000000..72f1e8a9 --- /dev/null +++ b/Assets/Tests/Editor/VMCTestEditorMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc5d452e8c208164a937d4feedb118ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/README.md b/Assets/Tests/README.md new file mode 100644 index 00000000..7117aaee --- /dev/null +++ b/Assets/Tests/README.md @@ -0,0 +1,292 @@ +# VMC 自動テストハーネス + +実機のVR機器・コントロールパネル(WPF)・ネットワークを使わずに、 +**本番のシーンをそのまま動かして**アバターの挙動を検証するためのE2Eテストハーネス。 + +## 仕組み + +| 検証したいもの | 実機の代わりに使うもの | +| --- | --- | +| VR機器(HMD/コントローラ/トラッカー) | VMCProtocolのトラッカーメッセージを注入 | +| VMCProtocolの受信 | `uOscServer.onDataReceived` を直接叩く(UDP不使用) | +| VMCProtocolの送信 | `ExternalSender.SendHook` でバイト列を捕まえ、uOSCのParserで読み戻す | +| コントロールパネル(WPF) | `server.IsConnected = false` にして送信自体を止める | + +> **なぜパイプを止めるのか**: `MemoryMappedFileServer` は相手が居なくても `IsConnected = true` になる。 +> その状態で `SendCommand` を2回呼ぶと、1回目に立てた完了フラグを誰もクリアしないため +> `while (senderAccessor.ReadByte(0) == 1) Thread.Sleep(1);` で永久に待つ。 +> 結果、`await` 側(`ImportVRM` 等)が返らずテストが進まなくなり、 +> さらに再生停止時の `OnApplicationQuit` が**同期の** `SendCommand` を呼ぶためメインスレッドごと固まる。 +> ハーネスは最初にこれを無効化し、**再生セッション中は元に戻さない**(戻すと停止時に固まるため)。 + +UDPを介さないため**フレーム単位で決定論的**に再現できる。 +さらに `Time.captureDeltaTime` の固定、乱数シードの固定、まばたきの停止、 +受信トラッカーのローパスフィルタの無効化により、実行のたびに同じ結果が出るようにしている。 + +## 検証方法(ゴールデンスナップショット) + +各段階で次の情報を1つのJSONに固めて、前回の結果(ゴールデン)と比較する。 + +- ルートとHumanoid全ボーンのローカル姿勢 +- 表情の**最終的な重み**(`Vrm10RuntimeExpression.ActualWeights` = LookAtやOverride適用後) +- 視線の yaw / pitch +- その区間にVMCProtocolとして送信されたOSCメッセージ + +比較は許容誤差つき(位置は距離、回転は角度)。 +毎回変わる `/VMC/Ext/T` や絶対パスを含む `/VMC/Ext/VRM` `/VMC/Ext/Config` は比較対象外。 + +### ゴールデンだけでは足りない + +ゴールデン比較は「前回と同じか」しか見ないので、**壊れた状態が期待値として保存されると +以後ずっとPASSし続ける**。そこで各シナリオは、ゴールデンに依存しない不変条件も +`result.CheckThat(...)` で検査する。 + +- 注入したトラッカー姿勢が `TrackingPointManager` に届いているか +- トラッカーを動かしたときアバターのボーンが実際に回転するか +- 受信した表情/視線の値が `ActualWeights` / `LookAt.Yaw` に出ているか +- 送信された `/VMC/Ext/Bone/Pos` が実際のボーン姿勢と一致するか + +シナリオを追加するときも、この手の「意味の検査」を必ず1つ以上入れること。 + +## 準備 + +1. テスト用のVRMを用意する(ライセンスの都合でリポジトリには含めない) +2. 一度メニューを実行するか `VMC/自動テスト/設定ファイルを開く` で + `TestData/vmctest.json` の雛形を作り、パスを記入する + +```json +{ + "Vrm0Path": "TestData/Models/sample_vrm0.vrm", + "Vrm10Path": "TestData/Models/sample_vrm10.vrm", + "GoldenDirectory": "TestData/Golden", + "OutputDirectory": "TestData/Results", + "UpdateGolden": false, + "PositionTolerance": 0.001, + "RotationToleranceDegrees": 0.2, + "WeightTolerance": 0.002, + "Seed": 12345, + "FixedDeltaTime": 0.016666668 +} +``` + +パスはプロジェクト直下(`VirtualMotionCapture/`)からの相対パスか絶対パス。 +VRMが見つからないモデル種別は自動的にSKIPになる。 + +> **パス区切りに注意**: JSONなので `\` は `\\` にエスケープが必要 +> (`"C:\\Users\\me\\model.vrm"`)。`/` で書けばエスケープ不要 +> (`"C:/Users/me/model.vrm"`)。`\` を1つで書くとJSONのパースに失敗し、 +> 設定が既定値に戻って全シナリオがSKIPになる。 + +## 実行 + +### Unity Editor + +メニュー `VMC/自動テスト/` から実行する。再生モードに入り、 +コンソールに結果が出て `TestData/Results/report.txt` が書き出される。 + +- `全シナリオを実行` … ゴールデンと比較する +- `全シナリオを実行(ゴールデンを更新)` … 現在の結果でゴールデンを上書きする +- `VRM0.xのみ実行` / `VRM1.0のみ実行` + +**初回はゴールデンが無いので、その時の結果がそのままゴールデンとして保存される。** +保存された内容が妥当かどうかは一度目視で確認すること。 + +### ビルド済みexe / CI + +```bash +VirtualMotionCapture.exe -vmctest -vmctest-config TestData/vmctest.json +``` + +| 引数 | 意味 | +| --- | --- | +| `-vmctest` | テストを実行する(これが無いと通常起動) | +| `-vmctest-scenarios A,B` | 実行するシナリオ名(省略時は全部) | +| `-vmctest-models vrm0,vrm10` | 対象モデル(省略時は全部) | +| `-vmctest-updategolden` | 比較せずゴールデンを更新する | +| `-vmctest-config ` | 設定ファイルのパス | +| `-vmctest-noquit` | 終了後にアプリを終了しない | + +失敗があると終了コード1で終了する。 + +## コンパイルだけ確認したいとき + +Unity Editor を開いたままだと `-batchmode` は起動できない +(`HandleProjectAlreadyOpenInAnotherInstance` でクラッシュする)。 +Unityが生成する `Assembly-CSharp.csproj` を MSBuild でビルドすれば、 +Editorを閉じずにコンパイルエラーだけ確認できる。 + +```bash +"C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe" Assembly-CSharp.csproj /t:Build /v:minimal +``` + +## ファイル構成 + +| ファイル | 役割 | +| --- | --- | +| `VMCTestConfig.cs` | 設定(VRMのパス、許容誤差、ゴールデンの場所) | +| `VMCTestSnapshot.cs` | スナップショットの形式・保存・比較 | +| `VMCTestOsc.cs` | VMCProtocolメッセージの組み立て・注入・送信キャプチャ | +| `VMCTestContext.cs` | アプリ操作ヘルパー(モデル読込/受信機作成/キャリブレーション/フレーム進行) | +| `VMCTestScenario.cs` | シナリオ基底とレポート生成 | +| `VMCTestRunner.cs` | ランナー(コマンドライン引数 / Editorメニューの要求で起動) | +| `Scenarios/` | 各シナリオ(下記) | +| `Editor/VMCTestEditorMenu.cs` | Editorメニュー | + +## シナリオ一覧 + +すべて VRM0.x と VRM1.0 の両方で実行される。 + +| シナリオ | 内容 | モデル | +| --- | --- | --- | +| `BasicVMCProtocol` | VRM読込 → トラッカー受信 → キャリブレーション → 追従 → 表情/LookAt受信 → 送信 | 両方 | +| `VMCProtocolBoneRoundTrip` | 既知のボーン姿勢を受信し、同じ値が送信されて出るか(VMCの数珠つなぎ) | 両方 | +| `VMCProtocolSendCoverage` | 送信されうる全アドレスが実際に出ているか | 両方 | +| `VMCProtocolControlMessages` | カメラ画角/ライト/送信周期/スルー/入力/リモートキャリブの受信 | 両方 | +| `MotionVrmaRoundTrip` | 記録 → VRMA書き出し → 読み込みでボーン・表情・視線が復元されるか | 両方 | +| `BvhExport` | 記録 → BVH書き出し → 読み込みで見た目の姿勢が保たれるか | VRM0.x | +| `SettingsSaveLoad` | 設定の保存 → 再読み込みで全項目が往復し、アバターとキャリブレーションが復元されるか | 両方 | +| `SettingsMigration` | 旧バージョン(v0.55)の設定ファイルからの移行と二重移行の防止 | VRM0.x | +| `PipeCommandsSerialization` | コントロールパネルとの通信コマンド全型のシリアライズ往復 | 不要 | +| `ModelSwitch` | 別アバター(VRM0.x⇔VRM1.0)に差し替えたときの自動再キャリブレーションと追従・表情・視線の引き継ぎ | 両方 | +| `MultipleReceivers` | 受信機を2つ使ったときの担当範囲の分離と独立性 | VRM0.x | +| `FaceMixing` | ベース/加算/上書きの合成順序、クランプ、VRM0.x名での指定 | 両方 | +| `BlinkFrameDrop` | 処理落ちでまばたきを飛び越しても目が開いた状態に戻るか | 両方 | +| `KeyActions` | ショートカットキーによる表情・機能の実行と同時押しの優先 | VRM0.x | +| `FaceHardwareInputs` | リップシンク(viseme)・リップトラッキング・アイトラッキングの反映 | 両方 | +| `MocopiReceive` | mocopiのスケルトン受信とアバターへの適用 | VRM0.x | +| `VMTSend` | Virtual Motion Trackerへの送信内容 | VRM0.x | +| `DeviceInfoTracking` | トラッキングの飛び検出・復帰補間・一時停止 | 不要 | +| `Robustness` | 壊れたOSC・壊れたVRM・存在しない設定ファイルで落ちないか | VRM0.x | +| `RenderingAndStability` | 写真撮影・スプリングボーン・モデル入れ替えのリーク・処理時間 | VRM0.x | + +`ModelSwitch` は `ModelKey` が読み込み元、もう一方が切り替え先になるので、 +VRM0.x→VRM1.0 と VRM1.0→VRM0.x の両方向が検証される。 + +## VMCProtocol の網羅状況 + +`ExternalSender` / `ExternalReceiverForVMC` のソースから抽出した全アドレス。 + +### 送信 (ExternalSender) + +| アドレス | テスト | +| --- | --- | +| `/VMC/Ext/OK` `/VMC/Ext/T` `/VMC/Ext/Root/Pos` `/VMC/Ext/Bone/Pos` | SendCoverage(有無) + BasicVMCProtocol / BoneRoundTrip(値) | +| `/VMC/Ext/Blend/Val` `/VMC/Ext/Blend/Apply` | SendCoverage + BasicVMCProtocol(値) | +| `/VMC/Ext/Cam` | SendCoverage + ControlMessages(画角の値・座標系・往復) | +| `/VMC/Ext/Hmd/Pos` `/Local` `/VMC/Ext/Con/Pos` `/Local` `/VMC/Ext/Tra/Pos` `/Local` | SendCoverage + BasicVMCProtocol(値) | +| `/VMC/Ext/Rcv` `/VMC/Ext/Light` `/VMC/Ext/Setting/Color` `/VMC/Ext/Setting/Win` `/VMC/Ext/Config` `/VMC/Ext/Opt` `/VMC/Ext/VRM` | SendCoverage(有無) | +| `/VMC/Ext/Con` `/VMC/Ext/Key` `/VMC/Ext/Midi/Note` `/VMC/Ext/Midi/CC/Val` `/VMC/Ext/Midi/CC/Bit` | SendCoverage(有無) | +| `/VMC/Thru/*` の転送 | ControlMessages | +| `/VMC/Ext/Remote` | **未カバー**(VRoid Hub から読み込んだ時だけ送られるため、SDKとログインが要る) | + +### 受信 (ExternalReceiverForVMC) + +| アドレス | テスト | +| --- | --- | +| `/VMC/Ext/Hmd/Pos` `/VMC/Ext/Con/Pos` `/VMC/Ext/Tra/Pos` | BasicVMCProtocol | +| `/VMC/Ext/Root/Pos` `/VMC/Ext/Bone/Pos` | VMCProtocolBoneRoundTrip | +| `/VMC/Ext/Blend/Val` `/VMC/Ext/Blend/Apply` `/VMC/Ext/Set/Eye` | BasicVMCProtocol | +| `/VMC/Ext/Cam` `/VMC/Ext/Light` `/VMC/Ext/Set/Period` `/VMC/Ext/Set/Req` `/VMC/Ext/Set/Res` | ControlMessages | +| `/VMC/Ext/Con` `/VMC/Ext/Key` `/VMC/Ext/Midi/CC/Val` `/VMC/Thru/*` | ControlMessages | +| `/VMC/Ext/Set/Calib/Ready` `/VMC/Ext/Set/Calib/Exec` | ControlMessages | +| `/VMC/Ext/Set/Config` | **間接的**(SettingsSaveLoad が同じ `LoadSettings` を直接呼んでいる) | +| `/VMC/Ext/OK` | **未カバー**(受信側の内部状態=キャリブ完了検出にしか使われず、外から観測しづらい) | + +### カメラについて分かっていること + +- 受信した画角は `ControlCamera.fieldOfView` にだけ入り、`Settings.Current.CameraFOV` や + 各カメラリグの `currentFOV` は更新されない。送信側が毎フレーム送るので実害は無いが、 + **受信側のコントロールパネルに表示される画角は自分の値のまま**になる +- `Camera.fieldOfView` は**垂直**画角。送受信のウインドウ解像度(アスペクト比)が違うと + 同じ垂直画角でも水平方向の写る範囲が変わる。これはプロトコルの仕様上の制約で、コードの不具合ではない +- `/VMC/Ext/Cam` の座標は `IKManager.HandTrackerRoot` から見た**ローカル座標**。 + この親はキャリブレーションで身長比のスケールとオフセットを持つため、ローカルで受け取ることで + 送られてきた座標が**受信側アバターのスケールへ写像される**(2021-03 の `58efc0a` で受信側をこの形にした)。 + 送信側も同じ座標系で送らないと、VMC同士でスケールが二重に掛かってカメラ距離がずれる。 + ControlMessages シナリオは `HandTrackerRoot` にあえて非単位のスケール・オフセットを入れて + この食い違いを検出する(ワールドとローカルが同値だと検出できないので、前提自体もチェックしている) + +## シナリオの追加方法 + +`VMCTestScenario` を継承して `Scenarios/` に置き、 +`VMCTestRunner.AllScenarios` に追加する。 + +```csharp +public sealed class Scenario_Something : VMCTestScenario +{ + public override string Name => "Something"; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + // ... + result.CheckSnapshot(context, context.Capture("01_something")); + } +} +``` + +シナリオ内で例外を投げるとその実行は失敗として記録される +(入れ子のコルーチンの例外も `VMCTestRunner.Drive` が拾う)。 + +## 本番コードに入れたテスト用の穴 + +最小限だけ。いずれも通常動作には影響しない。 + +- `ExternalSender.SendHook` … 送信内容のキャプチャ用の static event。通常は誰も購読していない +- `ControlWPFWindow.Test_*` … `internal` のアクセサ(CurrentModel / 受信機の追加 / 設定の保存 / MotionPlayer / MotionRecorder) +- `MotionRecorder.Test_*` … `internal` のアクセサ(記録状態 / フレーム数 / 書き出し) +- `MotionPlayer.ApplyPoseByPathAsync` … 既存の `ApplyPoseByPath`(async void)を待機可能にした版。 + `ApplyPoseByPath` はこれを呼ぶだけになっており、動作は変わらない +- `CameraManager.Test_SetCameraFOV` … `internal` のアクセサ +- `DynamicOVRLipSync.ApplyVisemes` / `Test_ApplyVisemes` … visemeの加工処理を `Update` から切り出したもの。 + マイクが無くても同じ経路を通せる(`Update` はこれを呼ぶだけになっており、動作は変わらない) +- `LipTracking_Vive.Test_ApplyLipWeights` … シェイプ名と重みを直接与える `internal` メソッド +- `EyeTracking_ViveProEye.Test_ApplyEyeState` … まぶたの開き具合と視線方向を直接与える `internal` メソッド +- `MocopiConnector.UpdateSkeletonForTest` … UDPの代わりにフレームデータを流し込む `internal` メソッド +- `VMTClient.SendHook` … 送信内容のキャプチャ用の static event。通常は誰も購読していない +- `AnimationController.TestTimeProvider` … 時計の差し替え用の static デリゲート。 + 通常は null で `Time.realtimeSinceStartup` が使われる。まばたきは全体で0.19秒しかなく、 + 処理落ちを実時間で再現できないため、`BlinkFrameDrop` はこれで疑似時計を与えて + 「1フレームで0.333秒進んだ」状況を決定論的に作る + +### ハードウェアが無くても検査できる範囲 + +境界(SDKから値が返る地点)に注入しているので、実機が要るのは +**「デバイスが繋がるか」「ドライバが応答するか」だけ**になっている。 + +| 対象 | 注入する地点 | 実機でしか確認できないこと | +| --- | --- | --- | +| VR機器 | VMCProtocolのトラッカーメッセージ | SteamVRとの接続 | +| mocopi | `MocopiConnector.InitializeSkeleton` / `UpdateSkeletonForTest` | センサーとの接続 | +| マイク(リップシンク) | `DynamicOVRLipSync.Test_ApplyVisemes` | マイク入力とOVRLipSyncの解析 | +| Viveリップトラッキング | `LipTracking_Vive.Test_ApplyLipWeights` | SRanipalとの接続 | +| Vive Pro Eye | `EyeTracking_ViveProEye.Test_ApplyEyeState` | SRanipalとの接続 | +| MIDI | `MidiCCWrapper` のデリゲート | MIDIデバイスとの接続 | +| VMTドライバ | `VMTClient.SendHook` | ドライバ側の受信 | +| コントロールパネル | `PipeCommands` のシリアライズ往復 | WPF側のUI動作 | + +なお、ハーネスはテスト中に `Settings/common.json`(起動時に読み込む設定ファイルのパス)を +退避して終了時に戻す。`SaveSettings` / `LoadSettings` がここを書き換えるため、 +テストで作った設定ファイルが次回のVMC起動時に読まれてしまうのを防いでいる。 + +## 既知の制限 + +- `Time.realtimeSinceStartup` に依存する箇所(受信遅延バッファ、`enableLocalHandFix` の5秒判定)は + 実時間に依存する。前者は `DelayMs = 0` で回避しているが、後者は長いシナリオでは影響しうる +- `Application.targetFrameRate` は**必ず正の値**にすること。`DeviceInfo.updateOkTime()` が + `okTime = validFrames / Application.targetFrameRate` を計算しているため、`-1`(制限なし)にすると + `okTime` が負になり、トラッキング復帰の補間係数が常に0になって + **トラッカーの姿勢が最初の値で永久に固定される**(見た目上は何のエラーも出ない) +- トラッカーは認識から1秒(`DeviceInfo.LEAP_SECONDS`)の間、飛び対策で過去値から補間される。 + 注入した姿勢をそのまま使いたい場合は `context.WaitTrackingWarmup()` で待つこと +- スプリングボーンや実際の描画結果は検証していない(ボーン・表情・視線・送信データのみ) +- **Humanoidのリターゲットは腕のツイストを上腕と手の間で配分し直す**。 + VRMのアバターとVRMAから作ったアバターで twist 設定が違うため、 + 末端(頭・手・足)の向きが完全に一致していてもボーン単位では数度ずれる(実測で最大5度)。 + そのため `MotionVrmaRoundTrip` は「末端の向き」(厳しく1度)と「ボーン単位」(緩く8度)を分けて検査する。 + ボーン単位だけを見ると、見た目が同じでも落ちてしまう +- 同様に、Unity Humanoidのマッスル空間は可動範囲が限られており、 + 腕を下ろした姿勢の肩・上腕と、指(特に親指)は元の回転をそのまま表現できない。 + 記録→再生の総合誤差は `MotionRetargetToleranceDegrees` / `MotionFingerToleranceDegrees` で別枠にしている +- BVH書き出しは未検証(VRMAのみ)。スプリングボーンと実際の描画結果も対象外 diff --git a/Assets/Tests/README.md.meta b/Assets/Tests/README.md.meta new file mode 100644 index 00000000..aa29d8d8 --- /dev/null +++ b/Assets/Tests/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: aa568b891316c7f47bea5d3c4a430a98 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios.meta b/Assets/Tests/Scenarios.meta new file mode 100644 index 00000000..ccd0ef2e --- /dev/null +++ b/Assets/Tests/Scenarios.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7e6b3314a08e0e04983abafa8d82c2b4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs b/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs new file mode 100644 index 00000000..edb995c8 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs @@ -0,0 +1,173 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// 縦串の基本シナリオ。 + /// + /// VRM読み込み + /// → VMCProtocolでトラッカー姿勢を受信(実機VRの代替) + /// → キャリブレーション + /// → トラッカーを動かしてアバターが追従することを確認 + /// → VMCProtocolで表情とLookAtを受信 + /// → その状態がVMCProtocolとして送信されることを確認 + /// + /// 各段階でスナップショットを取ってゴールデンと比較するのに加えて、 + /// 「そもそも動いているか」をゴールデンに依存しない不変条件として検査する。 + /// (壊れた状態のままゴールデンが作られると回帰テストが無意味になるため) + /// + public sealed class Scenario_BasicVMCProtocol : VMCTestScenario + { + //受信した表情の期待値 + private const float ExpectedJoy = 0.7f; + private const float ExpectedA = 0.3f; + + public override string Name => "BasicVMCProtocol"; + + public override string Description + => "VRM読込→トラッカー受信→キャリブレーション→追従→表情/LookAt受信→送信"; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + var vrmPath = context.Config.GetModelPath(context.ModelKey); + + //--- 1. モデル読み込み --- + context.Log($"1. VRM読み込み: {vrmPath}"); + context.ResetSettings(); + yield return context.LoadModel(vrmPath); + + //--- 2. VMCProtocol受信機を用意してトラッカーを流し込む --- + context.Log("2. VMCProtocol受信機の作成とトラッカー注入"); + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyBlendShape = true; + setting.ApplyLookAt = true; + //ボーンは受信しない(このシナリオはトラッカー駆動のVRIKを見る) + setting.ApplyRootPosition = false; + setting.ApplyRootRotation = false; + setting.ApplySpine = false; + setting.ApplyChest = false; + setting.ApplyHead = false; + setting.ApplyLeftArm = false; + setting.ApplyRightArm = false; + setting.ApplyLeftHand = false; + setting.ApplyRightHand = false; + setting.ApplyLeftLeg = false; + setting.ApplyRightLeg = false; + setting.ApplyLeftFoot = false; + setting.ApplyRightFoot = false; + setting.ApplyLeftFinger = false; + setting.ApplyRightFinger = false; + setting.ApplyEye = false; + }); + + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + + var rigError = context.GetTrackerRigError(VMCTestTrackerRig.IPose); + result.CheckThat("トラッカーの受信", + rigError >= 0f && rigError < 0.005f, + rigError < 0f + ? "注入したトラッカーがTrackingPointManagerに登録されていません" + : $"注入した姿勢が反映されていません(最大位置誤差 {rigError:F4}m)"); + + //--- 3. キャリブレーション --- + context.Log("3. キャリブレーション(Iポーズ)"); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + //キャリブレーション中はトラッカー入力が止まるので、もう一度姿勢を送って落ち着かせる + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + var iposeSnapshot = context.Capture("01_calibrated_ipose", includeSent: false); + result.CheckSnapshot(context, iposeSnapshot); + + //--- 4. トラッカーを動かしてアバターが追従することを確認 --- + context.Log("4. トラッカーを動かして追従を確認"); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + + var tposeError = context.GetTrackerRigError(VMCTestTrackerRig.TPose); + result.CheckThat("トラッカーの移動", + tposeError >= 0f && tposeError < 0.005f, + $"トラッカーを動かしたのに姿勢が更新されていません(最大位置誤差 {tposeError:F4}m)"); + + var tposeSnapshot = context.Capture("02_tracking_tpose", includeSent: false); + result.CheckSnapshot(context, tposeSnapshot); + + //腕を横に上げたので、腕のボーンが大きく回転しているはず。 + //ここが0度のままなら、キャリブレーションかVRIKかトラッカー入力のどこかが死んでいる。 + var maxDelta = VMCTestSnapshot.MaxBoneRotationDelta(iposeSnapshot, tposeSnapshot, out var movedBone); + result.CheckThat("アバターの追従", + maxDelta > 15f, + $"トラッカーを動かしてもアバターが動いていません(最大回転差 {maxDelta:F2}度 / 最大は {movedBone ?? "なし"})"); + + //--- 5. 表情とLookAtを受信 --- + context.Log("5. 表情とLookAtの受信"); + //VMCProtocolの仕様上、表情はVRM1.0モデルでもVRM0.xの名称で送られてくる + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(new[] + { + new KeyValuePair("Joy", ExpectedJoy), + new KeyValuePair("A", ExpectedA), + })); + //頭ボーンから見て右斜め前を見る + context.Inject(receiver, VMCTestOscBuilder.Eye(true, new Vector3(0.3f, 0.05f, 1.0f))); + yield return context.Step(5); + + var faceSnapshot = context.Capture("03_expression_lookat", includeSent: false); + result.CheckSnapshot(context, faceSnapshot); + + var joy = faceSnapshot.GetExpression("Joy"); + var aa = faceSnapshot.GetExpression("A"); + result.CheckThat("表情の受信", + Mathf.Abs(joy - ExpectedJoy) < 0.01f && Mathf.Abs(aa - ExpectedA) < 0.01f, + $"受信した表情が反映されていません(Joy {joy:F3} 期待{ExpectedJoy} / A {aa:F3} 期待{ExpectedA})"); + + result.CheckThat("LookAtの受信", + faceSnapshot.HasLookAt && Mathf.Abs(faceSnapshot.LookAtYaw) > 5f, + $"受信した視線が反映されていません(has={faceSnapshot.HasLookAt} yaw={faceSnapshot.LookAtYaw:F2} pitch={faceSnapshot.LookAtPitch:F2})"); + + //--- 6. この状態がVMCProtocolとして送信されることを確認 --- + context.Log("6. VMCProtocol送信のキャプチャ"); + context.EnableSender(); + yield return context.Step(3); + //送信開始直後のフレームを避けてからキャプチャする + context.ClearSent(); + yield return context.Step(4); + + var sentSnapshot = context.Capture("04_sent", includeSent: true); + result.CheckSnapshot(context, sentSnapshot); + + //受信して合成した結果が、そのままVMCProtocolとして出ていること + var sentBoneDifferences = sentSnapshot.VerifySentBonesMatchState( + context.Config.PositionTolerance, context.Config.RotationToleranceDegrees); + result.CheckThat("送信ボーンと状態の一致", + sentBoneDifferences.Count == 0, + $"送信されたボーン姿勢が実際のアバターと食い違っています: {string.Join(" / ", sentBoneDifferences.GetRange(0, Mathf.Min(5, sentBoneDifferences.Count)))}"); + + var sentJoy = FindSentBlendShape(sentSnapshot, "Joy"); + result.CheckThat("表情の送信", + sentJoy.HasValue && Mathf.Abs(sentJoy.Value - ExpectedJoy) < 0.01f, + sentJoy.HasValue + ? $"送信された Joy が {sentJoy.Value:F3} で期待値 {ExpectedJoy} と違います" + : "/VMC/Ext/Blend/Val に Joy が含まれていません(VRM1.0でもVRM0.x名で送る必要がある)"); + + context.DisableSender(); + } + + private static float? FindSentBlendShape(VMCTestSnapshot snapshot, string name) + { + foreach (var message in snapshot.Sent) + { + if (message.Address != "/VMC/Ext/Blend/Val") continue; + if (message.Args.Count != 2 || message.Args[0].T != "s") continue; + if (message.Args[0].S != name) continue; + return message.Args[1].F; + } + return null; + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs.meta b/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs.meta new file mode 100644 index 00000000..3f292bdc --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BasicVMCProtocol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f117cb6663ec4184b9e432c5629199aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs b/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs new file mode 100644 index 00000000..f0016a7c --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs @@ -0,0 +1,170 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// 処理落ち時の自動まばたき。 + /// + /// まばたきは 閉じる(0.06秒) → 閉じたまま維持(0.1秒) → 開く(0.03秒) の合計0.19秒しかなく、 + /// Maximum Allowed Timestep が0.33333秒なので、重い1フレームで丸ごと飛び越せる。 + /// 飛び越したときに中間状態(目を閉じたまま)で止まると、 + /// 次のまばたきまで(最大10秒)目が閉じっぱなしになる。 + /// + /// AnimationController.TestTimeProvider で時計を差し替え、処理落ちを決定論的に再現する。 + /// + public sealed class Scenario_BlinkFrameDrop : VMCTestScenario + { + public override string Name => "BlinkFrameDrop"; + + public override string Description => "処理落ちしても自動まばたきが開いた状態に戻ること"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 }; + + private const float WaitTime = 0.5f; //まばたきの間隔(最短=最長にして乱数を排除する) + private const float FrameTime = 1f / 60f; + private const float DropTime = 0.33333334f; //Maximum Allowed Timestep(1フレームで進みうる最大時間) + + private float clock; //疑似時計 + private float cycleStart; //今のまばたきサイクルが始まった時刻 + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var face = context.FaceController; + var savedBlinkTimeMin = face.BlinkTimeMin; + var savedBlinkTimeMax = face.BlinkTimeMax; + face.EnableBlink = false; + face.StopBlink = false; + face.BlinkTimeMin = WaitTime; + face.BlinkTimeMax = WaitTime; + yield return context.Step(5); + + var closeTime = face.CloseAnimationTime; + var openStart = WaitTime + closeTime + face.ClosingTime; + var openTime = face.OpenAnimationTime; + var blinkEnd = openStart + openTime; + + clock = 1000f; + AnimationController.TestTimeProvider = () => clock; + try + { + //--- 2. 処理落ちしていない時のまばたき --- + context.Log("2. 通常のまばたき"); + yield return StartCycle(context, face); + + var maxBlink = 0f; + var frames = Mathf.CeilToInt((blinkEnd + 0.05f) / FrameTime); + for (int i = 0; i < frames; i++) + { + yield return AdvanceBy(context, FrameTime); + maxBlink = Mathf.Max(maxBlink, ReadBlink(context)); + } + var afterBlink = ReadBlink(context); + + result.CheckThat("通常のまばたき", + maxBlink > 0.9f && afterBlink < 0.01f, + $"目を閉じて開くまでの一連の動作になっていません" + + $"(最大 {maxBlink:F3} 期待 1.000 / 終了後 {afterBlink:F3} 期待 0.000)"); + + //--- 3. 処理落ちで開くアニメーションの途中に着地した時 --- + context.Log("3. 処理落ちで開くアニメーションの途中に着地"); + yield return StartCycle(context, face); + yield return JumpTo(context, WaitTime - FrameTime); + + //1フレームで「閉じる」「維持」を飛び越して、「開く」のちょうど中間へ着地させる + yield return JumpTo(context, openStart + openTime * 0.5f); + var midOpenBlink = ReadBlink(context); + + result.CheckThat("処理落ち後の目の開き具合", + Mathf.Abs(midOpenBlink - 0.5f) < 0.1f, + $"飛び越した後、その時刻に対応した開き具合になっていません" + + $"(Blink={midOpenBlink:F3} 期待 0.500)。" + + $"アニメーションの先頭の値に戻していると1.000(目を閉じたまま)になります"); + + //--- 4. 処理落ちでまばたき全体を飛び越した時 --- + context.Log("4. 処理落ちでまばたき全体を飛び越す"); + yield return StartCycle(context, face); + yield return JumpTo(context, WaitTime - FrameTime); + + //目を閉じている途中まで進めてから、そこで処理落ちさせる + yield return JumpTo(context, WaitTime + closeTime * 0.5f); + var duringClose = ReadBlink(context); + + yield return AdvanceBy(context, DropTime); + var afterDrop = ReadBlink(context); + + result.CheckThat("処理落ち直後の復帰", + duringClose > 0.3f && afterDrop < 0.01f, + $"まばたきの途中で{DropTime:F3}秒の処理落ちが起きた後、目が開いた状態に戻っていません" + + $"(処理落ち前 {duringClose:F3} / 直後 {afterDrop:F3} 期待 0.000)"); + + //報告された症状そのもの。処理落ち後、次のまばたきが来るまで目が閉じたままになっていないか + var maxDuringWait = 0f; + var waitFrames = Mathf.CeilToInt(WaitTime * 0.8f / FrameTime); + for (int i = 0; i < waitFrames; i++) + { + yield return AdvanceBy(context, FrameTime); + maxDuringWait = Mathf.Max(maxDuringWait, ReadBlink(context)); + } + + result.CheckThat("処理落ち後の待機中に目が閉じたままにならない", + maxDuringWait < 0.01f, + $"処理落ちの後、次のまばたきまでの待機中に目が閉じたままになっています" + + $"(最大 {maxDuringWait:F3} 期待 0.000)"); + } + finally + { + AnimationController.TestTimeProvider = null; + face.EnableBlink = false; + face.BlinkTimeMin = savedBlinkTimeMin; + face.BlinkTimeMax = savedBlinkTimeMax; + } + } + + /// まばたきを止めてリセットし、新しいサイクルを開始する + private IEnumerator StartCycle(VMCTestContext context, FaceController face) + { + face.EnableBlink = false; + clock += FrameTime; + yield return Apply(context); + + face.EnableBlink = true; + clock += FrameTime; + cycleStart = clock; //このフレームのNext()でシーケンスの開始時刻になる + yield return Apply(context); + } + + /// サイクル開始からの経過時間がtargetElapsedになるところまで、1フレームで一気に進める + private IEnumerator JumpTo(VMCTestContext context, float targetElapsed) + { + clock = cycleStart + targetElapsed; + yield return Apply(context); + } + + private IEnumerator AdvanceBy(VMCTestContext context, float seconds) + { + clock += seconds; + yield return Apply(context); + } + + /// + /// 今の時刻をアバターに反映し、読み取れる状態にする。 + /// 表情は FaceController が Update で積み上げ、VRMのランタイムがその後で反映するため、 + /// 1フレームだけでは前フレームの値しか読めない。 + /// 時計を進めずにもう1フレーム回す(同じ時刻なので Next() の結果は変わらない)。 + /// + private static IEnumerator Apply(VMCTestContext context) + { + yield return context.Step(1); + yield return context.Step(1); + } + + private static float ReadBlink(VMCTestContext context) + => context.Capture("tmp", includeSent: false).GetExpression("Blink"); + } +} diff --git a/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs.meta b/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs.meta new file mode 100644 index 00000000..96b5c97c --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BlinkFrameDrop.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 365a35f4ea4a9754c9e304eabd674181 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_BvhExport.cs b/Assets/Tests/Scenarios/Scenario_BvhExport.cs new file mode 100644 index 00000000..b5728535 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BvhExport.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// BVH書き出しの往復。 + /// + /// BVHはUniVRMに頼らず BvhWriter で完全に自前実装しており、 + /// チャンネル順(Yrotation Xrotation Zrotation) / X反転 / cm単位 / + /// 「レストからのワールド差分回転」という独自の約束事が多い。 + /// 書き出したBVHを読み直して、見た目の姿勢が保たれるかを確認する。 + /// + public sealed class Scenario_BvhExport : VMCTestScenario + { + public override string Name => "BvhExport"; + + public override string Description => "BVH書き出しと読み込みの往復"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込みとキャリブレーション"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var receiver = context.CreateReceiver(setting => setting.ApplyTracker = true); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + //Tポーズにして、特徴のある姿勢を記録する + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + + //--- 2. 記録 --- + context.Log("2. モーションの記録"); + Settings.Current.MotionRecord_Fps = 30; + Settings.Current.MotionRecord_CountdownSeconds = 0; + Settings.Current.MotionRecord_SaveMotion = true; + + var recorder = context.MotionRecorder; + recorder.StartRecording(); + yield return context.Step(40); + var recorded = context.Capture("01_recorded", includeSent: false); + var lastFrame = recorder.Test_RecordedFrameCount - 1; + recorder.StopRecording(); + yield return context.Step(2); + + result.CheckThat("モーションの記録", + recorder.Test_State == MotionRecorder.RecordState.Recorded && lastFrame > 0, + $"記録できていません(state={recorder.Test_State} frames={recorder.Test_RecordedFrameCount})"); + if (recorder.Test_State != MotionRecorder.RecordState.Recorded) yield break; + + //--- 3. BVHで書き出す --- + context.Log("3. BVHの書き出し"); + var bvhPath = context.OutputPath($"{Name}.{context.ModelKey}.bvh"); + recorder.Test_SaveRecording(bvhPath, 1, 0, lastFrame); //format 1 = BVH + yield return context.Step(2); + + var fileInfo = new FileInfo(bvhPath); + result.CheckThat("BVHの書き出し", + fileInfo.Exists && fileInfo.Length > 1024, + $"BVHが書き出されていません({bvhPath})"); + if (fileInfo.Exists == false) yield break; + + //中身の体裁も見ておく(壊れたBVHは読めても無音で崩れる) + var text = File.ReadAllText(bvhPath); + result.CheckThat("BVHの体裁", + text.StartsWith("HIERARCHY") && text.Contains("MOTION") + && text.Contains("Frames:") && text.Contains("Frame Time:") + && text.Contains("Yrotation Xrotation Zrotation"), + "BVHの必須セクション(HIERARCHY/MOTION/Frames/Frame Time/回転チャンネル順)が揃っていません"); + + //--- 4. 記録データのプレビューを基準にする --- + context.Log("4. 記録データのプレビュー取得"); + context.SetReceiverActive(receiver, false); + yield return context.Step(2); + recorder.PreviewSeek(lastFrame); + yield return context.Step(5); + var preview = context.Capture("02_preview", includeSent: false); + recorder.PreviewStop(); + yield return context.Step(2); + + //--- 5. 書き出したBVHを再生する --- + context.Log("5. BVHの読み込みと再生"); + var player = context.MotionPlayer; + yield return context.Await(player.ApplyPoseByPathAsync(bvhPath, lastFrame)); + yield return context.Step(5); + var replayed = context.Capture("03_replayed", includeSent: false); + result.CheckSnapshot(context, replayed); + + //--- 6. 見た目の姿勢が保たれているか --- + context.Log("6. 往復の一致確認"); + var endEffectorDifferences = VMCTestSnapshot.CompareEndEffectors(preview, replayed, + context.Config.MotionRetargetToleranceDegrees, out var maxEnd, out var worstEnd); + result.CheckThat("BVH往復の見た目", + endEffectorDifferences.Count == 0, + $"BVHの往復で末端の向きが変わっています(最大 {maxEnd:F2}度 @ {worstEnd}): " + + string.Join(", ", endEffectorDifferences)); + + //記録時の実際の姿勢とも比べる(こちらはマッスル空間のリターゲット誤差が乗る) + var recordedDifferences = VMCTestSnapshot.CompareEndEffectors(recorded, replayed, + context.Config.MotionRetargetToleranceDegrees, out var maxRecorded, out var worstRecorded); + result.CheckThat("BVH往復と実際の姿勢", + recordedDifferences.Count == 0, + $"記録時の姿勢とBVH再生後で末端の向きが違います(最大 {maxRecorded:F2}度 @ {worstRecorded})"); + + Debug.Log($"[VMCTest] BVH往復誤差: プレビュー比 最大{maxEnd:F2}度 @ {worstEnd} / 実姿勢比 最大{maxRecorded:F2}度 @ {worstRecorded}"); + + player.Stop(); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_BvhExport.cs.meta b/Assets/Tests/Scenarios/Scenario_BvhExport.cs.meta new file mode 100644 index 00000000..58bb026a --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_BvhExport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54979cd8e5bd3d340be34ab8e48e5c81 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs b/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs new file mode 100644 index 00000000..bb409ec4 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs @@ -0,0 +1,124 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// DeviceInfo(トラッキングの飛び検出・復帰補間)の検証。 + /// + /// 実機のトラッカーが無くても、姿勢を直接与えるだけで全経路を通せる。 + /// ここは「エラーも警告も出ないまま姿勢が固定される」壊れ方をするため、 + /// 手動では極めて気付きにくい(実際に Application.targetFrameRate = -1 で踏んだ)。 + /// + public sealed class Scenario_DeviceInfoTracking : VMCTestScenario + { + public override string Name => "DeviceInfoTracking"; + + public override string Description => "トラッキングの飛び検出・復帰補間・一時停止"; + + public override bool RequiresModel => false; + + public override IReadOnlyList Models => new[] { VMCTestModels.None }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.ResetSettings(); + + var frameRate = context.FrameRate; + var poseA = new SteamVR_Utils.RigidTransform(new Vector3(0.10f, 1.20f, 0.30f), Quaternion.Euler(10f, 20f, 30f)); + var poseB = new SteamVR_Utils.RigidTransform(new Vector3(-0.40f, 0.80f, -0.20f), Quaternion.Euler(-15f, 40f, 5f)); + + //--- 1. 初回の姿勢がそのまま出るか --- + context.Log("1. 初回の姿勢"); + var device = new DeviceInfo(); + device.UpdateDeviceInfo(poseA, "VMCTEST_DEVICE"); + + result.CheckThat("初回の姿勢", + Vector3.Distance(device.transform.pos, poseA.pos) < 0.0001f && device.isOK, + $"初回に与えた姿勢が出てきません({device.transform.pos} / isOK={device.isOK})"); + + //--- 2. 復帰補間の間は過去値から徐々に近づく --- + //DeviceInfo は認識から LEAP_SECONDS(1秒) の間、飛び対策で過去値から補間する + context.Log("2. 復帰補間中の挙動"); + device.UpdateDeviceInfo(poseB, "VMCTEST_DEVICE"); + var duringWarmup = device.transform.pos; + result.CheckThat("復帰補間", + Vector3.Distance(duringWarmup, poseB.pos) > 0.01f, + $"認識直後なのに新しい姿勢がそのまま採用されています({duringWarmup})。飛び対策の補間が効いていません"); + + //--- 3. ウォームアップ後は与えた姿勢がそのまま出る --- + context.Log("3. ウォームアップ後の追従"); + //okTime = validFrames / Application.targetFrameRate なので、1秒ぶん呼べば補間が終わる + for (int i = 0; i < frameRate + 10; i++) + { + device.UpdateDeviceInfo(poseB, "VMCTEST_DEVICE"); + } + + var afterWarmup = device.transform.pos; + result.CheckThat("ウォームアップ後の追従", + Vector3.Distance(afterWarmup, poseB.pos) < 0.001f, + $"ウォームアップ後も姿勢が追従していません({afterWarmup} 期待 {poseB.pos})。" + + $"Application.targetFrameRate({Application.targetFrameRate})が正の値か確認してください"); + + //ウォームアップ後は次の姿勢が即座に反映されること + device.UpdateDeviceInfo(poseA, "VMCTEST_DEVICE"); + result.CheckThat("ウォームアップ後の即時反映", + Vector3.Distance(device.transform.pos, poseA.pos) < 0.001f, + $"ウォームアップ後なのに姿勢の変化が即座に反映されません({device.transform.pos} 期待 {poseA.pos})"); + + //--- 4. ゼロ姿勢(トラッキングロスト)は過去値に差し替えられる --- + context.Log("4. トラッキングロストの検出"); + device.UpdateDeviceInfo(new SteamVR_Utils.RigidTransform(Vector3.zero, Quaternion.identity), "VMCTEST_DEVICE"); + + result.CheckThat("トラッキングロストの検出", + device.isOK == false && Vector3.Distance(device.transform.pos, poseA.pos) < 0.001f, + $"ゼロ姿勢を受けたときに過去値へ差し替えられていません(isOK={device.isOK} pos={device.transform.pos})"); + + //--- 5. 一時停止中は位置が固定される --- + context.Log("5. トラッキング一時停止"); + var pausedDevice = new DeviceInfo(); + for (int i = 0; i < frameRate + 10; i++) + { + pausedDevice.UpdateDeviceInfo(poseA, "VMCTEST_PAUSED"); + } + + DeviceInfo.pauseTracking = true; + try + { + pausedDevice.UpdateDeviceInfo(poseB, "VMCTEST_PAUSED"); + result.CheckThat("トラッキング一時停止", + Vector3.Distance(pausedDevice.transform.pos, poseA.pos) < 0.001f, + $"一時停止中なのに位置が動いています({pausedDevice.transform.pos} 期待 {poseA.pos})"); + } + finally + { + DeviceInfo.pauseTracking = false; + } + + //--- 6. 種別ごとの無効化 --- + context.Log("6. 機器種別ごとの無効化"); + var trackerDevice = new DeviceInfo(); + for (int i = 0; i < frameRate + 10; i++) + { + trackerDevice.UpdateDeviceInfo(poseA, "VMCTEST_TRACKER"); + } + + DeviceInfo.trackerEnable = false; + try + { + trackerDevice.UpdateDeviceInfo(poseB, "VMCTEST_TRACKER"); + //無効時は saveAndSwapZeroTransform に切り替わり、ゼロ以外はそのまま記録される + result.CheckThat("機器種別の無効化", + trackerDevice.isOK, + "トラッカー無効時の処理でisOKが落ちています"); + } + finally + { + DeviceInfo.trackerEnable = true; + } + + yield break; + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs.meta b/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs.meta new file mode 100644 index 00000000..d201aa35 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_DeviceInfoTracking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f6b16aecbb1b28438883d68fff01d3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs b/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs new file mode 100644 index 00000000..00705a01 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs @@ -0,0 +1,113 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// 顔まわりのハードウェア入力(リップシンク)。 + /// + /// マイク無しで、実機から値が来た所と同じ地点に値を注入して表情への反映を確認する。 + /// 残る実機依存は「デバイスが繋がるか」だけになる。 + /// + /// VIVEのリップトラッキング/アイトラッキングの検査はプラグイン側へ移した + /// (PluginProjects/VMC.Plugin.ViveSR)。 + /// + public sealed class Scenario_FaceHardwareInputs : VMCTestScenario + { + public override string Name => "FaceHardwareInputs"; + + public override string Description => "リップシンクの反映"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + context.FaceController.EnableBlink = false; + yield return context.Step(5); + + //--- 2. リップシンク --- + context.Log("2. リップシンク(viseme)の反映"); + var lipSync = context.Window.LipSync; + if (lipSync == null) + { + result.CheckThat("リップシンクの参照", false, "ControlWPFWindow.LipSync が設定されていません"); + } + else + { + yield return CheckLipSync(context, result, lipSync); + } + + } + + private static IEnumerator CheckLipSync(VMCTestContext context, VMCTestResult result, DynamicOVRLipSync lipSync) + { + lipSync.MaxLevel = 1.0f; + lipSync.WeightThreashold = 0.0f; + lipSync.MaxWeightEmphasis = false; + lipSync.MaxWeightEnable = false; + + //「あ」を強く、「い」を弱く + lipSync.Test_ApplyVisemes(0.8f, 0.2f, 0f, 0f, 0f); + yield return context.Step(3); + + var basic = context.Capture("01_lipsync_basic", includeSent: false); + result.CheckThat("リップシンクの反映", + Near(basic.GetExpression("A"), 0.8f) && Near(basic.GetExpression("I"), 0.2f), + $"visemeが口の表情に反映されていません(A={basic.GetExpression("A"):F3} 期待0.800 / " + + $"I={basic.GetExpression("I"):F3} 期待0.200)"); + + //MaxLevel は全体の倍率 + lipSync.MaxLevel = 0.5f; + lipSync.Test_ApplyVisemes(0.8f, 0.2f, 0f, 0f, 0f); + yield return context.Step(3); + var scaled = context.Capture("tmp", false); + result.CheckThat("リップシンクのMaxLevel", + Near(scaled.GetExpression("A"), 0.4f), + $"MaxLevelが効いていません(A={scaled.GetExpression("A"):F3} 期待0.400)"); + + //しきい値未満は切り捨てられる + lipSync.MaxLevel = 1.0f; + lipSync.WeightThreashold = 0.3f; + lipSync.Test_ApplyVisemes(0.8f, 0.2f, 0f, 0f, 0f); + yield return context.Step(3); + var thresholded = context.Capture("tmp", false); + result.CheckThat("リップシンクのしきい値", + Near(thresholded.GetExpression("A"), 0.8f) && thresholded.GetExpression("I") < 0.01f, + $"しきい値未満のvisemeが切り捨てられていません(A={thresholded.GetExpression("A"):F3} / " + + $"I={thresholded.GetExpression("I"):F3} 期待0.000)"); + + //最大のものだけ残す + lipSync.WeightThreashold = 0.0f; + lipSync.MaxWeightEnable = true; + lipSync.Test_ApplyVisemes(0.5f, 0.9f, 0.3f, 0f, 0f); + yield return context.Step(3); + var maxOnly = context.Capture("02_lipsync_maxonly", includeSent: false); + result.CheckThat("リップシンクの最大値のみ", + Near(maxOnly.GetExpression("I"), 0.9f) + && maxOnly.GetExpression("A") < 0.01f && maxOnly.GetExpression("U") < 0.01f, + $"MaxWeightEnableで最大のviseme以外が残っています(A={maxOnly.GetExpression("A"):F3} " + + $"I={maxOnly.GetExpression("I"):F3} U={maxOnly.GetExpression("U"):F3})"); + + //強調(3倍・1.0でクランプ) + lipSync.MaxWeightEnable = false; + lipSync.MaxWeightEmphasis = true; + lipSync.Test_ApplyVisemes(0.2f, 0f, 0f, 0f, 0f); + yield return context.Step(3); + var emphasized = context.Capture("tmp", false); + result.CheckThat("リップシンクの強調", + Near(emphasized.GetExpression("A"), 0.6f), + $"MaxWeightEmphasis(3倍)が効いていません(A={emphasized.GetExpression("A"):F3} 期待0.600)"); + + //後片付け + lipSync.MaxWeightEmphasis = false; + lipSync.Test_ApplyVisemes(0f, 0f, 0f, 0f, 0f); + yield return context.Step(3); + } + + private static bool Near(float actual, float expected) => Mathf.Abs(actual - expected) < 0.02f; + } +} diff --git a/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs.meta b/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs.meta new file mode 100644 index 00000000..2c2c8daa --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_FaceHardwareInputs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29039c2ec7f853c44b7ea1cfc6b1df85 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_FaceMixing.cs b/Assets/Tests/Scenarios/Scenario_FaceMixing.cs new file mode 100644 index 00000000..ff8c10b0 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_FaceMixing.cs @@ -0,0 +1,148 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UniVRM10; + +namespace VMC.Tests +{ + /// + /// 表情の合成順序。 + /// + /// FaceController には + /// ベース(SetFace) → 加算(MixPresets, 1.0でクランプ) → 上書き(OverwritePresets) + /// の3段があり、リップシンク・VMCProtocol・MIDI・まばたき・モーション再生の + /// 5系統が同時に書き込む。優先順位が崩れると「口が動かない」「表情が戻らない」になる。 + /// + public sealed class Scenario_FaceMixing : VMCTestScenario + { + public override string Name => "FaceMixing"; + + public override string Description => "複数入力源からの表情合成の優先順位"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + context.FaceController.EnableBlink = false; + yield return context.Step(5); + + var face = context.FaceController; + var joy = ExpressionKey.CreateFromPreset(ExpressionPreset.happy); + var angry = ExpressionKey.CreateFromPreset(ExpressionPreset.angry); + var aa = ExpressionKey.CreateFromPreset(ExpressionPreset.aa); + + //--- 2. ベースの表情 --- + context.Log("2. ベース表情"); + face.SetFace(joy, 0.5f, false); + yield return context.Step(3); + result.CheckThat("ベース表情", + Near(context.Capture("tmp", false).GetExpression("Joy"), 0.5f), + $"SetFaceで設定した表情が反映されていません(Joy={context.Capture("tmp", false).GetExpression("Joy"):F3})"); + + //--- 3. 加算は足し合わされ、1.0でクランプされる --- + context.Log("3. 加算の合成とクランプ"); + face.MixPresets("SourceA", new[] { joy }, new[] { 0.3f }); + face.MixPresets("SourceB", new[] { joy }, new[] { 0.4f }); + yield return context.Step(3); + + var mixed = context.Capture("01_mixed", includeSent: false).GetExpression("Joy"); + //0.5(ベース) + 0.3 + 0.4 = 1.2 → 1.0にクランプ + result.CheckThat("加算の合成とクランプ", + Near(mixed, 1.0f), + $"複数ソースの加算とクランプが期待どおりではありません(Joy={mixed:F3} 期待 1.000)"); + + //加算値を下げると合計も下がる + face.MixPresets("SourceA", new[] { joy }, new[] { 0.1f }); + face.MixPresets("SourceB", new[] { joy }, new[] { 0.1f }); + yield return context.Step(3); + var lowered = context.Capture("tmp", false).GetExpression("Joy"); + result.CheckThat("加算値の反映", + Near(lowered, 0.7f), + $"加算値を下げても合計に反映されていません(Joy={lowered:F3} 期待 0.700)"); + + //--- 4. 上書きは加算より強い --- + context.Log("4. 上書きの優先"); + face.OverwritePresets("Playback", new[] { joy }, new[] { 0.2f }); + yield return context.Step(3); + + var overwritten = context.Capture("02_overwritten", includeSent: false).GetExpression("Joy"); + result.CheckThat("上書きの優先", + Near(overwritten, 0.2f), + $"OverwritePresetsがMixPresetsより優先されていません(Joy={overwritten:F3} 期待 0.200)"); + + //上書きを空にすると加算の合計に戻る + face.OverwritePresets("Playback", new ExpressionKey[0], new float[0]); + yield return context.Step(3); + var restored = context.Capture("tmp", false).GetExpression("Joy"); + result.CheckThat("上書き解除", + Near(restored, 0.7f), + $"上書きを解除しても加算の合計に戻りません(Joy={restored:F3} 期待 0.700)"); + + //--- 5. 別のキーは互いに影響しない --- + context.Log("5. キーごとの独立性"); + face.MixPresets("SourceA", new[] { angry }, new[] { 0.6f }); + yield return context.Step(3); + + var independent = context.Capture("03_independent", includeSent: false); + //SourceAはangryに切り替わったのでjoyへの寄与は消える(0.5 + 0.1(SourceB) = 0.6) + result.CheckThat("キーごとの独立性", + Near(independent.GetExpression("Angry"), 0.6f) && Near(independent.GetExpression("Joy"), 0.6f), + $"表情キーごとの合成が独立していません(Angry={independent.GetExpression("Angry"):F3} 期待 0.600 / " + + $"Joy={independent.GetExpression("Joy"):F3} 期待 0.600)"); + + //--- 6. VRM0.x互換名でも同じ表情を指せる --- + context.Log("6. VRM0.x互換名での指定"); + face.MixPresets("SourceA", new ExpressionKey[0], new float[0]); + face.MixPresets("SourceB", new ExpressionKey[0], new float[0]); + face.SetFace(new List { "Neutral" }, new List { 1f }, false); + yield return context.Step(3); + + //"A" は VRM0.x での aa の名前 + face.MixPresets("Vrm0Name", new[] { "A" }, new[] { 0.8f }); + yield return context.Step(3); + + var byVrm0Name = context.Capture("04_vrm0_name", includeSent: false).GetExpression("A"); + result.CheckThat("VRM0.x互換名での指定", + Near(byVrm0Name, 0.8f), + $"VRM0.x名(\"A\")で表情を指定できていません(A={byVrm0Name:F3} 期待 0.800)。" + + "VRM1.0モデルでもVRM0.x名で受信できる必要があります"); + + //--- 7. 存在しない表情名を送っても壊れない --- + context.Log("7. 存在しない表情名"); + //直前の "Vrm0Name" ソースが A=0.8 を加算し続けているので、先に解除しておく + //(加算は入力源ごとに保持され、解除するまで残る) + face.MixPresets("Vrm0Name", new string[0], new float[0]); + yield return context.Step(3); + + context.BeginErrorCapture(); + face.MixPresets("Unknown", new[] { "ThisExpressionDoesNotExist", "A" }, new[] { 1.0f, 0.4f }); + yield return context.Step(5); + var errors = context.EndErrorCapture(); + + var stillWorks = context.Capture("05_unknown_name", includeSent: false).GetExpression("A"); + result.CheckThat("存在しない表情名の無視", + errors.Count == 0 && Near(stillWorks, 0.4f), + $"存在しない表情名でエラー({errors.Count}件)、または他の表情が壊れました(A={stillWorks:F3} 期待 0.400)"); + + //--- 8. まばたきとの共存 --- + context.Log("8. まばたきの停止指定"); + face.EnableBlink = true; + face.StopBlink = true; + yield return context.Step(30); + + var blinkStopped = context.Capture("06_blink_stopped", includeSent: false); + result.CheckThat("まばたきの停止", + blinkStopped.GetExpression("Blink") < 0.01f, + $"StopBlink中なのにまばたきが適用されています(Blink={blinkStopped.GetExpression("Blink"):F3})"); + + face.EnableBlink = false; + face.StopBlink = false; + } + + private static bool Near(float actual, float expected) => Mathf.Abs(actual - expected) < 0.02f; + } +} diff --git a/Assets/Tests/Scenarios/Scenario_FaceMixing.cs.meta b/Assets/Tests/Scenarios/Scenario_FaceMixing.cs.meta new file mode 100644 index 00000000..b204906b --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_FaceMixing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6870ce8986b956458e71135140a137d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_KeyActions.cs b/Assets/Tests/Scenarios/Scenario_KeyActions.cs new file mode 100644 index 00000000..47131567 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_KeyActions.cs @@ -0,0 +1,167 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// ショートカットキー(キーアクション)の検証。 + /// + /// InputManager.CheckKey は「押されているキーの集合に、そのアクションのキーが全部含まれるか」で + /// 判定し、同時押しの多いアクションを優先する。条件分岐が多く手動確認が難しい。 + /// + public sealed class Scenario_KeyActions : VMCTestScenario + { + public override string Name => "KeyActions"; + + public override string Description => "ショートカットキーによる表情・ハンド・機能の実行"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + private const int KeyA = 65; + private const int KeyB = 66; + private const int KeyC = 67; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + //まばたきが表情を上書きしないようにする + context.FaceController.EnableBlink = false; + yield return context.Step(5); + + //--- 2. キーアクションを登録する --- + context.Log("2. キーアクションの登録"); + Settings.Current.KeyActions = new List + { + //A単独 → Joy + FaceAction("Joy_A", new[] { KeyA }, "Joy", 1.0f), + //A+B同時 → Angry (Aだけのアクションより優先されるはず) + FaceAction("Angry_AB", new[] { KeyA, KeyB }, "Angry", 1.0f), + //C単独 → 背景色を緑に変える機能 + FunctionAction("Green_C", new[] { KeyC }, Functions.ColorGreen), + }; + yield return context.Step(2); + + //--- 3. 単独キー --- + context.Log("3. 単独キーの実行"); + PressKey(KeyA); + yield return context.Step(5); + + var afterA = context.Capture("01_key_a", includeSent: false); + result.CheckThat("単独キーでの表情", + Mathf.Abs(afterA.GetExpression("Joy") - 1.0f) < 0.01f, + $"Aキーで Joy が適用されていません(Joy={afterA.GetExpression("Joy"):F3})"); + + ReleaseKey(KeyA); + yield return context.Step(3); + + //--- 4. 同時押しは「キーの多い方」が優先される --- + context.Log("4. 同時押しの優先"); + PressKey(KeyA); + yield return context.Step(2); + PressKey(KeyB); + yield return context.Step(5); + + var afterAB = context.Capture("02_key_ab", includeSent: false); + var angry = afterAB.GetExpression("Angry"); + var joyOnAB = afterAB.GetExpression("Joy"); + result.CheckThat("同時押しの優先", + Mathf.Abs(angry - 1.0f) < 0.01f && joyOnAB < 0.01f, + $"A+Bの同時押しでAngryが優先されていません(Angry={angry:F3} Joy={joyOnAB:F3})。" + + "キーの少ないアクションが後から上書きしている可能性があります"); + + ReleaseKey(KeyB); + ReleaseKey(KeyA); + yield return context.Step(3); + + //--- 5. 機能アクション --- + context.Log("5. 機能アクションの実行"); + Settings.Current.BackgroundColor = new Color(0.5f, 0.5f, 0.5f, 1f); + yield return context.Step(2); + + PressKey(KeyC); + yield return context.Step(5); + ReleaseKey(KeyC); + yield return context.Step(3); + + var background = Settings.Current.BackgroundColor; + result.CheckThat("機能アクションの実行", + background.g > 0.9f && background.r < 0.1f && background.b < 0.1f, + $"Cキーで背景色が緑になっていません({background})"); + + //--- 6. 未登録のキーでは何も起きない --- + context.Log("6. 未登録キー"); + context.FaceController.SetFace(new List(), new List(), false); + yield return context.Step(3); + var beforeUnknown = context.Capture("03_before_unknown_key", includeSent: false); + + PressKey(90); //Z + yield return context.Step(5); + ReleaseKey(90); + yield return context.Step(3); + + var afterUnknown = context.Capture("04_after_unknown_key", includeSent: false); + var expressionDelta = 0f; + foreach (var entry in beforeUnknown.Expressions) + { + expressionDelta = Mathf.Max(expressionDelta, Mathf.Abs(entry.Value - afterUnknown.GetExpression(entry.Name))); + } + result.CheckThat("未登録キーで何も起きないこと", + expressionDelta < 0.01f, + $"登録していないキーで表情が変わりました(最大差 {expressionDelta:F3})"); + } + + private static void PressKey(int keyCode) + => KeyboardAction.KeyDownEvent?.Invoke(null, new KeyboardEventArgs(keyCode)); + + private static void ReleaseKey(int keyCode) + => KeyboardAction.KeyUpEvent?.Invoke(null, new KeyboardEventArgs(keyCode)); + + private static KeyAction FaceAction(string name, int[] keyCodes, string faceName, float strength) + { + var action = NewAction(name, keyCodes); + action.FaceAction = true; + action.FaceNames = new List { faceName }; + action.FaceStrength = new List { strength }; + return action; + } + + private static KeyAction FunctionAction(string name, int[] keyCodes, Functions function) + { + var action = NewAction(name, keyCodes); + action.FunctionAction = true; + action.Function = function; + return action; + } + + private static KeyAction NewAction(string name, int[] keyCodes) + { + var configs = new List(); + foreach (var keyCode in keyCodes) + { + //InputManager.KeyboardAction_KeyDown が組み立てる KeyConfig と + //IsEqualKeyCode で一致するように同じ内容にする。 + //keyName も比較対象なので、実際のイベントと同じ値を入れる必要がある + configs.Add(new KeyConfig + { + type = KeyTypes.Keyboard, + actionType = KeyActionTypes.Face, + keyCode = keyCode, + keyName = new KeyboardEventArgs(keyCode).KeyName, + }); + } + return new KeyAction + { + Name = name, + KeyConfigs = configs, + HandAngles = new List(), + FaceNames = new List(), + FaceStrength = new List(), + LipSyncMaxLevel = 1f, + }; + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_KeyActions.cs.meta b/Assets/Tests/Scenarios/Scenario_KeyActions.cs.meta new file mode 100644 index 00000000..d92f288b --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_KeyActions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba30a9c1e0291b14487cf683f9d9b2e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs b/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs new file mode 100644 index 00000000..4472dcfe --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs @@ -0,0 +1,135 @@ +using System.Collections; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// 別のアバターを読み込んだとき。 + /// + /// モデルAを読んでキャリブレーション + /// → モデルB(VRM0.x⇔VRM1.0の他方)に差し替え + /// → 記録済みのトラッカー姿勢で自動再キャリブレーションが走る + /// → Tポーズを取り直さずにトラッカーへ追従する + /// → 表情とLookAtも新しいモデルに引き継がれる + /// + /// ModelKey が読み込み元、もう一方が切り替え先になる。 + /// VRM0.x→VRM1.0 と VRM1.0→VRM0.x の両方向を検証する。 + /// + public sealed class Scenario_ModelSwitch : VMCTestScenario + { + public override string Name => "ModelSwitch"; + + public override string Description => "別アバターに差し替えたときの自動再キャリブレーションと追従"; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + var fromKey = context.ModelKey; + var toKey = fromKey == VMCTestModels.Vrm0 ? VMCTestModels.Vrm10 : VMCTestModels.Vrm0; + var toPath = context.Config.GetModelPath(toKey); + + if (toPath == null) + { + //切り替え先が無いと検証できない。失敗ではなくスキップ扱いにする + result.Skipped = true; + result.SkipReason = $"切り替え先の {toKey} のVRMが設定されていません"; + yield break; + } + + //--- 1. モデルAを読み込んでキャリブレーション --- + context.Log($"1. モデルA({fromKey})の読み込みとキャリブレーション"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(fromKey)); + + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyBlendShape = true; + setting.ApplyLookAt = true; + }); + + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + //表情と視線も入れておく(モデル差し替えで引き継がれるかを見る) + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(new[] + { + new System.Collections.Generic.KeyValuePair("Joy", 0.6f), + })); + context.Inject(receiver, VMCTestOscBuilder.Eye(true, new Vector3(0.3f, 0.05f, 1.0f))); + yield return context.Step(5); + + var beforeSwitch = context.Capture("01_before_switch", includeSent: false); + result.CheckSnapshot(context, beforeSwitch); + + result.CheckThat("切り替え前のキャリブレーション記録", + Settings.Current.LastCalibrationSnapshot != null && + Settings.Current.LastCalibrationSnapshot.Poses.Count >= 6, + "キャリブレーション時のトラッカー姿勢が記録されていません。自動再キャリブレーションが動作しません"); + + //--- 2. 別のアバターに差し替える --- + context.Log($"2. モデルB({toKey})への差し替え"); + //自動再キャリブレーションを有効にする(これが本シナリオの検証対象) + Settings.Current.EnableAutoCalibrationOnModelLoad = true; + + var previousModel = context.CurrentModel; + yield return context.SwitchModel(toPath); + + result.CheckThat("モデルの差し替え", + context.CurrentModel != null && context.CurrentModel != previousModel, + "モデルが差し替わっていません"); + + //--- 3. 自動再キャリブレーションの完了を待つ --- + context.Log("3. 自動再キャリブレーションの待機"); + //トラッカーは流し続ける(実運用と同じ状況にする) + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitUntil( + () => IKManager.Instance.CalibrationState == CalibrationState.Calibrated, + 900, "自動再キャリブレーションの完了"); + yield return context.Step(20); + + result.CheckThat("自動再キャリブレーション", + IKManager.Instance.CalibrationState == CalibrationState.Calibrated, + $"別アバター読み込み後にキャリブレーションが完了していません(state={IKManager.Instance.CalibrationState})。" + + "Tポーズを取り直す必要が出てしまいます"); + + var afterSwitch = context.Capture("02_after_switch_ipose", includeSent: false); + result.CheckSnapshot(context, afterSwitch); + + //--- 4. 差し替え後もトラッカーに追従するか --- + //実際のVMCProtocol送信側は毎フレーム送り続けるので、表情・視線も送り直した状態にする。 + //(表情はFaceControllerが値を保持するので送り直さなくても残るが、 + // 視線のターゲットは旧モデルの頭ボーン配下にあり破棄されているため、再送で復帰する) + context.Log("4. 差し替え後の追従確認"); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(new[] + { + new System.Collections.Generic.KeyValuePair("Joy", 0.6f), + })); + context.Inject(receiver, VMCTestOscBuilder.Eye(true, new Vector3(0.3f, 0.05f, 1.0f))); + yield return context.Step(20); + + var afterSwitchTpose = context.Capture("03_after_switch_tpose", includeSent: false); + result.CheckSnapshot(context, afterSwitchTpose); + + var delta = VMCTestSnapshot.MaxBoneRotationDelta(afterSwitch, afterSwitchTpose, out var movedBone); + result.CheckThat("差し替え後の追従", + delta > 15f, + $"別アバターに差し替えた後、トラッカーに追従していません(最大回転差 {delta:F2}度 / {movedBone ?? "なし"})"); + + //--- 5. 表情とLookAtが新しいモデルにも適用されるか --- + context.Log("5. 表情とLookAtの引き継ぎ確認"); + var joy = afterSwitchTpose.GetExpression("Joy"); + result.CheckThat("表情の引き継ぎ", + Mathf.Abs(joy - 0.6f) < 0.01f, + $"モデル差し替え後に表情が引き継がれていません(Joy {joy:F3} 期待 0.600)"); + + result.CheckThat("LookAtの引き継ぎ", + afterSwitchTpose.HasLookAt && Mathf.Abs(afterSwitchTpose.LookAtYaw) > 5f, + $"モデル差し替え後に視線が引き継がれていません(has={afterSwitchTpose.HasLookAt} yaw={afterSwitchTpose.LookAtYaw:F2})"); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs.meta b/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs.meta new file mode 100644 index 00000000..50a69b39 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_ModelSwitch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5dd2adbce1d20694990ef949c815f02c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs b/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs new file mode 100644 index 00000000..997bae09 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs @@ -0,0 +1,262 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// 「すべて受信した状態」のVRMA書き出しと読み込みの往復。 + /// + /// トラッカー(VRIK) + 表情 + LookAt を全部受けた状態を記録 + /// → VRMAに書き出し + /// → 書き出したVRMAを読み込んで再生 + /// → 記録時のボーン・表情・視線が復元されるか + /// + /// 「合成後のデータが書き出されているか」を、記録時のスナップショットと + /// 再生後のスナップショットの一致で確認する。 + /// 途中で姿勢を変えるので、モーションが時間変化として記録されていることも見る。 + /// + public sealed class Scenario_MotionVrmaRoundTrip : VMCTestScenario + { + public override string Name => "MotionVrmaRoundTrip"; + + public override string Description => "受信状態を記録→VRMA書き出し→読み込みで復元されるか"; + + //記録するポーズAとポーズBの表情 + private static readonly KeyValuePair[] FaceA = + { + new KeyValuePair("Joy", 0.8f), + new KeyValuePair("A", 0.4f), + }; + private static readonly KeyValuePair[] FaceB = + { + new KeyValuePair("Sorrow", 0.6f), + new KeyValuePair("O", 0.5f), + }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + //--- 2. トラッカーを受けてキャリブレーション --- + context.Log("2. トラッカー受信とキャリブレーション"); + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyBlendShape = true; + setting.ApplyLookAt = true; + }); + + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + //--- 3. 記録設定 --- + Settings.Current.MotionRecord_Fps = 30; + Settings.Current.MotionRecord_CountdownSeconds = 0; + Settings.Current.MotionRecord_SaveMotion = true; + Settings.Current.MotionRecord_SaveExpressionPreset = true; + Settings.Current.MotionRecord_SaveExpressionCustom = true; + Settings.Current.MotionRecord_SaveLookAt = true; + + var recorder = context.MotionRecorder; + if (recorder == null) + { + throw new System.Exception("MotionRecorder が見つかりません"); + } + + //--- 4. ポーズA(Iポーズ + 表情A + 右を見る)を記録 --- + context.Log("4. ポーズAの記録"); + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(FaceA)); + context.Inject(receiver, VMCTestOscBuilder.Eye(true, new Vector3(0.35f, 0.05f, 1.0f))); + yield return context.Step(5); + + recorder.StartRecording(); + yield return context.Step(40); //30fps記録 x 約0.67秒 + var poseA = context.Capture("01_recorded_pose_a", includeSent: false); + var frameA = recorder.Test_RecordedFrameCount - 1; + + //--- 5. ポーズB(Tポーズ + 表情B + 左を見る)を記録 --- + context.Log("5. ポーズBの記録"); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(FaceB)); + context.Inject(receiver, VMCTestOscBuilder.Eye(true, new Vector3(-0.35f, -0.10f, 1.0f))); + yield return context.Step(40); + + var poseB = context.Capture("02_recorded_pose_b", includeSent: false); + var frameB = recorder.Test_RecordedFrameCount - 1; + + recorder.StopRecording(); + yield return context.Step(2); + + result.CheckThat("モーションの記録", + recorder.Test_State == MotionRecorder.RecordState.Recorded && frameB > frameA && frameA >= 0, + $"記録できていません(state={recorder.Test_State} frameA={frameA} frameB={frameB})"); + + //ポーズAとBがちゃんと違う姿勢であること(そうでないと往復検証が意味を持たない) + var poseDelta = VMCTestSnapshot.MaxBoneRotationDelta(poseA, poseB, out _); + result.CheckThat("記録した2姿勢の差", + poseDelta > 15f, + $"ポーズAとBがほぼ同じです(最大回転差 {poseDelta:F2}度)。往復検証が成立しません"); + + if (recorder.Test_State != MotionRecorder.RecordState.Recorded) yield break; + + //--- 6. VRMAに書き出す --- + context.Log("6. VRMAの書き出し"); + var vrmaPath = context.OutputPath($"{Name}.{context.ModelKey}.vrma"); + recorder.Test_SaveRecording(vrmaPath, 0, 0, recorder.Test_RecordedFrameCount - 1); + yield return context.Step(2); + + var fileInfo = new FileInfo(vrmaPath); + result.CheckThat("VRMAの書き出し", + fileInfo.Exists && fileInfo.Length > 1024, + $"VRMAが書き出されていません({vrmaPath})"); + if (fileInfo.Exists == false) yield break; + + //--- 7. 比較の基準として、記録データそのもののプレビューを取る --- + //プレビューもVRMA再生も同じHumanPose経由なので、両者の差はVRMAの書き出し/読み込みの精度だけになる。 + //これで「ファイル形式の往復」と「マッスル空間のリターゲット誤差」を切り分けられる。 + context.Log("7. 記録データのプレビュー取得(比較の基準)"); + context.SetReceiverActive(receiver, false); + yield return context.Step(2); + + recorder.PreviewSeek(frameA); + yield return context.Step(5); + var previewA = context.Capture("03_preview_pose_a", includeSent: false); + + recorder.PreviewSeek(frameB); + yield return context.Step(5); + var previewB = context.Capture("04_preview_pose_b", includeSent: false); + + recorder.PreviewStop(); + yield return context.Step(2); + context.SetReceiverActive(receiver, true); + yield return context.Step(2); + + //--- 8. 他の入力を止めてから、書き出したVRMAを再生する --- + context.Log("8. VRMAの読み込みと再生"); + //視線のOSCターゲットを外す(SpecifiedTransformが残っているとSetYawPitchManuallyが効かない) + context.Inject(receiver, VMCTestOscBuilder.Eye(false, Vector3.zero)); + yield return context.Step(2); + context.SetReceiverActive(receiver, false); + yield return context.Step(2); + + var player = context.MotionPlayer; + if (player == null) + { + throw new System.Exception("MotionPlayer が見つかりません"); + } + + yield return context.Await(player.ApplyPoseByPathAsync(vrmaPath, frameA)); + yield return context.Step(5); + var replayA = context.Capture("05_replayed_pose_a", includeSent: false); + + yield return context.Await(player.ApplyPoseByPathAsync(vrmaPath, frameB)); + yield return context.Step(5); + var replayB = context.Capture("06_replayed_pose_b", includeSent: false); + + //--- 9. 一致確認 --- + context.Log("9. 往復の一致確認"); + //(a) VRMAファイル自体の往復。プレビューと再生の差はglTFの書き出し/読み込みの精度だけ + VerifyVrmaFile(context, result, "A", previewA, replayA); + VerifyVrmaFile(context, result, "B", previewB, replayB); + //(b) 記録→再生の総合。マッスル空間のリターゲット誤差が乗る + VerifyRoundTrip(context, result, "A", poseA, replayA); + VerifyRoundTrip(context, result, "B", poseB, replayB); + + //再生した2フレームがちゃんと違うこと(=モーションが時間変化として記録されている) + var replayDelta = VMCTestSnapshot.MaxBoneRotationDelta(replayA, replayB, out _); + result.CheckThat("再生した2フレームの差", + replayDelta > 15f, + $"VRMAの2フレームがほぼ同じです(最大回転差 {replayDelta:F2}度)。姿勢の変化が記録されていません"); + + result.CheckSnapshot(context, previewA); + result.CheckSnapshot(context, previewB); + result.CheckSnapshot(context, replayA); + result.CheckSnapshot(context, replayB); + + player.Stop(); + } + + /// + /// VRMAファイルの往復。記録データのプレビューと、書き出したVRMAの再生を比べる。 + /// どちらもHumanPose経由なので、差が出たらglTFの書き出し/読み込み側の問題。 + /// + private static void VerifyVrmaFile(VMCTestContext context, VMCTestResult result, string label, + VMCTestSnapshot preview, VMCTestSnapshot replayed) + { + //見た目の姿勢が保たれているか。ボーン単位の回転が多少違っても、 + //末端(頭・手・足)の向きが同じならアバターの見た目は変わらない + var endEffectorDifferences = VMCTestSnapshot.CompareEndEffectors(preview, replayed, + context.Config.VrmaEndEffectorToleranceDegrees, out var maxEnd, out var worstEnd); + result.CheckThat($"VRMAファイルの往復・見た目({label})", + endEffectorDifferences.Count == 0, + $"書き出したVRMAで末端の向きが変わっています(最大 {maxEnd:F2}度 @ {worstEnd}): " + + string.Join(", ", endEffectorDifferences)); + + //ボーン単位。Humanoidのリターゲットが腕のツイストを配分し直すぶんだけ緩く見る + var boneDifferences = VMCTestSnapshot.CompareBoneRotations(preview, replayed, + context.Config.VrmaFileToleranceDegrees, out var maxAngle, out var worstBone); + result.CheckThat($"VRMAファイルの往復・ボーン単位({label})", + boneDifferences.Count == 0, + $"書き出したVRMAが記録データと一致しません(最大 {maxAngle:F2}度 @ {worstBone}, {boneDifferences.Count}本): " + + string.Join(", ", boneDifferences.GetRange(0, Mathf.Min(8, boneDifferences.Count)))); + + Debug.Log($"[VMCTest] VRMAファイルの往復誤差({label}): 末端 最大{maxEnd:F3}度 @ {worstEnd} / ボーン単位 最大{maxAngle:F2}度 @ {worstBone}"); + } + + private static void VerifyRoundTrip(VMCTestContext context, VMCTestResult result, string label, + VMCTestSnapshot recorded, VMCTestSnapshot replayed) + { + var config = context.Config; + + //指はマッスル空間の表現力が特に低いので別枠で見る + var bodyDifferences = VMCTestSnapshot.CompareBoneRotations(recorded, replayed, + config.MotionRetargetToleranceDegrees, name => VMCTestSnapshot.IsFingerBone(name) == false, + out var maxBody, out var worstBody); + result.CheckThat($"ボーンの往復・指以外({label})", + bodyDifferences.Count == 0, + $"記録時と再生後でボーンが一致しません(最大 {maxBody:F2}度 @ {worstBody}, {bodyDifferences.Count}本): " + + string.Join(", ", bodyDifferences.GetRange(0, Mathf.Min(8, bodyDifferences.Count)))); + + var fingerDifferences = VMCTestSnapshot.CompareBoneRotations(recorded, replayed, + config.MotionFingerToleranceDegrees, VMCTestSnapshot.IsFingerBone, + out var maxFinger, out var worstFinger); + result.CheckThat($"ボーンの往復・指({label})", + fingerDifferences.Count == 0, + $"記録時と再生後で指が一致しません(最大 {maxFinger:F2}度 @ {worstFinger}, {fingerDifferences.Count}本): " + + string.Join(", ", fingerDifferences.GetRange(0, Mathf.Min(8, fingerDifferences.Count)))); + + //見た目の姿勢(末端の向き)が保たれているか + var endEffectorDifferences = VMCTestSnapshot.CompareEndEffectors(recorded, replayed, + config.MotionRetargetToleranceDegrees, out var maxEnd, out var worstEnd); + result.CheckThat($"記録→再生の見た目({label})", + endEffectorDifferences.Count == 0, + $"記録時と再生後で末端の向きが変わっています(最大 {maxEnd:F2}度 @ {worstEnd}): " + + string.Join(", ", endEffectorDifferences)); + + Debug.Log($"[VMCTest] 記録→再生のリターゲット誤差({label}): 末端 最大{maxEnd:F2}度 @ {worstEnd} / " + + $"指以外 最大{maxBody:F2}度 @ {worstBody} / 指 最大{maxFinger:F2}度 @ {worstFinger}"); + + var expressionDifferences = VMCTestSnapshot.CompareExpressions(recorded, replayed, + config.MotionWeightTolerance, out var maxWeight, out var worstKey); + result.CheckThat($"表情の往復({label})", + expressionDifferences.Count == 0, + $"記録時と再生後で表情が一致しません(最大 {maxWeight:F3} @ {worstKey}): " + + string.Join(", ", expressionDifferences)); + + var yawDelta = Mathf.Abs(Mathf.DeltaAngle(recorded.LookAtYaw, replayed.LookAtYaw)); + var pitchDelta = Mathf.Abs(Mathf.DeltaAngle(recorded.LookAtPitch, replayed.LookAtPitch)); + result.CheckThat($"視線の往復({label})", + yawDelta < config.MotionRotationToleranceDegrees && pitchDelta < config.MotionRotationToleranceDegrees, + $"記録時と再生後で視線が一致しません(yaw {recorded.LookAtYaw:F2}->{replayed.LookAtYaw:F2} / " + + $"pitch {recorded.LookAtPitch:F2}->{replayed.LookAtPitch:F2})"); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs.meta b/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs.meta new file mode 100644 index 00000000..cd52008f --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_MotionVrmaRoundTrip.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3d57363280ea797489073a034b8f7760 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs b/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs new file mode 100644 index 00000000..c9c16a79 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs @@ -0,0 +1,128 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// VMCProtocol受信機を複数使ったときの挙動。 + /// + /// 「トラッカーは1台目から、表情は2台目から」のような使い方ができる。 + /// 受信機ごとにVirtualAvatarが作られるので、適用範囲の分離と + /// 片方を無効にしたときの独立性が壊れやすい。 + /// + public sealed class Scenario_MultipleReceivers : VMCTestScenario + { + public override string Name => "MultipleReceivers"; + + public override string Description => "複数のVMCProtocol受信機の分離と独立性"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + context.FaceController.EnableBlink = false; + + //--- 2. 受信機を2つ作る --- + //1台目: トラッカーのみ / 2台目: 表情と視線のみ + context.Log("2. 受信機を2つ作成"); + var trackerReceiver = context.CreateReceiver(setting => + { + setting.Name = "TrackerOnly"; + setting.ApplyTracker = true; + setting.ApplyBlendShape = false; + setting.ApplyLookAt = false; + }); + var faceReceiver = context.CreateReceiver(setting => + { + setting.Name = "FaceOnly"; + setting.ApplyTracker = false; + setting.ApplyBlendShape = true; + setting.ApplyLookAt = true; + }); + + result.CheckThat("受信機の作成", + context.Window.externalMotionReceivers.Count >= 2, + $"受信機が2つ作られていません({context.Window.externalMotionReceivers.Count}個)"); + + //--- 3. それぞれの担当だけが効くか --- + context.Log("3. 担当範囲の分離"); + context.InjectTrackerRig(trackerReceiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(trackerReceiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + var ipose = context.Capture("01_two_receivers_ipose", includeSent: false); + result.CheckSnapshot(context, ipose); + + //トラッカー担当でない方にトラッカーを送っても動かないこと + context.InjectTrackerRig(faceReceiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + var afterWrongReceiver = context.Capture("tmp", includeSent: false); + var wrongDelta = VMCTestSnapshot.MaxBoneRotationDelta(ipose, afterWrongReceiver, out _); + result.CheckThat("トラッカー無効の受信機", + wrongDelta < 5f, + $"ApplyTracker=falseの受信機にトラッカーを送ったのにアバターが動きました(最大回転差 {wrongDelta:F2}度)"); + + //トラッカー担当に送れば動くこと + context.InjectTrackerRig(trackerReceiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + var tpose = context.Capture("02_two_receivers_tpose", includeSent: false); + result.CheckSnapshot(context, tpose); + + var rightDelta = VMCTestSnapshot.MaxBoneRotationDelta(ipose, tpose, out var movedBone); + result.CheckThat("トラッカー担当の受信機", + rightDelta > 15f, + $"トラッカー担当の受信機に送ってもアバターが動きません(最大回転差 {rightDelta:F2}度 / {movedBone ?? "なし"})"); + + //--- 4. 表情は表情担当だけが効く --- + context.Log("4. 表情の分離"); + context.Inject(trackerReceiver, VMCTestOscBuilder.BlendShapes(new[] + { + new KeyValuePair("Angry", 0.9f), + })); + yield return context.Step(5); + var afterWrongFace = context.Capture("tmp", includeSent: false); + result.CheckThat("表情無効の受信機", + afterWrongFace.GetExpression("Angry") < 0.01f, + $"ApplyBlendShape=falseの受信機で表情が適用されました(Angry={afterWrongFace.GetExpression("Angry"):F3})"); + + context.Inject(faceReceiver, VMCTestOscBuilder.BlendShapes(new[] + { + new KeyValuePair("Joy", 0.7f), + })); + context.Inject(faceReceiver, VMCTestOscBuilder.Eye(true, new Vector3(0.3f, 0.05f, 1.0f))); + yield return context.Step(5); + + var withFace = context.Capture("03_two_receivers_face", includeSent: false); + result.CheckSnapshot(context, withFace); + result.CheckThat("表情担当の受信機", + Mathf.Abs(withFace.GetExpression("Joy") - 0.7f) < 0.01f + && withFace.HasLookAt && Mathf.Abs(withFace.LookAtYaw) > 5f, + $"表情担当の受信機で表情/視線が適用されていません(Joy={withFace.GetExpression("Joy"):F3} yaw={withFace.LookAtYaw:F2})"); + + //--- 5. 片方を無効にしても、もう片方は動き続ける --- + context.Log("5. 片方の無効化"); + context.SetReceiverActive(faceReceiver, false); + yield return context.Step(5); + + context.InjectTrackerRig(trackerReceiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + var afterDisable = context.Capture("04_face_receiver_disabled", includeSent: false); + result.CheckSnapshot(context, afterDisable); + + var recovered = VMCTestSnapshot.MaxBoneRotationDelta(tpose, afterDisable, out _); + result.CheckThat("片方無効時の独立性", + recovered > 15f, + $"表情用受信機を無効にしたら、トラッカー用受信機まで止まりました(最大回転差 {recovered:F2}度)"); + + context.SetReceiverActive(faceReceiver, true); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs.meta b/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs.meta new file mode 100644 index 00000000..a98d1826 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_MultipleReceivers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9aeb4f27d3d0cdc4698ba02da342f74d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs b/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs new file mode 100644 index 00000000..1c0c0308 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// コントロールパネル(WPF)との通信に使う全コマンド型のシリアライズ往復。 + /// + /// PipeCommands は DataContractSerializer でやり取りされる。 + /// メンバーの追加漏れ・[OptionalField]の付け忘れ・新しいenum値の追加などで + /// 値が欠落したり、旧バージョンとの通信が例外になったりする。 + /// 全型に既定値以外の値を詰めて往復させ、値が保たれるかを一括で確認する。 + /// + public sealed class Scenario_PipeCommandsSerialization : VMCTestScenario + { + public override string Name => "PipeCommandsSerialization"; + + public override string Description => "WPFとの通信コマンド全型のシリアライズ往復"; + + public override bool RequiresModel => false; + + public override IReadOnlyList Models => new[] { VMCTestModels.None }; + + private const float FloatTolerance = 1e-4f; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. コマンド型の収集"); + + //PipeCommands のネストクラス + 同じアセンブリの公開データ型 + var assembly = typeof(PipeCommands).Assembly; + var types = new List(); + types.AddRange(typeof(PipeCommands).GetNestedTypes(BindingFlags.Public)); + types.AddRange(assembly.GetTypes().Where(t => t.IsPublic && t.IsClass && t.IsAbstract == false + && t.Namespace == typeof(PipeCommands).Namespace + && t != typeof(PipeCommands) + && t.GetConstructor(Type.EmptyTypes) != null)); + + types = types.Where(t => t.IsClass && t.IsAbstract == false && t.GetConstructor(Type.EmptyTypes) != null) + .Distinct() + .OrderBy(t => t.FullName, StringComparer.Ordinal) + .ToList(); + + Debug.Log($"[VMCTest] シリアライズ往復の対象: {types.Count} 型"); + + result.CheckThat("コマンド型の収集", + types.Count > 50, + $"コマンド型が想定より少ないです({types.Count}型)。収集条件が壊れていないか確認してください"); + + //--- 2. 往復 --- + context.Log($"2. {types.Count}型のシリアライズ往復"); + var failures = new List(); + var mismatches = new List(); + int tested = 0; + + foreach (var type in types) + { + object filled; + try + { + filled = VMCTestObjectFiller.CreateFilled(type, 1); + } + catch (Exception ex) + { + failures.Add($"{type.Name}: 値の生成に失敗 {ex.GetType().Name} {ex.Message}"); + continue; + } + if (filled == null) + { + failures.Add($"{type.Name}: インスタンスを生成できません"); + continue; + } + + object restored; + try + { + var bytes = BinarySerializer.Serialize(filled); + restored = BinarySerializer.Deserialize(bytes, type); + } + catch (Exception ex) + { + //ここで落ちる型は、実際の通信でも例外になる + failures.Add($"{type.Name}: {ex.GetType().Name} {ex.Message}"); + continue; + } + + tested++; + var differences = VMCTestObjectComparer.Compare(filled, restored, FloatTolerance, 5); + if (differences.Count > 0) + { + mismatches.Add($"{type.Name}: {string.Join(" / ", differences)}"); + } + + //型数が多いので、たまにフレームを回してエディタが固まらないようにする + if (tested % 40 == 0) yield return null; + } + + result.CheckThat("シリアライズの例外", + failures.Count == 0, + $"シリアライズできない型があります({failures.Count}件): " + + string.Join("\n ", failures.Take(15))); + + result.CheckThat("シリアライズ往復の値", + mismatches.Count == 0, + $"往復で値が変わる型があります({mismatches.Count}件): " + + string.Join("\n ", mismatches.Take(15))); + + Debug.Log($"[VMCTest] シリアライズ往復: {tested}型を検証 / 例外 {failures.Count}件 / 値の不一致 {mismatches.Count}件"); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs.meta b/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs.meta new file mode 100644 index 00000000..3b5e2431 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_PipeCommandsSerialization.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7ecb3ca1a1bb7744b975af0cea7394d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs b/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs new file mode 100644 index 00000000..d50cc57b --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs @@ -0,0 +1,249 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; + +namespace VMC.Tests +{ + /// + /// 描画まわりと長時間安定性。 + /// - 写真撮影(PNG書き出し・透過背景) + /// - スプリングボーンが動くか / 発散しないか + /// - アバターを何度も入れ替えたときのリーク + /// - 1フレームの処理時間 + /// + public sealed class Scenario_RenderingAndStability : VMCTestScenario + { + public override string Name => "RenderingAndStability"; + + public override string Description => "写真撮影・スプリングボーン・モデル入れ替えのリーク・処理時間"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + /// アバターを入れ替える回数 + private const int ReloadCount = 8; + + /// 1フレームの処理時間の上限(ms)。致命的な劣化だけを捕まえるゆるい値 + private const float FrameBudgetMs = 200f; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込みとキャリブレーション"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var receiver = context.CreateReceiver(setting => setting.ApplyTracker = true); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + //--- 2. 写真撮影 --- + context.Log("2. 写真撮影"); + yield return TakePhoto(context, result, transparent: false, label: "opaque"); + yield return TakePhoto(context, result, transparent: true, label: "transparent"); + + //--- 3. スプリングボーン --- + context.Log("3. スプリングボーン"); + yield return CheckSpringBones(context, result, receiver); + + //--- 4. 1フレームの処理時間 --- + context.Log("4. 処理時間の測定"); + yield return MeasureFrameTime(context, result); + + //--- 5. モデル入れ替えのリーク --- + context.Log($"5. アバターを{ReloadCount}回入れ替え"); + yield return CheckReloadLeak(context, result); + } + + private static IEnumerator TakePhoto(VMCTestContext context, VMCTestResult result, bool transparent, string label) + { + var camera = CameraManager.Current != null ? CameraManager.Current.ControlCamera : null; + if (camera == null) + { + result.CheckThat($"写真撮影({label})", false, "ControlCameraが見つかりません"); + yield break; + } + + byte[] png = null; + var resolution = new Resolution { width = 640, height = 360 }; + context.BeginErrorCapture(); + yield return Photo.TakePNGPhoto(camera, resolution, transparent, bytes => png = bytes); + yield return context.Step(2); + var errors = context.EndErrorCapture(); + + var path = context.OutputPath($"{nameof(Scenario_RenderingAndStability)}.{label}.png"); + if (png != null) File.WriteAllBytes(path, png); + + //PNGのシグネチャとIHDRの幅を検証する(真っ黒でも生成はされるので、形式と寸法だけ見る) + var validSignature = png != null && png.Length > 8 + && png[0] == 0x89 && png[1] == (byte)'P' && png[2] == (byte)'N' && png[3] == (byte)'G'; + var width = png != null && png.Length > 20 + ? (png[16] << 24) | (png[17] << 16) | (png[18] << 8) | png[19] + : 0; + + result.CheckThat($"写真撮影({label})", + validSignature && width == resolution.width && errors.Count == 0, + $"PNGが正しく生成されていません(bytes={png?.Length ?? 0} signature={validSignature} width={width} " + + $"errors={errors.Count})"); + } + + private static IEnumerator CheckSpringBones(VMCTestContext context, VMCTestResult result, ExternalReceiverForVMC receiver) + { + var vrm10Instance = context.CurrentModel.GetComponent(); + var springBones = CollectSpringBones(context.CurrentModel, vrm10Instance); + + if (springBones.Count == 0) + { + Debug.Log("[VMCTest] このモデルにはスプリングボーンがありません。検査をスキップします"); + yield break; + } + Debug.Log($"[VMCTest] スプリングボーン {springBones.Count} 本を検査します"); + + var before = springBones.Select(d => d.localRotation).ToList(); + + //大きく動かして揺れを起こす + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + yield return context.Step(5); + var during = springBones.Select(d => d.localRotation).ToList(); + + //十分に時間を置いて落ち着かせる + yield return context.Step(180); + var settled = springBones.Select(d => d.localRotation).ToList(); + + var moved = MaxAngle(before, during); + result.CheckThat("スプリングボーンが動くこと", + moved > 0.5f, + $"アバターを大きく動かしてもスプリングボーンが揺れていません(最大 {moved:F3}度)"); + + //発散(NaN/無限大)していないこと + var broken = springBones.Where(d => + float.IsNaN(d.localRotation.x) || float.IsInfinity(d.localRotation.x) || + float.IsNaN(d.localPosition.x) || float.IsInfinity(d.localPosition.x) || + d.localPosition.magnitude > 1000f).ToList(); + result.CheckThat("スプリングボーンが発散しないこと", + broken.Count == 0, + $"スプリングボーンの値が壊れています({broken.Count}本 例: {broken.FirstOrDefault()?.name})"); + + //静止後は落ち着いていること(揺れ続けない) + yield return context.Step(30); + var afterSettle = springBones.Select(d => d.localRotation).ToList(); + var residual = MaxAngle(settled, afterSettle); + result.CheckThat("スプリングボーンが収束すること", + residual < 1.0f, + $"静止しているのにスプリングボーンが揺れ続けています(最大 {residual:F3}度/30フレーム)"); + + Debug.Log($"[VMCTest] スプリングボーン: 揺れ幅 最大{moved:F2}度 / 収束後の残留 {residual:F3}度"); + } + + private static IEnumerator MeasureFrameTime(VMCTestContext context, VMCTestResult result) + { + const int samples = 120; + //最初の数フレームは読み込み直後で不安定なので捨てる + yield return context.Step(10); + + var start = Time.realtimeSinceStartup; + yield return context.Step(samples); + var elapsed = Time.realtimeSinceStartup - start; + var perFrameMs = elapsed / samples * 1000f; + + Debug.Log($"[VMCTest] 1フレームの処理時間: 平均 {perFrameMs:F2} ms ({samples}フレーム測定)"); + + result.CheckThat("処理時間", + perFrameMs < FrameBudgetMs, + $"1フレームの処理時間が {perFrameMs:F2} ms で上限 {FrameBudgetMs} ms を超えています"); + } + + private static IEnumerator CheckReloadLeak(VMCTestContext context, VMCTestResult result) + { + var vrmPath = context.Config.GetModelPath(context.ModelKey); + + //まず1回入れ替えて、初回だけ生成されるものを含めない状態にする + yield return context.SwitchModel(vrmPath); + yield return context.Step(10); + yield return UnloadAndCollect(context); + + var baselineObjects = CountSceneObjects(); + var baselineMemory = System.GC.GetTotalMemory(false); + + for (int i = 0; i < ReloadCount; i++) + { + yield return context.SwitchModel(vrmPath); + yield return context.Step(5); + } + yield return context.Step(10); + yield return UnloadAndCollect(context); + + var afterObjects = CountSceneObjects(); + var afterMemory = System.GC.GetTotalMemory(false); + var objectGrowth = afterObjects - baselineObjects; + var memoryGrowthMb = (afterMemory - baselineMemory) / 1024f / 1024f; + + Debug.Log($"[VMCTest] {ReloadCount}回入れ替え後: GameObject {baselineObjects} -> {afterObjects} " + + $"(+{objectGrowth}) / マネージドメモリ +{memoryGrowthMb:F1} MB"); + + //1回の入れ替えにつき数個までの増加は許容(遅延破棄やキャッシュのため)。 + //リークしていれば入れ替え回数に比例して増える + var allowedGrowth = ReloadCount * 5; + result.CheckThat("モデル入れ替えのリーク", + objectGrowth < allowedGrowth, + $"アバターを{ReloadCount}回入れ替えたらシーン上のGameObjectが {objectGrowth} 個増えました" + + $"(許容 {allowedGrowth} 個未満)。破棄漏れの可能性があります"); + + //読み込み直後もアバターが正常であること + result.CheckThat("入れ替え後のモデル", + context.CurrentModel != null && context.CurrentModel.GetComponent() != null, + "繰り返し入れ替えた後にアバターが壊れています"); + } + + private static IEnumerator UnloadAndCollect(VMCTestContext context) + { + var unload = Resources.UnloadUnusedAssets(); + while (unload.isDone == false) yield return null; + System.GC.Collect(); + System.GC.WaitForPendingFinalizers(); + System.GC.Collect(); + yield return context.Step(2); + } + + private static int CountSceneObjects() + { + //シーンに属する(=非アセットの)GameObjectだけを数える + return Object.FindObjectsOfType(true).Length; + } + + private static List CollectSpringBones(GameObject model, Vrm10Instance vrm10Instance) + { + var result = new List(); + if (vrm10Instance == null) return result; + + var springBone = vrm10Instance.SpringBone; + if (springBone == null || springBone.Springs == null) return result; + + foreach (var spring in springBone.Springs) + { + if (spring?.Joints == null) continue; + foreach (var joint in spring.Joints) + { + if (joint == null || joint.transform == null) continue; + result.Add(joint.transform); + } + } + return result; + } + + private static float MaxAngle(List a, List b) + { + float max = 0f; + for (int i = 0; i < a.Count && i < b.Count; i++) + { + max = Mathf.Max(max, Quaternion.Angle(a[i], b[i])); + } + return max; + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs.meta b/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs.meta new file mode 100644 index 00000000..4138ce05 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_RenderingAndStability.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d12abf0d5412334d98f3e83c56c2f61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_Robustness.cs b/Assets/Tests/Scenarios/Scenario_Robustness.cs new file mode 100644 index 00000000..ee3771c6 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_Robustness.cs @@ -0,0 +1,174 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// 異常系。壊れた入力を与えても落ちないことを確認する。 + /// + /// VMCProtocolは他アプリからも送られてくるので、 + /// 引数の数や型が仕様と違うメッセージが届くことは普通にある。 + /// 例外を投げると、そのフレーム以降の受信処理が止まったり、 + /// uOSCの受信スレッドごと死んだりする。 + /// + public sealed class Scenario_Robustness : VMCTestScenario + { + public override string Name => "Robustness"; + + public override string Description => "壊れたOSC・不正なファイルを与えても落ちないか"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込みと受信機の用意"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyBlendShape = true; + setting.ApplyLookAt = true; + setting.ApplyCamera = true; + setting.ApplyLight = true; + setting.ApplySetting = true; + setting.ApplyControl = true; + setting.ApplyStatus = true; + setting.ApplyMidi = true; + setting.ApplyControllerInput = true; + setting.ApplyKeyboardInput = true; + }); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + var healthySnapshot = context.Capture("01_before_malformed", includeSent: false); + + //--- 2. 壊れたメッセージを投げ込む --- + context.Log("2. 壊れたOSCメッセージの注入"); + var malformed = BuildMalformedMessages(); + + context.BeginErrorCapture(); + foreach (var message in malformed) + { + context.Inject(receiver, message); + //1件ごとにフレームを進めて、遅延処理まで含めて確認する + yield return context.Step(1); + } + yield return context.Step(10); + var errors = context.EndErrorCapture(); + + result.CheckThat("壊れたOSCで落ちないこと", + errors.Count == 0, + $"壊れたメッセージ{malformed.Count}件で {errors.Count}件のエラー/例外が出ました:\n " + + string.Join("\n ", errors.GetRange(0, Mathf.Min(10, errors.Count)))); + + //--- 3. 壊れた入力の後も正常に動くか --- + context.Log("3. 復帰の確認"); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + + var afterSnapshot = context.Capture("02_after_malformed", includeSent: false); + var delta = VMCTestSnapshot.MaxBoneRotationDelta(healthySnapshot, afterSnapshot, out var movedBone); + result.CheckThat("壊れた入力の後の復帰", + delta > 15f, + $"壊れたメッセージを受けた後、トラッキングが止まっています(最大回転差 {delta:F2}度 / {movedBone ?? "なし"})"); + + //--- 4. 存在しない/壊れたVRMの読み込み --- + context.Log("4. 不正なVRMファイルの読み込み"); + var brokenPath = context.OutputPath($"{Name}.broken.vrm"); + System.IO.File.WriteAllBytes(brokenPath, new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }); + + var modelBefore = context.CurrentModel; + var loadFailed = false; + var loadTask = context.Window.ImportVRM(brokenPath); + while (loadTask.IsCompleted == false) yield return context.Step(1); + if (loadTask.IsFaulted) loadFailed = true; + yield return context.Step(10); + + //壊れたVRMは読み込めなくてよいが、アプリが壊れてはいけない + result.CheckThat("壊れたVRMで落ちないこと", + context.CurrentModel != null, + $"壊れたVRMを読み込もうとしてモデルが失われました(例外={loadFailed})"); + + result.CheckThat("壊れたVRMで元のモデルが残ること", + context.CurrentModel == modelBefore, + "壊れたVRMの読み込みで、元のアバターが破棄されてしまいました"); + + //--- 5. 存在しないパスの設定ファイル読み込み --- + context.Log("5. 存在しない設定ファイルの読み込み"); + context.BeginErrorCapture(); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Config", context.OutputPath("does_not_exist.json"))); + yield return context.Step(10); + var configErrors = context.EndErrorCapture(); + + result.CheckThat("存在しない設定ファイルで落ちないこと", + configErrors.Count == 0, + $"存在しない設定ファイルの指定で {configErrors.Count}件のエラーが出ました: " + + string.Join(" / ", configErrors.GetRange(0, Mathf.Min(5, configErrors.Count)))); + } + + /// + /// 仕様から外れたメッセージ。引数不足・型違い・想定外の値・未知のアドレス。 + /// + private static List BuildMalformedMessages() + { + var longString = new string('X', 4096); + return new List + { + //引数がまったく無い + new uOSC.Message("/VMC/Ext/Hmd/Pos"), + new uOSC.Message("/VMC/Ext/Bone/Pos"), + new uOSC.Message("/VMC/Ext/Blend/Val"), + new uOSC.Message("/VMC/Ext/Cam"), + new uOSC.Message("/VMC/Ext/Set/Eye"), + new uOSC.Message("/VMC/Ext/Set/Period"), + new uOSC.Message("/VMC/Ext/Set/Res"), + new uOSC.Message("/VMC/Ext/Light"), + new uOSC.Message("/VMC/Ext/Con"), + new uOSC.Message("/VMC/Ext/Key"), + new uOSC.Message("/VMC/Ext/Midi/CC/Val"), + new uOSC.Message("/VMC/Ext/OK"), + new uOSC.Message("/VMC/Ext/Root/Pos"), + new uOSC.Message("/VMC/Ext/Set/Calib/Exec"), + + //引数が足りない + new uOSC.Message("/VMC/Ext/Hmd/Pos", "name", 1.0f), + new uOSC.Message("/VMC/Ext/Bone/Pos", "Head", 0f, 0f), + new uOSC.Message("/VMC/Ext/Cam", "Camera", 0f, 0f, 0f), + new uOSC.Message("/VMC/Ext/Set/Period", 1, 1), + new uOSC.Message("/VMC/Ext/Light", "Light", 0f), + + //型が違う + new uOSC.Message("/VMC/Ext/Hmd/Pos", 1, 2, 3, 4, 5, 6, 7, 8), + new uOSC.Message("/VMC/Ext/Bone/Pos", 12345, 0f, 0f, 0f, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Blend/Val", 1, "notafloat"), + new uOSC.Message("/VMC/Ext/Set/Period", "a", "b", "c", "d", "e", "f"), + new uOSC.Message("/VMC/Ext/Set/Eye", "on", 0f, 0f, 0f), + new uOSC.Message("/VMC/Ext/Set/Res", 12345), + new uOSC.Message("/VMC/Ext/Set/Calib/Exec", "Ipose"), + + //値が異常 + new uOSC.Message("/VMC/Ext/Bone/Pos", "存在しないボーン名", 0f, 0f, 0f, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Bone/Pos", "", 0f, 0f, 0f, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Bone/Pos", "Head", float.NaN, float.NaN, float.NaN, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Bone/Pos", "Head", float.PositiveInfinity, 0f, 0f, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Hmd/Pos", longString, 0f, 0f, 0f, 0f, 0f, 0f, 1f), + new uOSC.Message("/VMC/Ext/Blend/Val", "存在しない表情", 999f), + new uOSC.Message("/VMC/Ext/Cam", "Camera", 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f), //回転が全ゼロ + new uOSC.Message("/VMC/Ext/Set/Calib/Exec", 99), //未定義のキャリブ種別 + new uOSC.Message("/VMC/Ext/Set/Period", -1, -1, -1, -1, -1, -1), + + //未知のアドレス + new uOSC.Message("/VMC/Ext/UnknownAddress", 1, 2f, "three"), + new uOSC.Message("/NotVMC/Something", 1), + new uOSC.Message(""), + }; + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_Robustness.cs.meta b/Assets/Tests/Scenarios/Scenario_Robustness.cs.meta new file mode 100644 index 00000000..5c7252ae --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_Robustness.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ac4e9cfb15d2b14da2b4d2d6e8b19e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs b/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs new file mode 100644 index 00000000..fbb640d8 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs @@ -0,0 +1,142 @@ +using sh_akira; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// 旧バージョンの設定ファイルからのマイグレーション。 + /// + /// ControlWPFWindow.ApplySettings に IsSettingVersionBefore による移行処理があり、 + /// ここが壊れると「アップデートしたら設定が消える/壊れる」という形でユーザー環境だけで発覚する。 + /// 手動では絶対に回帰確認しない部分なので自動化する。 + /// + public sealed class Scenario_SettingsMigration : VMCTestScenario + { + public override string Name => "SettingsMigration"; + + public override string Description => "旧バージョンの設定ファイルの読み込みと移行"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + //--- 2. v0.48とv0.56の両方の移行対象になる設定ファイルを作る --- + //v0.48: 表情名の大文字小文字を正しい表記へ補正する + //v0.56: VMCProtocolReceiverSettingsList を ExternalMotionReceiver*List から生成する + context.Log("2. 旧バージョン(v0.47)の設定ファイルを作成"); + //実際の保存形式に合わせる(IsSettingVersionBefore は "v" を除去して解釈する)。 + //v0.48とv0.56の両方の移行を通すため、どちらより前のバージョンにする + Settings.Current.AAA_SavedVersion = "v0.47"; + Settings.Current.VMCProtocolReceiverSettingsList = new List(); + Settings.Current.ExternalMotionReceiverPortList = new List { 39540, 39541 }; + Settings.Current.ExternalMotionReceiverDelayMsList = new List { 0, 120 }; + Settings.Current.ExternalMotionReceiverEnableList = new List { true, false }; + + //v0.48より前の移行(表情名の大文字小文字補正)も同時に確認する + Settings.Current.KeyActions = new List + { + new KeyAction + { + Name = "VMCTestFace", + KeyConfigs = new List(), + FaceAction = true, + //v0.48より前は表情名が大文字で保存されていた。当時はVRM0.x形式なので "JOY" 等。 + //移行時に FaceController.GetCaseSensitiveKeyName で正しい表記へ直される。 + //VRM1.0名(HAPPY)からも引けることを併せて確認する + FaceNames = new List { "JOY", "BLINK_L", "HAPPY" }, + FaceStrength = new List { 1f, 1f, 1f }, + HandAngles = new List(), + }, + }; + + var oldSettingsPath = context.OutputPath($"{Name}.v047.json"); + File.WriteAllText(oldSettingsPath, + Json.Serializer.ToReadable(Json.Serializer.Serialize(Settings.Current))); + + //保存したファイルはSaveSettings経由ではないのでバージョンはv0.47のまま + result.CheckThat("旧設定ファイルの作成", + File.Exists(oldSettingsPath), + $"旧バージョンの設定ファイルを作成できませんでした({oldSettingsPath})"); + + //--- 3. 読み込ませて移行を走らせる --- + context.Log("3. 読み込みと移行"); + var previousModel = context.CurrentModel; + context.Window.LoadSettings(oldSettingsPath); + yield return context.WaitUntil( + () => context.CurrentModel != null && context.CurrentModel != previousModel, + 600, "設定読み込みによるモデルの読み直し"); + yield return context.Step(30); + //ApplySettingsはasync voidで、モデル読み込みの後に移行処理が走る。完了を待ってから判定する + yield return context.WaitUntilOrTimeout( + () => Settings.Current.VMCProtocolReceiverSettingsList != null + && Settings.Current.VMCProtocolReceiverSettingsList.Count >= 2, 600); + + //--- 4. v0.56の移行: 受信機リストが作られているか --- + context.Log("4. 移行結果の確認"); + var receiverSettings = Settings.Current.VMCProtocolReceiverSettingsList; + result.CheckThat("v0.56移行(受信機リスト)", + receiverSettings != null && receiverSettings.Count == 2 + && receiverSettings[0].Port == 39540 && receiverSettings[0].Enable + && receiverSettings[1].Port == 39541 && receiverSettings[1].Enable == false + && receiverSettings[1].DelayMs == 120, + "旧形式の受信機設定がVMCProtocolReceiverSettingsListへ移行されていません: " + + (receiverSettings == null ? "null" : + string.Join(", ", receiverSettings.Select(d => $"[port={d.Port} enable={d.Enable} delay={d.DelayMs}]")))); + + //移行で作られた受信機は、ボーン適用が全てオフになっているのが正しい + //(旧バージョンはトラッカー受信のみだったため) + result.CheckThat("v0.56移行(ボーン適用の既定)", + receiverSettings != null && receiverSettings.Count > 0 + && receiverSettings[0].ApplyHead == false && receiverSettings[0].ApplyLeftArm == false, + "移行で作られた受信機のボーン適用が有効になっています。旧バージョンの挙動と変わってしまいます"); + + //--- 5. v0.48の移行: 表情名の大文字小文字 --- + var keyAction = Settings.Current.KeyActions?.FirstOrDefault(d => d.Name == "VMCTestFace"); + result.CheckThat("v0.48移行(表情名の大小文字)", + keyAction != null && keyAction.FaceNames != null + && keyAction.FaceNames.Contains("Joy") //VRM0.x名 + && keyAction.FaceNames.Contains("Blink_L") //VRM0.x名(アンダースコア入り) + && keyAction.FaceNames.Contains("happy"), //VRM1.0名 + "大文字で保存された表情名が正しい表記に補正されていません: " + + (keyAction?.FaceNames == null ? "null" : string.Join(", ", keyAction.FaceNames))); + + //--- 6. 移行後に保存し直すと、現在のバージョンで安定するか --- + context.Log("5. 移行後の再保存と再読み込み"); + var migratedPath = context.OutputPath($"{Name}.migrated.json"); + context.Window.Test_SaveSettings(migratedPath); + yield return context.Step(2); + + var reloaded = Json.Serializer.Deserialize(File.ReadAllText(migratedPath)); + result.CheckThat("移行後の再保存", + reloaded.VMCProtocolReceiverSettingsList != null + && reloaded.VMCProtocolReceiverSettingsList.Count == 2 + && string.IsNullOrEmpty(reloaded.AAA_SavedVersion) == false + && reloaded.AAA_SavedVersion != "v0.47", + "移行後に保存し直したファイルが正しくありません" + + $"(version={reloaded.AAA_SavedVersion} receivers={reloaded.VMCProtocolReceiverSettingsList?.Count.ToString() ?? "null"})"); + + //二重移行が起きないこと(既に移行済みのファイルを読んでも受信機が増えない) + var previousModel2 = context.CurrentModel; + context.Window.LoadSettings(migratedPath); + yield return context.WaitUntil( + () => context.CurrentModel != null && context.CurrentModel != previousModel2, + 600, "移行後ファイルの読み込み"); + yield return context.Step(30); + + result.CheckThat("二重移行の防止", + Settings.Current.VMCProtocolReceiverSettingsList != null + && Settings.Current.VMCProtocolReceiverSettingsList.Count == 2, + "移行済みのファイルを読み直すと受信機が増えています" + + $"({Settings.Current.VMCProtocolReceiverSettingsList?.Count.ToString() ?? "null"}個)"); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs.meta b/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs.meta new file mode 100644 index 00000000..54c602d8 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_SettingsMigration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1d0988b6d32fcd4b9a9b82d7988b3e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs b/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs new file mode 100644 index 00000000..cf5a555e --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs @@ -0,0 +1,156 @@ +using sh_akira; +using System.Collections; +using System.IO; +using UnityEngine; +using UnityMemoryMappedFile; +using Valve.VR; + +namespace VMC.Tests +{ + /// + /// 設定の保存と再読み込み。 + /// + /// 設定を変更 → 保存 → ファイルから読み直し + /// → 全項目が往復すること + /// → 読み直した後もアバターとキャリブレーションが復元されること + /// + /// 設定項目の追加時に [OptionalField] の付け忘れや初期化漏れで + /// 値が失われるのを検出することが目的。 + /// + public sealed class Scenario_SettingsSaveLoad : VMCTestScenario + { + public override string Name => "SettingsSaveLoad"; + + public override string Description => "設定の保存→再読み込みで全項目とキャリブレーションが復元されるか"; + + //色は適用時にガンマ/リニア変換を通って最下位ビットが揺れるため、桁落ちしない範囲で許容する + private const float FloatTolerance = 1e-4f; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + //--- 2. キャリブレーションまで済ませて、保存すべき状態を作る --- + context.Log("2. トラッカー受信とキャリブレーション"); + var receiver = context.CreateReceiver(setting => setting.ApplyTracker = true); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + yield return context.Calibrate(PipeCommands.CalibrateType.Ipose); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + + //--- 3. いろいろな型の設定を既定値から変える --- + context.Log("3. 設定の変更"); + Settings.Current.ShowCameraGrid = true; + Settings.Current.LeftHandTrackerOffsetToBottom = 0.035f; + Settings.Current.WristRotationFix_UpperArmWeight = 321; + Settings.Current.MotionRecord_Fps = 24; + Settings.Current.CameraFOV = 42f; + //LipSyncGainは適用時に[1,256]へクランプされる(Settingsの既定値0は範囲外)。 + //有効な値を入れておかないと「保存0 → 再読み込み後1」になり往復比較のノイズになる。 + Settings.Current.LipSyncGain = 4f; + Settings.Current.ExternalMotionSenderPort = 39501; + Settings.Current.ExternalMotionSenderAddress = "127.0.0.1"; + Settings.Current.ExternalMotionSenderOptionString = "VMCTest_Option"; + Settings.Current.LightColor = new Color(0.25f, 0.5f, 0.75f, 1f); + Settings.Current.BackgroundColor = new Color(0.1f, 0.2f, 0.3f, 1f); + Settings.Current.EnableAutoCalibrationOnModelLoad = true; + Settings.Current.Head = System.Tuple.Create(ETrackedDeviceClass.HMD, VMCTestTrackerRig.Hmd); + //ウインドウ関連はエディタでは無効なので既定のままにしておく + Settings.Current.HideBorder = false; + Settings.Current.IsTransparent = false; + + var snapshotBeforeSave = context.Capture("01_before_save", includeSent: false); + + //--- 4. 保存 --- + context.Log("4. 設定の保存"); + var settingsPath = context.OutputPath($"{Name}.{context.ModelKey}.settings.json"); + context.Window.Test_SaveSettings(settingsPath); + yield return context.Step(2); + + result.CheckThat("設定ファイルの書き出し", + File.Exists(settingsPath) && new FileInfo(settingsPath).Length > 100, + $"設定ファイルが書き出されていません({settingsPath})"); + if (File.Exists(settingsPath) == false) yield break; + + //SaveSettingsがAAA_SavedVersionを書き換えるので、保存後の状態を基準にする + var expected = Settings.Current; + + //--- 5. ファイルからの復元(シリアライズの往復) --- + context.Log("5. シリアライズ往復の確認"); + var deserialized = Json.Serializer.Deserialize(File.ReadAllText(settingsPath)); + + var serializeDifferences = VMCTestObjectComparer.Compare(expected, deserialized, FloatTolerance); + result.CheckThat("設定のシリアライズ往復", + serializeDifferences.Count == 0, + $"保存して読み直すと値が変わる項目があります({serializeDifferences.Count}件): " + + string.Join(" / ", serializeDifferences)); + + //LastCalibrationSnapshotは自動再キャリブレーションの要なので個別に確認する + result.CheckThat("キャリブレーション記録の保存", + deserialized.LastCalibrationSnapshot != null && + deserialized.LastCalibrationSnapshot.Poses != null && + deserialized.LastCalibrationSnapshot.Poses.Count >= 6, + "キャリブレーション時のトラッカー姿勢が設定ファイルに保存されていません" + + $"(poses={deserialized.LastCalibrationSnapshot?.Poses?.Count.ToString() ?? "null"})"); + + //--- 6. アプリとして読み直す --- + context.Log("6. 設定ファイルの再読み込み"); + var previousModel = context.CurrentModel; + context.Window.LoadSettings(settingsPath); + + //LoadSettings -> ApplySettings は async void でVRMを読み直すため、完了を待つ + yield return context.WaitUntil( + () => context.CurrentModel != null && context.CurrentModel != previousModel, + 600, "設定再読み込みによるモデルの読み直し"); + yield return context.Step(20); + + //ApplySettingsはasync voidで、モデル読み込み後もLipSync等の適用が続く。 + //比較が落ち着くまで待ってから判定する(待っても一致しなければ本物の不一致) + yield return context.WaitUntilOrTimeout( + () => VMCTestObjectComparer.Compare(deserialized, Settings.Current, FloatTolerance).Count == 0, 300); + + //LoadSettingsはSettings.Currentを差し替えるので、保存時の内容(ファイル)を基準に比較する + var reloadDifferences = VMCTestObjectComparer.Compare(deserialized, Settings.Current, FloatTolerance); + result.CheckThat("再読み込み後の設定", + reloadDifferences.Count == 0, + $"再読み込み後のSettingsが保存内容と違います({reloadDifferences.Count}件): " + + string.Join(" / ", reloadDifferences)); + + result.CheckThat("再読み込み後のモデル", + context.CurrentModel != null && context.CurrentModel.GetComponent() != null, + "設定の再読み込み後にアバターが読み込まれていません"); + + //--- 7. 再読み込み後もトラッカーで動くか --- + //(EnableAutoCalibrationOnModelLoad=trueなので自動再キャリブレーションが走るはず) + context.Log("7. 再読み込み後の自動再キャリブレーション"); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitUntil( + () => IKManager.Instance.CalibrationState == CalibrationState.Calibrated, + 600, "自動再キャリブレーションの完了"); + yield return context.Step(20); + + var reloadedIpose = context.Capture("02_after_reload_ipose", includeSent: false); + result.CheckSnapshot(context, reloadedIpose); + + context.InjectTrackerRig(receiver, VMCTestTrackerRig.TPose); + yield return context.Step(20); + var reloadedTpose = context.Capture("03_after_reload_tpose", includeSent: false); + result.CheckSnapshot(context, reloadedTpose); + + var delta = VMCTestSnapshot.MaxBoneRotationDelta(reloadedIpose, reloadedTpose, out var movedBone); + result.CheckThat("再読み込み後の追従", + delta > 15f, + $"設定を読み直した後にアバターがトラッカーへ追従していません(最大回転差 {delta:F2}度 / {movedBone ?? "なし"})"); + + //保存前と保存後で同じ姿勢が再現されているか(キャリブレーション記録が効いているか) + var reproduce = VMCTestSnapshot.MaxBoneRotationDelta(snapshotBeforeSave, reloadedIpose, out var worstBone); + result.CheckThat("キャリブレーションの再現", + reproduce < 5f, + $"再読み込み後のIポーズが保存前と違います(最大回転差 {reproduce:F2}度 @ {worstBone})"); + } + + } +} diff --git a/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs.meta b/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs.meta new file mode 100644 index 00000000..55622ec6 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_SettingsSaveLoad.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24fb5ae4043d7a54db8712538294ab82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs b/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs new file mode 100644 index 00000000..9395e95c --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// VMCProtocolのボーン受信 → 送信の同値性。 + /// + /// 既知のボーン姿勢を /VMC/Ext/Bone/Pos で送り込み、 + /// アバターに適用された結果が同じ値で送信されて出てくるかを確認する。 + /// (VMCを2台数珠つなぎにしたときに姿勢が変質しないことの確認) + /// + public sealed class Scenario_VMCProtocolBoneRoundTrip : VMCTestScenario + { + public override string Name => "VMCProtocolBoneRoundTrip"; + + public override string Description => "ボーンをVMCProtocolで受信し、同じ値が送信されるか"; + + /// 送り込むボーンの回転(ローカル回転をこの角度だけ回す) + private static readonly (HumanBodyBones bone, Vector3 euler)[] Perturbations = + { + (HumanBodyBones.Head, new Vector3(10f, 20f, 0f)), + (HumanBodyBones.Spine, new Vector3(5f, 0f, 8f)), + (HumanBodyBones.LeftUpperArm, new Vector3(0f, 0f, 35f)), + (HumanBodyBones.RightUpperArm, new Vector3(0f, 0f, -35f)), + (HumanBodyBones.LeftLowerArm, new Vector3(0f, 25f, 0f)), + (HumanBodyBones.LeftIndexProximal, new Vector3(0f, 0f, 20f)), + (HumanBodyBones.RightHand, new Vector3(0f, 15f, 0f)), + (HumanBodyBones.LeftUpperLeg, new Vector3(12f, 0f, 0f)), + }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + //--- 2. ボーン受信用の受信機を作る --- + context.Log("2. ボーン受信用の受信機を作成"); + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = false; //トラッカーは使わない(ボーン受信のみを見る) + setting.ApplyBlendShape = false; + setting.ApplyLookAt = false; + setting.FixHandBone = false; //手首の補正を入れると1:1で戻らなくなるため切る + setting.UseBonePosition = false; //位置は受信しない(VMCProtocolの通常運用と同じ) + setting.IgnoreDefaultBone = false; //送った値がそのまま反映されるようにする + }); + + //--- 3. 既知の姿勢を送り込む --- + context.Log("3. ボーン姿勢の送り込み"); + var animator = context.CurrentModel.GetComponent(); + //VMCProtocolが送受信するのはオリジナル(非正規化)ボーンなので、 + //送り込む姿勢もそこから作る(animator.GetBoneTransformはControlRigの正規化ボーン) + var humanoid = context.CurrentModel.GetComponent()?.Humanoid; + var sentRotations = new Dictionary(); + var messages = new List(); + + var rootTransform = animator.transform; + messages.Add(VMCTestOscBuilder.Root(rootTransform.localPosition, rootTransform.localRotation)); + + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + var boneTransform = humanoid != null ? humanoid.GetBoneTransform(bone) : animator.GetBoneTransform(bone); + if (boneTransform == null) continue; + + var rotation = boneTransform.localRotation; + foreach (var perturbation in Perturbations) + { + if (perturbation.bone == bone) + { + rotation = rotation * Quaternion.Euler(perturbation.euler); + break; + } + } + + sentRotations[bone.ToString()] = rotation; + messages.Add(VMCTestOscBuilder.Bone(bone.ToString(), boneTransform.localPosition, rotation)); + } + + context.Inject(receiver, messages); + yield return context.Step(10); + + //--- 4. 受信した姿勢がアバターに反映されているか --- + context.Log("4. 受信結果の確認"); + var receivedSnapshot = context.Capture("01_received", includeSent: false); + result.CheckSnapshot(context, receivedSnapshot); + + var receiveDifferences = new List(); + float maxReceiveError = 0f; + foreach (var perturbation in Perturbations) + { + var name = perturbation.bone.ToString(); + if (sentRotations.TryGetValue(name, out var expected) == false) continue; + var actual = receivedSnapshot.GetProtocolBone(name); + if (actual == null) + { + receiveDifferences.Add($"{name} がアバターに存在しません"); + continue; + } + var angle = Quaternion.Angle(expected, actual.Rotation); + maxReceiveError = Mathf.Max(maxReceiveError, angle); + if (angle > context.Config.RotationToleranceDegrees) + { + receiveDifferences.Add($"{name} {angle:F2}度ずれ"); + } + } + + result.CheckThat("ボーンの受信", + receiveDifferences.Count == 0, + $"送り込んだボーン姿勢がアバターに反映されていません(最大 {maxReceiveError:F2}度): {string.Join(", ", receiveDifferences)}"); + + //--- 5. 同じ値が送信されて出てくるか --- + context.Log("5. 送信内容の確認"); + context.EnableSender(); + yield return context.Step(3); + context.ClearSent(); + yield return context.Step(4); + + var sentSnapshot = context.Capture("02_sent", includeSent: true); + result.CheckSnapshot(context, sentSnapshot); + + //(a) 送信内容が現在のアバターの状態と一致していること + var stateDifferences = sentSnapshot.VerifySentBonesMatchState( + context.Config.PositionTolerance, context.Config.RotationToleranceDegrees); + result.CheckThat("送信ボーンと状態の一致", + stateDifferences.Count == 0, + $"送信されたボーン姿勢が実際のアバターと食い違っています: {string.Join(" / ", Head(stateDifferences, 5))}"); + + //(b) 受信した値そのものが送信されて出てくること(受信→送信の同値性) + var roundTripDifferences = new List(); + float maxRoundTripError = 0f; + int compared = 0; + foreach (var message in sentSnapshot.Sent) + { + if (message.Address != "/VMC/Ext/Bone/Pos") continue; + if (message.Args.Count != 8 || message.Args[0].T != "s") continue; + if (sentRotations.TryGetValue(message.Args[0].S, out var expected) == false) continue; + + var actual = new Quaternion(message.Args[4].F, message.Args[5].F, message.Args[6].F, message.Args[7].F); + var angle = Quaternion.Angle(expected, actual); + maxRoundTripError = Mathf.Max(maxRoundTripError, angle); + compared++; + if (angle > context.Config.RotationToleranceDegrees) + { + roundTripDifferences.Add($"{message.Args[0].S} {angle:F2}度"); + } + } + + result.CheckThat("受信と送信の同値性", + compared > 0 && roundTripDifferences.Count == 0, + compared == 0 + ? "送信内容に /VMC/Ext/Bone/Pos が含まれていません" + : $"{compared}本中 {roundTripDifferences.Count}本で受信値と送信値が違います(最大 {maxRoundTripError:F2}度): {string.Join(", ", Head(roundTripDifferences, 8))}"); + + context.DisableSender(); + } + + private static IEnumerable Head(List list, int count) + => list.GetRange(0, Mathf.Min(count, list.Count)); + } +} diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs.meta b/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs.meta new file mode 100644 index 00000000..7e9aeadd --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolBoneRoundTrip.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b88d496ec1d8f1946be424d665485aad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs b/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs new file mode 100644 index 00000000..1c9031f1 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs @@ -0,0 +1,297 @@ +using System.Collections; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// VMCProtocolの制御系メッセージの受信。 + /// ボーン・表情・視線以外(カメラ / ライト / 周期設定 / スルー / 入力 / 状態文字列)を確認する。 + /// + /// 特にカメラは「送信した画角がそのまま受信側に入るか」を、 + /// 送信キャプチャを自分の受信機へ流し込む形で往復検証する。 + /// + public sealed class Scenario_VMCProtocolControlMessages : VMCTestScenario + { + public override string Name => "VMCProtocolControlMessages"; + + public override string Description => "カメラ・ライト・周期設定・スルー・入力の受信"; + + private const float SenderFov = 35f; + private const float LocalFov = 62f; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込みと受信機の用意"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyCamera = true; + setting.ApplyLight = true; + setting.ApplySetting = true; + setting.ApplyControl = true; + setting.ApplyStatus = true; + setting.ApplyMidi = true; + setting.ApplyControllerInput = true; + setting.ApplyKeyboardInput = true; //既定はfalseなので明示的に有効化する + }); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + + context.EnableSender(); + yield return context.Step(3); + + //--- 2. カメラ画角の往復 --- + context.Log("2. カメラ画角の往復"); + var cameraManager = CameraManager.Current; + if (cameraManager == null) + { + throw new System.Exception("CameraManager が見つかりません"); + } + + //カメラは HandTrackerRoot の子で、この親はキャリブレーションで身長比のスケールと + //オフセットを持つ。/VMC/Ext/Cam はこの親から見たローカル座標で送受信する取り決めなので + //(受信側アバターのスケールへ写像するため)、送信と受信で座標系が食い違うと + //親の変換が二重に掛かる/掛からない形でカメラ距離がずれる。 + //あえて非単位の値を入れて、その食い違いを検出できるようにする + var trackerRoot = IKManager.Instance.HandTrackerRoot; + var savedScale = trackerRoot.localScale; + var savedPosition = trackerRoot.position; + trackerRoot.localScale = new Vector3(1.2f, 1.15f, 1.2f); + trackerRoot.position = new Vector3(0.03f, 0.07f, -0.02f); + + //送信側の画角を決める + cameraManager.Test_SetCameraFOV(SenderFov); + yield return context.Step(3); + context.ClearSent(); + yield return context.Step(4); + + var sentCamera = context.SendCapture.Messages.LastOrDefault(d => d.address == "/VMC/Ext/Cam"); + var hasCameraMessage = sentCamera.address == "/VMC/Ext/Cam" && sentCamera.values != null && sentCamera.values.Length == 9; + result.CheckThat("カメラの送信", + hasCameraMessage, + "/VMC/Ext/Cam が送信されていません。受信側VMCは自分の画角を使い続けます"); + if (hasCameraMessage == false) yield break; + + var sentFov = (float)sentCamera.values[8]; + result.CheckThat("送信された画角", + Mathf.Abs(sentFov - SenderFov) < 0.01f, + $"送信された画角が設定値と違います({sentFov:F3} 期待 {SenderFov})"); + + var sentPosition = new Vector3((float)sentCamera.values[1], (float)sentCamera.values[2], (float)sentCamera.values[3]); + var sentRotation = new Quaternion((float)sentCamera.values[4], (float)sentCamera.values[5], (float)sentCamera.values[6], (float)sentCamera.values[7]); + + //送信直前のカメラ姿勢。往復後にここへ戻ってくるのが正しい + var beforePosition = cameraManager.ControlCamera.transform.position; + var beforeRotation = cameraManager.ControlCamera.transform.rotation; + var beforeLocalPosition = cameraManager.ControlCamera.transform.localPosition; + var beforeLocalRotation = cameraManager.ControlCamera.transform.localRotation; + + //送信値がローカル座標であること(受信側の適用と同じ座標系か) + var sentLocalError = Vector3.Distance(sentPosition, beforeLocalPosition); + result.CheckThat("送信されたカメラ位置の座標系", + sentLocalError < 0.001f, + $"送信されたカメラ位置がローカル座標になっていません" + + $"(送信 {sentPosition} / ローカル {beforeLocalPosition} / ワールド {beforePosition})。" + + $"受信側は localPosition として適用するため、送信もローカル座標で揃える必要があります"); + + result.CheckThat("送信されたカメラ回転の座標系", + Quaternion.Angle(sentRotation, beforeLocalRotation) < 0.1f, + $"送信されたカメラ回転がローカル回転になっていません" + + $"(誤差 {Quaternion.Angle(sentRotation, beforeLocalRotation):F3}度)"); + + //テスト自体が意味を持つ条件の確認。ワールドとローカルが同じ値なら + //座標系の食い違いは検出できず、以降のチェックは素通りしてしまう + result.CheckThat("カメラ座標系テストの前提", + Vector3.Distance(beforePosition, beforeLocalPosition) > 0.01f, + $"HandTrackerRootの変換が効いておらず、ワールドとローカルが同値です" + + $"({beforePosition} / {beforeLocalPosition})。座標系の食い違いを検出できません"); + + //受信側を別の画角・別の位置にしてから、送信内容を流し込む + cameraManager.Test_SetCameraFOV(LocalFov); + cameraManager.FreeCamera.transform.position = beforePosition + new Vector3(1.5f, 0.8f, -1.2f); + yield return context.Step(3); + context.Inject(receiver, sentCamera); + yield return context.Step(5); + + var appliedFov = cameraManager.ControlCamera.fieldOfView; + result.CheckThat("受信した画角の反映", + Mathf.Abs(appliedFov - SenderFov) < 0.01f, + $"受信した画角がカメラに反映されていません(実際 {appliedFov:F3} / 受信値 {sentFov:F3} / 受信前 {LocalFov})"); + + //VMC同士の往復ではカメラが送信直前と同じ場所に戻ること。 + //ずれる場合、送信と受信で座標系が食い違っていて + //HandTrackerRootのスケール・オフセットが二重に掛かっている(または一度も掛かっていない) + var appliedPosition = cameraManager.ControlCamera.transform.position; + var appliedRotation = cameraManager.ControlCamera.transform.rotation; + var positionError = Vector3.Distance(beforePosition, appliedPosition); + var rotationError = Quaternion.Angle(beforeRotation, appliedRotation); + + result.CheckThat("受信したカメラ位置の反映", + positionError < 0.001f, + $"往復後のカメラ位置がずれています(誤差 {positionError:F4}m / 送信前 {beforePosition} → 実際 {appliedPosition})。" + + $"HandTrackerRoot(scale={trackerRoot.localScale} pos={trackerRoot.position})の変換が二重に掛かっていないか確認してください"); + + result.CheckThat("受信したカメラ回転の反映", + rotationError < 0.1f, + $"往復後のカメラ回転がずれています(誤差 {rotationError:F3}度)"); + + trackerRoot.localScale = savedScale; + trackerRoot.position = savedPosition; + yield return context.Step(2); + + //受信した画角が勝手に戻らないか(受信側のカメラ制御が上書きし返さないこと) + yield return context.Step(30); + result.CheckThat("受信した画角の維持", + Mathf.Abs(cameraManager.ControlCamera.fieldOfView - SenderFov) < 0.01f, + $"受信した画角が維持されていません({cameraManager.ControlCamera.fieldOfView:F3} / 受信値 {sentFov:F3})"); + + //受信した画角はSettings.CameraFOVには入らない(設計上の仕様)。 + //送信側が毎フレーム送るので実害は無いが、受信側のコントロールパネルの表示は自分の値のままになる。 + Debug.Log($"[VMCTest] 受信後のSettings.CameraFOV = {Settings.Current.CameraFOV:F3}" + + $"(受信値 {sentFov:F3} / カメラ実値 {cameraManager.ControlCamera.fieldOfView:F3})"); + + //--- 3. ライトの受信 --- + context.Log("3. ライトの受信"); + var lightPosition = new Vector3(1.5f, 2.5f, -3.5f); + var lightRotation = Quaternion.Euler(30f, 40f, 50f); + var lightColor = new Color(0.2f, 0.4f, 0.6f, 1f); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Light", "Light", + lightPosition.x, lightPosition.y, lightPosition.z, + lightRotation.x, lightRotation.y, lightRotation.z, lightRotation.w, + lightColor.r, lightColor.g, lightColor.b, lightColor.a)); + yield return context.Step(3); + + var light = context.Window.MainDirectionalLight; + var lightTransform = context.Window.MainDirectionalLightTransform; + result.CheckThat("ライトの受信", + Vector3.Distance(lightTransform.position, lightPosition) < 0.001f + && Quaternion.Angle(lightTransform.rotation, lightRotation) < 0.1f + && Mathf.Abs(light.color.r - lightColor.r) < 0.01f + && Mathf.Abs(light.color.g - lightColor.g) < 0.01f + && Mathf.Abs(light.color.b - lightColor.b) < 0.01f, + $"ライトが反映されていません(pos {lightTransform.position} / color {light.color})"); + + //--- 4. 送信周期設定の受信 --- + context.Log("4. 送信周期設定の受信"); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Period", 2, 3, 4, 5, 6, 7)); + yield return context.Step(2); + + var sender = context.Sender; + result.CheckThat("送信周期設定の受信", + sender.periodStatus == 2 && sender.periodRoot == 3 && sender.periodBone == 4 + && sender.periodBlendShape == 5 && sender.periodCamera == 6 && sender.periodDevices == 7, + $"送信周期が反映されていません(status={sender.periodStatus} root={sender.periodRoot} bone={sender.periodBone} " + + $"blend={sender.periodBlendShape} cam={sender.periodCamera} dev={sender.periodDevices})"); + + //元に戻す(以降の送信が止まらないように) + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Period", 1, 1, 1, 1, 1, 1)); + yield return context.Step(2); + + //--- 5. スルー転送 --- + context.Log("5. スルー転送の確認"); + context.ClearSent(); + context.Inject(receiver, new uOSC.Message("/VMC/Thru/VMCTest", "hello", 42)); + yield return context.Step(3); + + var forwarded = context.SendCapture.Messages.FirstOrDefault(d => d.address == "/VMC/Thru/VMCTest"); + result.CheckThat("スルー転送", + forwarded.address == "/VMC/Thru/VMCTest" + && forwarded.values != null && forwarded.values.Length == 2 + && (string)forwarded.values[0] == "hello" && (int)forwarded.values[1] == 42, + "/VMC/Thru/* が転送されていません"); + + //--- 6. 状態文字列の受信 --- + context.Log("6. 状態文字列の受信"); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Res", "VMCTestStatus")); + yield return context.Step(3); + result.CheckThat("状態文字列の受信", + receiver.statusString == "VMCTestStatus", + $"状態文字列が反映されていません(\"{receiver.statusString}\")"); + + //--- 7. 情報要求で低頻度情報が即時送信されるか --- + context.Log("7. 情報要求の受信"); + context.ClearSent(); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Req")); + yield return context.Step(2); + result.CheckThat("情報要求の受信", + context.SendCapture.Messages.Any(d => d.address == "/VMC/Ext/Setting/Color"), + "/VMC/Ext/Set/Req を受けても低頻度情報が即時送信されていません"); + + //--- 8. 入力の受信 --- + context.Log("8. 入力の受信"); + //MIDIの受信は MidiCCWrapper.Update() で通知されるため、MIDIが有効(GameObjectがアクティブ)である必要がある + Settings.Current.MidiEnable = true; + context.Window.midiCCWrapper.gameObject.SetActive(true); + yield return context.Step(2); + + OVRKeyEventArgs receivedController = null; + KeyboardEventArgs receivedKey = null; + var receivedKnob = -1; + var receivedKnobValue = 0f; + + System.EventHandler onController = (s, e) => receivedController = e; + System.EventHandler onKey = (s, e) => receivedKey = e; + System.Action onKnob = (no, value) => { receivedKnob = no; receivedKnobValue = value; }; + + SteamVR2Input.Instance.KeyDownEvent += onController; + KeyboardAction.KeyDownEvent += onKey; + context.Window.midiCCWrapper.knobUpdateFloatDelegate += onKnob; + try + { + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Con", 1, "VMCTestButton", 1, 0, 0, 0.1f, 0.2f, 0.3f)); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Key", 1, "A", 65)); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Midi/CC/Val", 3, 0.75f)); + yield return context.Step(3); + } + finally + { + SteamVR2Input.Instance.KeyDownEvent -= onController; + KeyboardAction.KeyDownEvent -= onKey; + context.Window.midiCCWrapper.knobUpdateFloatDelegate -= onKnob; + } + + result.CheckThat("コントローラ入力の受信", + receivedController != null && receivedController.Name == "VMCTestButton" && receivedController.IsLeft, + $"/VMC/Ext/Con が反映されていません({receivedController?.Name ?? "受信なし"})"); + + result.CheckThat("キーボード入力の受信", + receivedKey != null && receivedKey.KeyCode == 65, + $"/VMC/Ext/Key が反映されていません({receivedKey?.KeyCode.ToString() ?? "受信なし"})"); + + result.CheckThat("MIDI入力の受信", + receivedKnob == 3 && Mathf.Abs(receivedKnobValue - 0.75f) < 0.01f, + $"/VMC/Ext/Midi/CC/Val が反映されていません(knob={receivedKnob} value={receivedKnobValue:F3})"); + + //--- 9. リモートキャリブレーション --- + context.Log("9. リモートキャリブレーションの受信"); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Calib/Ready")); + yield return context.Step(5); + result.CheckThat("キャリブレーション準備の受信", + IKManager.Instance.CalibrationState == CalibrationState.WaitingForCalibrating, + $"/VMC/Ext/Set/Calib/Ready でキャリブレーション待機に入りません(state={IKManager.Instance.CalibrationState})"); + + //仕様の mode は PipeCommands.CalibrateType の値。0 = 通常(Default) + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Calib/Exec", (int)PipeCommands.CalibrateType.Default)); + //受信側は Invoke("EndCalibrate", 2f) で完了させるため、実時間で2秒ぶん待つ + yield return context.WaitUntilOrTimeout( + () => IKManager.Instance.CalibrationState == CalibrationState.Calibrated, 900); + + result.CheckThat("キャリブレーション実行の受信", + IKManager.Instance.CalibrationState == CalibrationState.Calibrated + && IKManager.Instance.LastCalibrateType == PipeCommands.CalibrateType.Default, + $"/VMC/Ext/Set/Calib/Exec でキャリブレーションが完了しません" + + $"(state={IKManager.Instance.CalibrationState} type={IKManager.Instance.LastCalibrateType})"); + + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.Step(20); + result.CheckSnapshot(context, context.Capture("01_after_remote_calibration", includeSent: false)); + + context.DisableSender(); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs.meta b/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs.meta new file mode 100644 index 00000000..156198b5 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolControlMessages.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ae7eaabf5a19054eb8ff3b640316865 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs b/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs new file mode 100644 index 00000000..44829b32 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs @@ -0,0 +1,131 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; + +namespace VMC.Tests +{ + /// + /// VMCProtocolの送信網羅。 + /// ExternalSenderが送りうる全アドレスが実際に送信されているかを確認する。 + /// + /// ボーンや表情のように「値が正しいか」は他のシナリオで見ているので、 + /// ここは「そもそも送られているか」だけを見る。 + /// (送信されていないメッセージは、受信側VMCが自分の値を使い続けるので気付きにくい) + /// + public sealed class Scenario_VMCProtocolSendCoverage : VMCTestScenario + { + public override string Name => "VMCProtocolSendCoverage"; + + public override string Description => "ExternalSenderが送りうる全アドレスが実際に送信されるか"; + + //モデルとトラッカーがあれば毎フレーム送られるもの + private static readonly string[] PerFrameAddresses = + { + "/VMC/Ext/OK", + "/VMC/Ext/T", + "/VMC/Ext/Root/Pos", + "/VMC/Ext/Bone/Pos", + "/VMC/Ext/Blend/Val", + "/VMC/Ext/Blend/Apply", + "/VMC/Ext/Cam", + "/VMC/Ext/Hmd/Pos", + "/VMC/Ext/Hmd/Pos/Local", + "/VMC/Ext/Con/Pos", + "/VMC/Ext/Con/Pos/Local", + "/VMC/Ext/Tra/Pos", + "/VMC/Ext/Tra/Pos/Local", + }; + + //低頻度(1秒間隔 / 要求時に即時)で送られるもの + private static readonly string[] LowRateAddresses = + { + "/VMC/Ext/Rcv", + "/VMC/Ext/Light", + "/VMC/Ext/Setting/Color", + "/VMC/Ext/Setting/Win", + "/VMC/Ext/Config", + "/VMC/Ext/Opt", + "/VMC/Ext/VRM", + }; + + //入力イベントの発生時に送られるもの + private static readonly string[] InputAddresses = + { + "/VMC/Ext/Con", + "/VMC/Ext/Key", + "/VMC/Ext/Midi/Note", + "/VMC/Ext/Midi/CC/Val", + "/VMC/Ext/Midi/CC/Bit", + }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + var vrmPath = context.Config.GetModelPath(context.ModelKey); + + context.Log("1. VRM読み込みとトラッカー受信"); + context.ResetSettings(); + yield return context.LoadModel(vrmPath); + + var receiver = context.CreateReceiver(setting => setting.ApplyTracker = true); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + + //--- 2. 毎フレーム送信の網羅 --- + //カメラは意図的に触らない。ここで /VMC/Ext/Cam が出ないなら、 + //送信側がカメラを掴めていない(Start()の実行順に依存する不具合) + context.Log("2. 毎フレーム送信の確認"); + context.EnableSender(); + yield return context.Step(3); + context.ClearSent(); + yield return context.Step(6); + + CheckAddresses(context, result, "毎フレーム送信", PerFrameAddresses); + + //--- 3. 低頻度送信の網羅 --- + context.Log("3. 低頻度送信の確認"); + //VRMのメタ情報を通知する(/VMC/Ext/VRM の送信条件) + var metaTask = context.Window.LoadVRMMetaAsync(vrmPath); + yield return context.Await(metaTask); + context.Window.VRMmetaLoadedAction?.Invoke(metaTask.Result); + context.Sender.optionString = "VMCTest_Option"; + yield return context.Step(2); + + context.ClearSent(); + context.Sender.SendPerLowRate(); //即時送信を要求 + yield return context.Step(2); + + CheckAddresses(context, result, "低頻度送信", LowRateAddresses); + + //--- 4. 入力イベント送信の網羅 --- + context.Log("4. 入力イベント送信の確認"); + context.ClearSent(); + + SteamVR2Input.Instance.KeyDownEvent?.Invoke(this, + new OVRKeyEventArgs("VMCTestButton", new Vector3(0.1f, 0.2f, 0.3f), true, false, false)); + KeyboardAction.KeyDownEvent?.Invoke(this, new KeyboardEventArgs(65)); + context.Window.midiCCWrapper.noteOnDelegateProxy?.Invoke(MidiChannel.Ch1, 60, 0.8f); + context.Window.midiCCWrapper.knobUpdateFloatDelegate?.Invoke(3, 0.5f); + context.Window.midiCCWrapper.knobUpdateBoolDelegate?.Invoke(4, true); + yield return context.Step(3); + + CheckAddresses(context, result, "入力イベント送信", InputAddresses); + + context.DisableSender(); + } + + private static void CheckAddresses(VMCTestContext context, VMCTestResult result, string label, string[] expected) + { + var actual = new HashSet(context.SendCapture.Messages.Select(d => d.address)); + var missing = expected.Where(d => actual.Contains(d) == false).ToList(); + + Debug.Log($"[VMCTest] {label}: 送信された {actual.Count} 種類 / 期待 {expected.Length} 種類\n" + + $" 実際: {string.Join(", ", actual.OrderBy(d => d, System.StringComparer.Ordinal))}"); + + result.CheckThat($"{label}の網羅", + missing.Count == 0, + $"送信されていないアドレスがあります: {string.Join(", ", missing)}"); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs.meta b/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs.meta new file mode 100644 index 00000000..462937dd --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolSendCoverage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20586df25d289a74b916ba996d834fb5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs b/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs new file mode 100644 index 00000000..95f8aaa4 --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs @@ -0,0 +1,239 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityMemoryMappedFile; +using UniVRM10; + +namespace VMC.Tests +{ + /// + /// VMCProtocol仕様(protocol.vmc.info)への準拠。 + /// + /// このアプリはVMCProtocolのリファレンス実装なので、 + /// 仕様書に書かれた引数の数・既定値・オプションの扱いを機械的に検証する。 + /// + public sealed class Scenario_VMCProtocolSpecCompliance : VMCTestScenario + { + public override string Name => "VMCProtocolSpecCompliance"; + + public override string Description => "仕様書どおりの引数・既定値・オプションになっているか"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 }; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var receiver = context.CreateReceiver(setting => + { + setting.ApplyTracker = true; + setting.ApplyBlendShape = true; + setting.ApplyControl = true; + }); + context.InjectTrackerRig(receiver, VMCTestTrackerRig.IPose); + yield return context.WaitTrackingWarmup(); + + //--- 2. 既定値が仕様どおりか --- + context.Log("2. 既定値の確認"); + result.CheckThat("既定はオリジナルボーン送信", + Settings.Current.ExternalMotionSenderUseNormalizedBone == false, + "正規化(ControlRig)ボーンの送信は仕様上「既定で無効のオプション」ですが、既定で有効になっています"); + + result.CheckThat("既定はVRM0.x名のみ送信", + Settings.Current.ExternalMotionSenderSendVRM1Expression == false, + "VRM1.0形式の表情送信はオプションですが、既定で有効になっています"); + + //--- 3. 引数の数(V2.7準拠) --- + context.Log("3. 送信メッセージの引数"); + context.EnableSender(); + yield return context.Step(3); + context.ClearSent(); + //低頻度情報も出させる + var metaTask = context.Window.LoadVRMMetaAsync(context.Config.GetModelPath(context.ModelKey)); + yield return context.Await(metaTask); + context.Window.VRMmetaLoadedAction?.Invoke(metaTask.Result); + yield return context.Step(4); + + CheckArgumentCount(context, result, "/VMC/Ext/OK", 4, + "V2.7で (int)tracking status が追加されています"); + CheckArgumentCount(context, result, "/VMC/Ext/Rcv", 3, + "V2.7で (string)IP Address が追加されています"); + CheckArgumentCount(context, result, "/VMC/Ext/VRM", 3, + "V2.7で (string)Hash が追加されています"); + CheckArgumentCount(context, result, "/VMC/Ext/Root/Pos", 14, + "v2.1でスケールとオフセットが追加されています"); + CheckArgumentCount(context, result, "/VMC/Ext/Cam", 9, ""); + + //VRMハッシュが実際に計算されていること + var vrmMessage = context.SendCapture.Messages.LastOrDefault(d => d.address == "/VMC/Ext/VRM"); + var hash = vrmMessage.values != null && vrmMessage.values.Length >= 3 ? vrmMessage.values[2] as string : null; + result.CheckThat("VRMハッシュ", + string.IsNullOrEmpty(hash) == false && hash.Length == 64, + $"/VMC/Ext/VRM のHashが正しく計算されていません(\"{hash}\")"); + + //--- 4. 送信ボーンがオリジナル(非正規化)であること --- + context.Log("4. 送信ボーンの座標系"); + var vrm10Instance = context.CurrentModel.GetComponent(); + var animator = context.CurrentModel.GetComponent(); + var converter = context.Window.BonePostureConverter; + + result.CheckThat("変換器の生成", + converter != null, + "BonePostureConverter がモデル読み込み時に作られていません"); + + if (converter != null) + { + Debug.Log($"[VMCTest] このモデルは正規化済み(変換不要): {converter.IsIdentity}"); + + var mismatches = new List(); + foreach (var message in context.SendCapture.Messages.Where(d => d.address == "/VMC/Ext/Bone/Pos")) + { + if (message.values.Length != 8 || (message.values[0] is string) == false) continue; + if (System.Enum.TryParse((string)message.values[0], out var bone) == false) continue; + var original = vrm10Instance.Humanoid.GetBoneTransform(bone); + if (original == null) continue; + + var sent = new Quaternion((float)message.values[4], (float)message.values[5], + (float)message.values[6], (float)message.values[7]); + if (Quaternion.Angle(sent, original.localRotation) > 0.1f) + { + mismatches.Add($"{bone} {Quaternion.Angle(sent, original.localRotation):F2}度"); + } + } + + result.CheckThat("送信ボーンがオリジナル姿勢", + mismatches.Count == 0, + $"送信しているボーン姿勢が Humanoid.GetBoneTransform(オリジナル)と一致しません" + + $"({mismatches.Count}本): {string.Join(", ", mismatches.Take(6))}"); + } + + //--- 5. 正規化ボーン送信オプション --- + context.Log("5. 正規化ボーン送信オプション"); + Settings.Current.ExternalMotionSenderUseNormalizedBone = true; + yield return context.Step(3); + context.ClearSent(); + yield return context.Step(4); + + var normalizedMismatches = new List(); + foreach (var message in context.SendCapture.Messages.Where(d => d.address == "/VMC/Ext/Bone/Pos")) + { + if (message.values.Length != 8 || (message.values[0] is string) == false) continue; + if (System.Enum.TryParse((string)message.values[0], out var bone) == false) continue; + var normalized = animator.GetBoneTransform(bone); + if (normalized == null) continue; + + var sent = new Quaternion((float)message.values[4], (float)message.values[5], + (float)message.values[6], (float)message.values[7]); + if (Quaternion.Angle(sent, normalized.localRotation) > 0.1f) + { + normalizedMismatches.Add($"{bone} {Quaternion.Angle(sent, normalized.localRotation):F2}度"); + } + } + result.CheckThat("正規化ボーン送信オプション", + normalizedMismatches.Count == 0, + $"オプションを有効にしても正規化ボーンが送られていません({normalizedMismatches.Count}本): " + + string.Join(", ", normalizedMismatches.Take(6))); + + Settings.Current.ExternalMotionSenderUseNormalizedBone = false; + yield return context.Step(3); + + //--- 6. VRM1.0形式の表情送信オプション --- + context.Log("6. VRM1.0形式の表情送信オプション"); + context.Inject(receiver, VMCTestOscBuilder.BlendShapes(new[] + { + new KeyValuePair("Joy", 0.75f), + })); + yield return context.Step(5); + + context.ClearSent(); + yield return context.Step(4); + var vrm0Only = CollectBlendShapeNames(context); + result.CheckThat("既定ではVRM0.x名のみ", + vrm0Only.Contains("Joy") && vrm0Only.Contains("happy") == false, + $"既定でVRM1.0名が送信されています(送信名: {string.Join(", ", vrm0Only.Take(20))})"); + + Settings.Current.ExternalMotionSenderSendVRM1Expression = true; + yield return context.Step(3); + context.ClearSent(); + yield return context.Step(4); + var both = CollectBlendShapeNames(context); + result.CheckThat("オプション有効時はVRM1.0名も送信", + both.Contains("Joy") && both.Contains("happy"), + $"オプションを有効にしてもVRM0.x名とVRM1.0名の両方が送られていません" + + $"(送信名: {string.Join(", ", both.Take(25))})"); + + Settings.Current.ExternalMotionSenderSendVRM1Expression = false; + yield return context.Step(3); + + //--- 7. キャリブレーション番号の一貫性 --- + //仕様: 0=通常, 1=MR通常, 2=MR床補正。PipeCommands.CalibrateType の値と一致している必要がある + context.Log("7. キャリブレーション番号"); + result.CheckThat("キャリブレーション番号の一貫性", + (int)PipeCommands.CalibrateType.Default == 0 + && (int)PipeCommands.CalibrateType.FixedHand == 1 + && (int)PipeCommands.CalibrateType.FixedHandWithGround == 2, + "CalibrateType の値が仕様(0=通常,1=MR通常,2=MR床補正)と一致していません"); + + //--- 8. /VMC/Ext/Set/Shortcut --- + context.Log("8. ショートカット呼び出し"); + Settings.Current.KeyActions = new List + { + new KeyAction + { + Name = "VMCTestShortcut", + KeyConfigs = new List(), + FunctionAction = true, + Function = Functions.ColorGreen, + HandAngles = new List(), + FaceNames = new List(), + FaceStrength = new List(), + LipSyncMaxLevel = 1f, + }, + }; + Settings.Current.BackgroundColor = new Color(0.5f, 0.5f, 0.5f, 1f); + yield return context.Step(2); + + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Shortcut", "VMCTestShortcut")); + yield return context.Step(5); + + var background = Settings.Current.BackgroundColor; + result.CheckThat("/VMC/Ext/Set/Shortcut の受信", + background.g > 0.9f && background.r < 0.1f, + $"/VMC/Ext/Set/Shortcut でショートカットが実行されていません(背景色 {background})"); + + //存在しない名前でも落ちないこと + context.BeginErrorCapture(); + context.Inject(receiver, new uOSC.Message("/VMC/Ext/Set/Shortcut", "NotExistShortcut")); + yield return context.Step(3); + var errors = context.EndErrorCapture(); + result.CheckThat("存在しないショートカット名", + errors.Count == 0, + $"存在しないショートカット名でエラーが出ました({errors.Count}件)"); + + context.DisableSender(); + } + + private static void CheckArgumentCount(VMCTestContext context, VMCTestResult result, string address, int expected, string note) + { + var message = context.SendCapture.Messages.LastOrDefault(d => d.address == address); + var actual = message.address == address && message.values != null ? message.values.Length : -1; + result.CheckThat($"{address} の引数", + actual == expected, + actual < 0 + ? $"{address} が送信されていません" + : $"{address} の引数が {actual} 個です(仕様は {expected} 個)。{note}"); + } + + private static List CollectBlendShapeNames(VMCTestContext context) + { + return context.SendCapture.Messages + .Where(d => d.address == "/VMC/Ext/Blend/Val" && d.values != null && d.values.Length == 2 && d.values[0] is string) + .Select(d => (string)d.values[0]) + .Distinct() + .ToList(); + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs.meta b/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs.meta new file mode 100644 index 00000000..de23daeb --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMCProtocolSpecCompliance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b4b49ec8874372849af54dfccbb43ff9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Scenarios/Scenario_VMTSend.cs b/Assets/Tests/Scenarios/Scenario_VMTSend.cs new file mode 100644 index 00000000..7f59ec8c --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMTSend.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// Virtual Motion Tracker(VMT)への送信。 + /// + /// VMTドライバが無くても、送信内容をフックで捕まえれば + /// 「有効化したら送るか」「無効化したら停止パケットを送るか」 + /// 「トラッカー番号と姿勢が正しいか」を確認できる。 + /// + public sealed class Scenario_VMTSend : VMCTestScenario + { + public override string Name => "VMTSend"; + + public override string Description => "Virtual Motion Trackerへの送信内容"; + + public override IReadOnlyList Models => new[] { VMCTestModels.Vrm0 }; + + private const int TrackerNo = 3; + + public override IEnumerator Run(VMCTestContext context, VMCTestResult result) + { + context.Log("1. VRM読み込み"); + context.ResetSettings(); + yield return context.LoadModel(context.Config.GetModelPath(context.ModelKey)); + + var vmt = context.Window.vmtClient; + if (vmt == null) + { + result.CheckThat("VMTClientの参照", false, "ControlWPFWindow.vmtClient が設定されていません"); + yield break; + } + + var captured = new List<(string Address, object[] Values)>(); + Action hook = (address, values) => captured.Add((address, values)); + VMTClient.SendHook += hook; + + try + { + //--- 2. 無効の間は何も送らない --- + context.Log("2. 無効時は送信しないこと"); + vmt.SetEnable(false); + yield return context.Step(5); + captured.Clear(); + yield return context.Step(10); + + result.CheckThat("VMT無効時", + captured.Count == 0, + $"VMTが無効なのに {captured.Count} 件送信されています: " + + string.Join(", ", captured.Select(d => d.Address).Distinct())); + + //--- 3. 有効にすると毎フレーム送る --- + context.Log("3. 有効時の送信"); + vmt.SetNo(TrackerNo); + vmt.SetEnable(true); + yield return context.Step(3); + captured.Clear(); + yield return context.Step(5); + + var roomMessages = captured.Where(d => d.Address == "/VMT/Room/Unity").ToList(); + result.CheckThat("VMT有効時の送信", + roomMessages.Count > 0, + "VMTを有効にしても /VMT/Room/Unity が送信されていません"); + + if (roomMessages.Count > 0) + { + var values = roomMessages.Last().Values; + result.CheckThat("VMTの引数", + values.Length == 10 + && values[0] is int no && no == TrackerNo + && values[1] is int enable && enable == 1, + $"VMTの引数が想定と違います(数={values.Length} " + + $"no={(values.Length > 0 ? values[0] : null)} enable={(values.Length > 1 ? values[1] : null)} " + + $"期待 no={TrackerNo} enable=1)"); + + //送っている姿勢がControlCameraのローカル姿勢と一致するか + var camera = CameraManager.Current.ControlCamera.transform; + if (values.Length == 10) + { + var sentPosition = new Vector3((float)values[3], (float)values[4], (float)values[5]); + var sentRotation = new Quaternion((float)values[6], (float)values[7], (float)values[8], (float)values[9]); + result.CheckThat("VMTの姿勢", + Vector3.Distance(sentPosition, camera.localPosition) < 0.01f + && Quaternion.Angle(sentRotation, camera.localRotation) < 1f, + $"VMTに送っている姿勢がカメラと一致しません(送信 {sentPosition} / カメラ {camera.localPosition})"); + } + } + + //--- 4. 無効にすると停止パケットを1回送る --- + context.Log("4. 無効化時の停止パケット"); + captured.Clear(); + vmt.SetEnable(false); + yield return context.Step(5); + + var disableMessages = captured.Where(d => d.Address == "/VMT/Room/Unity").ToList(); + result.CheckThat("VMT無効化の通知", + disableMessages.Count == 1 + && disableMessages[0].Values.Length == 10 + && disableMessages[0].Values[1] is int off && off == 0, + $"無効化したときに enable=0 の停止パケットが1回だけ送られていません" + + $"({disableMessages.Count}件)"); + + //無効化後は送信が止まること + captured.Clear(); + yield return context.Step(10); + result.CheckThat("VMT無効化後の停止", + captured.Count == 0, + $"無効化した後も {captured.Count} 件送信され続けています"); + + //--- 5. トラッカー番号の変更 --- + context.Log("5. トラッカー番号の変更"); + vmt.SetNo(7); + vmt.SetEnable(true); + yield return context.Step(3); + captured.Clear(); + yield return context.Step(5); + + var renumbered = captured.Where(d => d.Address == "/VMT/Room/Unity").ToList(); + result.CheckThat("トラッカー番号の変更", + renumbered.Count > 0 && renumbered.Last().Values[0] is int newNo && newNo == 7, + $"トラッカー番号の変更が反映されていません(" + + $"{(renumbered.Count > 0 ? renumbered.Last().Values[0] : null)} 期待 7)"); + + vmt.SetEnable(false); + yield return context.Step(3); + } + finally + { + VMTClient.SendHook -= hook; + vmt.SetEnable(false); + } + } + } +} diff --git a/Assets/Tests/Scenarios/Scenario_VMTSend.cs.meta b/Assets/Tests/Scenarios/Scenario_VMTSend.cs.meta new file mode 100644 index 00000000..f1c4eada --- /dev/null +++ b/Assets/Tests/Scenarios/Scenario_VMTSend.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffaa920541535214a8b92224a8daafc5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestConfig.cs b/Assets/Tests/VMCTestConfig.cs new file mode 100644 index 00000000..a460be63 --- /dev/null +++ b/Assets/Tests/VMCTestConfig.cs @@ -0,0 +1,144 @@ +using System; +using System.IO; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// 自動テストの設定。 + /// テスト用VRMはライセンスの都合でリポジトリに含めないため、 + /// プロジェクト直下の TestData/vmctest.json でパスを指定する。 + /// + [Serializable] + public class VMCTestConfig + { + /// プロジェクト直下からの相対パス、または絶対パス + public string Vrm0Path = ""; + public string Vrm10Path = ""; + + /// 期待値(ゴールデン)の保存先 + public string GoldenDirectory = "TestData/Golden"; + + /// 実行結果・差分レポートの出力先 + public string OutputDirectory = "TestData/Results"; + + /// trueにすると比較せずゴールデンを上書きする + public bool UpdateGolden = false; + + //比較の許容誤差 + public float PositionTolerance = 0.001f; + public float RotationToleranceDegrees = 0.2f; + public float WeightTolerance = 0.002f; + + //モーション(VRMA/BVH)の往復はマッスル空間とglTFの量子化を通るため、通常より緩い許容誤差を使う + public float MotionRotationToleranceDegrees = 2.0f; + public float MotionWeightTolerance = 0.02f; + + /// + /// VRMAファイルの往復で、末端(頭・手・足)の向きに許す誤差。 + /// ここが一致していれば見た目の姿勢は保たれている。厳しく見る。 + /// + public float VrmaEndEffectorToleranceDegrees = 1.0f; + + /// + /// VRMAファイルの往復で、ボーン単位のローカル回転に許す誤差。 + /// Humanoidのリターゲットは腕のツイストを上腕と手の間で配分し直すため + /// (VRMのアバターとVRMAから作ったアバターでtwist設定が異なる)、 + /// 末端の向きが完全に一致していてもボーン単位では数度ずれる。実測で最大5度程度。 + /// + public float VrmaFileToleranceDegrees = 8.0f; + + /// + /// 記録→再生の総合誤差(実際のアバターの姿勢 vs 再生後)の許容誤差。 + /// Unity Humanoidのマッスル空間は可動範囲が限られており、 + /// 特に腕を下ろした姿勢の肩・上腕は元の回転をそのまま表現できないため大きめに取る。 + /// + public float MotionRetargetToleranceDegrees = 15.0f; + + /// + /// 指(特に親指)はマッスル空間の表現力がさらに低いため、別枠でさらに緩くする。 + /// + public float MotionFingerToleranceDegrees = 25.0f; + + /// まばたき等の乱数を固定するシード + public int Seed = 12345; + + /// 1フレームあたりの進行時間(Time.captureDeltaTimeに設定して決定論化する) + public float FixedDeltaTime = 1f / 60f; + + /// 1シナリオあたりの実時間の上限。超えたら中断して失敗にする(ハング対策) + public float TimeoutSeconds = 180f; + + public const string DefaultConfigPath = "TestData/vmctest.json"; + + public static string ProjectRoot => Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + + /// プロジェクト直下を基準に絶対パス化する + public static string ResolvePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) return null; + if (Path.IsPathRooted(path)) return Path.GetFullPath(path); + return Path.GetFullPath(Path.Combine(ProjectRoot, path)); + } + + public string ResolvedGoldenDirectory => ResolvePath(GoldenDirectory); + public string ResolvedOutputDirectory => ResolvePath(OutputDirectory); + + /// + /// モデル種別("vrm0"/"vrm10")からVRMのフルパスを得る。未設定/不存在ならnull。 + /// + public string GetModelPath(string modelKey) + { + var raw = modelKey == VMCTestModels.Vrm10 ? Vrm10Path : Vrm0Path; + var full = ResolvePath(raw); + if (string.IsNullOrEmpty(full) || File.Exists(full) == false) return null; + return full; + } + + public static VMCTestConfig Load(string path = null) + { + var fullPath = ResolvePath(path ?? DefaultConfigPath); + if (File.Exists(fullPath) == false) + { + //初回は雛形を書き出して、パスを埋めてもらう + var template = new VMCTestConfig(); + try + { + template.Save(fullPath); + Debug.Log($"[VMCTest] 設定ファイルの雛形を作成しました。VRMのパスを記入してください: {fullPath}"); + } + catch (Exception ex) + { + Debug.LogWarning($"[VMCTest] 設定ファイルを作成できませんでした: {ex.Message}"); + } + return template; + } + + try + { + return JsonUtility.FromJson(File.ReadAllText(fullPath)) ?? new VMCTestConfig(); + } + catch (Exception ex) + { + Debug.LogError($"[VMCTest] 設定ファイルの読み込みに失敗しました: {fullPath}\n{ex}"); + return new VMCTestConfig(); + } + } + + public void Save(string path) + { + var fullPath = ResolvePath(path); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + File.WriteAllText(fullPath, JsonUtility.ToJson(this, true)); + } + } + + public static class VMCTestModels + { + public const string Vrm0 = "vrm0"; + public const string Vrm10 = "vrm10"; + + /// アバターを必要としないシナリオ用(1回だけ実行される) + public const string None = "none"; + } +} diff --git a/Assets/Tests/VMCTestConfig.cs.meta b/Assets/Tests/VMCTestConfig.cs.meta new file mode 100644 index 00000000..c242f62d --- /dev/null +++ b/Assets/Tests/VMCTestConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33fba98053dbb094d9a1ed339df0198c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestContext.cs b/Assets/Tests/VMCTestContext.cs new file mode 100644 index 00000000..1d34a29b --- /dev/null +++ b/Assets/Tests/VMCTestContext.cs @@ -0,0 +1,615 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using UnityEngine; +using UnityMemoryMappedFile; +using Valve.VR; + +namespace VMC.Tests +{ + /// + /// シナリオから使うアプリ操作ヘルパー。 + /// 実機のVR機器・コントロールパネル(WPF)・ネットワークを使わずに、 + /// 本番と同じコードパスでアバターを動かすための足場を提供する。 + /// + public sealed class VMCTestContext : IDisposable + { + public VMCTestConfig Config { get; } + public ControlWPFWindow Window { get; private set; } + public ExternalSender Sender { get; private set; } + public FaceController FaceController { get; private set; } + public VMCTestSendCapture SendCapture { get; private set; } + + /// 現在実行中のシナリオ名(スナップショットのファイル名に使う) + public string ScenarioName { get; set; } + + /// 現在対象にしているモデル種別(vrm0 / vrm10) + public string ModelKey { get; set; } + + public GameObject CurrentModel => Window != null ? Window.Test_CurrentModel : null; + + public int FrameCount { get; private set; } + + /// 直近に実行した工程。ハングした時にどこで止まったかを示すために使う + public string CurrentStep { get; private set; } = "(未開始)"; + + private readonly List createdReceivers = new List(); + private float originalCaptureDeltaTime; + private float originalFilterStrength; + private int originalTargetFrameRate; + private bool disposed; + + /// 工程の開始を記録する + public void Log(string step) + { + CurrentStep = step; + Debug.Log($"[VMCTest] {ScenarioName}[{ModelKey}] {step}"); + } + + public VMCTestContext(VMCTestConfig config) + { + Config = config; + } + + #region セットアップ + + /// + /// シーン上のオブジェクトを解決し、実行を決定論的にする。 + /// + public bool Initialize() + { + var windowObject = GameObject.Find("ControlWPFWindow"); + if (windowObject == null) + { + Debug.LogError("[VMCTest] ControlWPFWindow がシーンに見つかりません"); + return false; + } + Window = windowObject.GetComponent(); + if (Window == null) + { + Debug.LogError("[VMCTest] ControlWPFWindow コンポーネントが見つかりません"); + return false; + } + + Sender = Window.ExternalMotionSenderObject != null + ? Window.ExternalMotionSenderObject.GetComponent() + : null; + FaceController = Window.faceController; + + //---コントロールパネル(WPF)への送信を無効化する--- + //MemoryMappedFileServerは相手が居なくてもIsConnected=trueになる。 + //その状態でSendCommandを2回呼ぶと、1回目に立てた完了フラグを誰もクリアしないため + // while (senderAccessor.ReadByte(0) == 1) Thread.Sleep(1); + //で永久に待ち続ける。結果、 + // ・await側(ImportVRM等)が二度と返らずテストが進まなくなる + // ・再生停止時のOnApplicationQuitが同期SendCommandを呼ぶためメインスレッドごと固まる + //テストではコントロールパネルを起動しないので、送信自体を無効にしておく。 + //一度無効にしたら再生セッション中は戻さない(戻すと停止時に上記のフリーズが起きるため)。 + if (Window.server != null && Window.server.IsConnected) + { + Window.server.IsConnected = false; + Debug.Log("[VMCTest] コントロールパネルへのパイプ送信を無効化しました(この再生セッション中は戻しません)"); + } + + //---決定論化--- + //Time.deltaTimeを固定してフレーム間の時間を実時間から切り離す + originalCaptureDeltaTime = Time.captureDeltaTime; + Time.captureDeltaTime = Config.FixedDeltaTime; + //DeviceInfo.updateOkTime() が okTime = validFrames / Application.targetFrameRate を計算するため、 + //ここを -1(=制限なし)にすると okTime が負になり、トラッキング復帰の補間係数が常に0になって + //トラッカーの姿勢が最初の値で固定されてしまう。必ず正の値、かつフレーム時間と整合させる。 + originalTargetFrameRate = Application.targetFrameRate; + Application.targetFrameRate = FrameRate; + //まばたきのランダム待ち時間などを固定する + UnityEngine.Random.InitState(Config.Seed); + + //受信トラッカーのローパスフィルタを実質無効化する(収束待ちフレームを不要にし、dt依存を消す) + originalFilterStrength = ExternalReceiverForVMC.filterStrength; + ExternalReceiverForVMC.filterStrength = 100000f; + + //---共通設定ファイルを退避する--- + //SaveSettings / LoadSettings は「起動時に読み込む設定ファイル」を common.json に書き込むため、 + //テストが作った設定ファイルが次回のVMC起動時に読まれてしまう。テスト後に元へ戻す。 + BackupCommonSettings(); + + SendCapture = new VMCTestSendCapture(); + return true; + } + + private string commonSettingsPath; + private string commonSettingsBackup; + private bool commonSettingsExisted; + + private void BackupCommonSettings() + { + try + { + commonSettingsPath = System.IO.Path.GetFullPath( + System.IO.Path.Combine(Application.dataPath, "..", "Settings", "common.json")); + commonSettingsExisted = System.IO.File.Exists(commonSettingsPath); + commonSettingsBackup = commonSettingsExisted ? System.IO.File.ReadAllText(commonSettingsPath) : null; + } + catch (Exception ex) + { + Debug.LogWarning($"[VMCTest] common.json の退避に失敗しました: {ex.Message}"); + commonSettingsPath = null; + } + } + + private void RestoreCommonSettings() + { + if (commonSettingsPath == null) return; + try + { + if (commonSettingsExisted) + { + System.IO.File.WriteAllText(commonSettingsPath, commonSettingsBackup); + } + else if (System.IO.File.Exists(commonSettingsPath)) + { + System.IO.File.Delete(commonSettingsPath); + } + CommonSettings.Load(); + } + catch (Exception ex) + { + Debug.LogWarning($"[VMCTest] common.json の復元に失敗しました: {ex.Message}"); + } + } + + /// + /// Settingsをテスト用の既定値に初期化する。 + /// new Settings() だけでは [OnDeserializing] の初期化が走らず + /// VMCProtocolReceiverSettingsList 等がnullのままになるため、 + /// 設定ファイル読み込み時と同じ初期化メソッドを明示的に呼ぶ。 + /// + public void ResetSettings() + { + var settings = new Settings(); + settings.OnDeserializingMethod(default(StreamingContext)); + Settings.Current = settings; + + //テスト中に自動再キャリブレーションが割り込まないようにする + Settings.Current.EnableAutoCalibrationOnModelLoad = false; + Settings.Current.LastCalibrationSnapshot = null; + //外部機器のUDPポートを掴まないようにする + //(mocopiはプラグインへ移ったので、プラグイン設定領域の方を落とす。 + // mocopiプラグインは設定オブジェクトをまとめて "mocopi/Setting" へ入れるため、 + // enable だけのJSONを置く。残りのメンバーはプラグイン側で既定値が入る) + if (Settings.Current.PluginSettings == null) + { + Settings.Current.PluginSettings = new Dictionary(); + } + Settings.Current.PluginSettings["mocopi/Setting"] = "{\"enable\":false}"; + + if (FaceController != null) + { + //まばたきは時間依存なのでスナップショット対象のテストでは止める + FaceController.EnableBlink = false; + FaceController.StopBlink = true; + //入力源ごとの表情は解除されるまで残り続けるので、シナリオ間で持ち越さないようにする + FaceController.Test_ClearAllMixes(); + } + + //疑似時計を使うシナリオが途中で失敗しても次のシナリオへ持ち越さないようにする + AnimationController.TestTimeProvider = null; + + //設定ファイル読み込み時と同じく、各コンポーネントへ設定を配る。 + //MotionPlayerのVirtualAvatarのApply*フラグはここでしか更新されないため、 + //これを呼ばないとモーション再生でボーンも視線も一切適用されない + //(起動時のSettings.Currentはnew Settings()で全boolがfalseのため)。 + Window.AdditionalSettingAction?.Invoke(null); + } + + #endregion + + #region モデル + + /// VRMを読み込む(VRM0.x / VRM1.0 どちらも同じ経路) + public IEnumerator LoadModel(string vrmPath) + { + var task = Window.ImportVRM(vrmPath); + while (task.IsCompleted == false) + { + yield return null; + } + if (task.IsFaulted) + { + throw new Exception($"VRMの読み込みに失敗しました: {vrmPath}", task.Exception); + } + + //読み込み直後はVRIKの生成やボーンの初期化が走るため数フレーム落ち着かせる + yield return Step(5); + + if (CurrentModel == null) + { + throw new Exception($"VRMを読み込みましたがモデルが生成されていません: {vrmPath}"); + } + } + + /// Settingsを維持したままモデルだけ入れ替える(別アバター読み込みの検証用) + public IEnumerator SwitchModel(string vrmPath) + { + var previousModel = CurrentModel; + yield return LoadModel(vrmPath); + if (CurrentModel == previousModel) + { + throw new Exception("モデルが入れ替わっていません"); + } + } + + #endregion + + #region モーション + + public MotionPlayer MotionPlayer => Window != null ? Window.Test_MotionPlayer : null; + + public MotionRecorder MotionRecorder => Window != null ? Window.Test_MotionRecorder : null; + + /// 結果フォルダ内のパスを作る + public string OutputPath(string fileName) + { + var directory = Config.ResolvedOutputDirectory; + System.IO.Directory.CreateDirectory(directory); + return System.IO.Path.Combine(directory, fileName); + } + + #endregion + + #region エラー捕捉 + + private List capturedErrors; + + /// + /// この区間に出力されたエラー/例外ログを集める。 + /// 「不正な入力を与えても落ちないこと」を検査するのに使う。 + /// + public void BeginErrorCapture() + { + capturedErrors = new List(); + Application.logMessageReceived += OnLogMessage; + } + + public List EndErrorCapture() + { + Application.logMessageReceived -= OnLogMessage; + var errors = capturedErrors ?? new List(); + capturedErrors = null; + return errors; + } + + private void OnLogMessage(string condition, string stackTrace, LogType type) + { + if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert) return; + //テストハーネス自身の失敗報告は対象外 + if (condition != null && condition.StartsWith("[VMCTest]")) return; + capturedErrors?.Add($"{type}: {condition}"); + } + + #endregion + + #region 待機 + + /// Taskの完了を待つ(例外はそのまま投げ直す) + public IEnumerator Await(Task task) + { + while (task.IsCompleted == false) + { + yield return null; + FrameCount++; + } + if (task.IsFaulted) + { + throw task.Exception; + } + } + + /// + /// 条件が成立するまでフレームを進める。成立しなくても例外にしない。 + /// async void の処理(ApplySettings等)の完了を待つのに使う。 + /// 待った後で改めて検査すれば、待ち時間による偽の失敗を避けつつ本物の不一致は検出できる。 + /// + public IEnumerator WaitUntilOrTimeout(Func condition, int maxFrames) + { + for (int i = 0; i < maxFrames; i++) + { + if (condition()) yield break; + yield return null; + FrameCount++; + } + } + + /// 条件が成立するまでフレームを進める。成立しなければ例外 + public IEnumerator WaitUntil(Func condition, int maxFrames, string description) + { + for (int i = 0; i < maxFrames; i++) + { + if (condition()) yield break; + yield return null; + FrameCount++; + } + if (condition() == false) + { + throw new Exception($"{description} が {maxFrames} フレーム以内に成立しませんでした"); + } + } + + #endregion + + #region VMCProtocol 受信 + + /// + /// VMCProtocolの受信機を1つ作る。 + /// UDPソケットは開かず(ポート衝突と到着タイミングのゆらぎを避けるため)、 + /// VMCTestOscInjector から直接メッセージを流し込んで使う。 + /// + public ExternalReceiverForVMC CreateReceiver(Action configure = null) + { + if (Settings.Current.VMCProtocolReceiverSettingsList == null) + { + Settings.Current.VMCProtocolReceiverSettingsList = new List(); + } + + var setting = new VMCProtocolReceiverSettings + { + Enable = false, //UDPを開かせないためfalseで追加し、あとで手動で有効化する + Name = $"VMCTest {Settings.Current.VMCProtocolReceiverSettingsList.Count + 1}", + Port = 39590 + Settings.Current.VMCProtocolReceiverSettingsList.Count, + DelayMs = 0, //遅延バッファは実時間依存なので使わない + ApplyTracker = true, + ApplyBlendShape = true, + ApplyLookAt = true, + }; + configure?.Invoke(setting); + setting.Enable = false; + setting.DelayMs = 0; + + Settings.Current.VMCProtocolReceiverSettingsList.Add(setting); + Window.Test_AddVMCProtocolReceiver(setting); + + var receiver = Window.externalMotionReceivers.LastOrDefault(); + if (receiver == null) + { + throw new Exception("VMCProtocol受信機の作成に失敗しました"); + } + + //ソケットを開かないまま受信処理だけを有効にする + var server = receiver.GetComponent(); + if (server != null) server.enabled = false; + receiver.gameObject.SetActive(true); + + //Enable=falseで追加したためSetSettingが未反映の項目がある。有効な設定を入れ直す。 + setting.Enable = true; + receiver.SetSetting(setting); + + createdReceivers.Add(receiver); + return receiver; + } + + /// + /// トラッキング機器から報告された生のローカル姿勢を取得する。 + /// キャリブレーションでTargetTransformは親子付け替えされるため、 + /// 「注入した値がそのまま届いているか」の確認にはこちらを使う。 + /// + public bool TryGetTrackerPose(string name, out Vector3 position, out Quaternion rotation) + { + position = Vector3.zero; + rotation = Quaternion.identity; + if (TrackingPointManager.Instance == null) return false; + if (TrackingPointManager.Instance.TryGetTrackingPoint(name, out var trackingPoint) == false) return false; + position = trackingPoint.LastLocalPosition; + rotation = trackingPoint.LastLocalRotation; + return true; + } + + /// 注入したトラッカー構成が実際に届いているか(最大位置誤差)を返す。届いていない機器があれば -1 + public float GetTrackerRigError(IEnumerable rig) + { + float max = 0f; + foreach (var entry in rig) + { + if (TryGetTrackerPose(entry.Name, out var position, out _) == false) return -1f; + max = Mathf.Max(max, Vector3.Distance(entry.Position, position)); + } + return max; + } + + /// 受信機の有効/無効を切り替える(他の入力源を止めて切り分けるため) + public void SetReceiverActive(ExternalReceiverForVMC receiver, bool active) + { + if (receiver == null) return; + receiver.gameObject.SetActive(active); + } + + public void Inject(ExternalReceiverForVMC receiver, params uOSC.Message[] messages) + => VMCTestOscInjector.Inject(receiver, messages); + + public void Inject(ExternalReceiverForVMC receiver, IEnumerable messages) + => VMCTestOscInjector.Inject(receiver, messages); + + /// 標準のトラッカー構成(HMD1 + コントローラ2 + トラッカー3)を送る + public void InjectTrackerRig(ExternalReceiverForVMC receiver, IEnumerable rig) + { + foreach (var entry in rig) + { + switch (entry.DeviceClass) + { + case ETrackedDeviceClass.HMD: + Inject(receiver, VMCTestOscBuilder.Hmd(entry.Name, entry.Position, entry.Rotation)); + break; + case ETrackedDeviceClass.Controller: + Inject(receiver, VMCTestOscBuilder.Controller(entry.Name, entry.Position, entry.Rotation)); + break; + default: + Inject(receiver, VMCTestOscBuilder.Tracker(entry.Name, entry.Position, entry.Rotation)); + break; + } + } + } + + #endregion + + #region VMCProtocol 送信 + + /// ExternalSenderを有効にする(送信先は設定しないのでUDPには出ない。フックだけが発火する) + public void EnableSender() + { + if (Window.ExternalMotionSenderObject == null) + { + throw new Exception("ExternalMotionSenderObject がシーンに設定されていません"); + } + Window.ExternalMotionSenderObject.SetActive(true); + } + + public void DisableSender() + { + if (Window.ExternalMotionSenderObject != null) + { + Window.ExternalMotionSenderObject.SetActive(false); + } + } + + #endregion + + #region キャリブレーション + + /// トラッカー姿勢を入力にしてキャリブレーションを実行する + public IEnumerator Calibrate(PipeCommands.CalibrateType calibrateType) + { + IKManager.Instance.ModelCalibrationInitialize(silent: true); + yield return Step(3); + yield return IKManager.Instance.Calibrate(calibrateType); + IKManager.Instance.EndCalibrate(); + yield return Step(3); + + if (IKManager.Instance.CalibrationState != CalibrationState.Calibrated) + { + throw new Exception($"キャリブレーションに失敗しました state={IKManager.Instance.CalibrationState}"); + } + } + + #endregion + + #region フレーム進行 / スナップショット + + public int FrameRate => Mathf.Max(1, Mathf.RoundToInt(1f / Mathf.Max(0.0001f, Config.FixedDeltaTime))); + + public IEnumerator Step(int frames = 1) + { + for (int i = 0; i < frames; i++) + { + yield return null; + FrameCount++; + } + } + + /// + /// トラッカーが「信用できる」状態になるまで待つ。 + /// DeviceInfo は認識直後の1秒間(LEAP_SECONDS)、飛び対策として過去値から徐々に補間するため、 + /// それを過ぎるまで待たないと注入した姿勢がそのまま反映されない。 + /// + public IEnumerator WaitTrackingWarmup() + { + Log("トラッキングのウォームアップ待ち(DeviceInfoの復帰補間 1秒)"); + yield return Step(FrameRate + 15); + } + + /// 現在の状態と、直近のClearSentから送信された内容をスナップショットに取る + public VMCTestSnapshot Capture(string label, bool includeSent = true) + { + var snapshot = VMCTestSnapshot.Capture(ScenarioName, ModelKey, label, FrameCount, CurrentModel); + if (includeSent && SendCapture != null) + { + snapshot.SetSentMessages(SendCapture.Messages); + } + return snapshot; + } + + public void ClearSent() => SendCapture?.Clear(); + + #endregion + + public void Dispose() + { + if (disposed) return; + disposed = true; + + SendCapture?.Dispose(); + SendCapture = null; + + //Destroyは遅延実行なので、先にリストから外す。 + //(残しておくと ExternalSender.externalReceiver が破棄済みオブジェクトを指したままになり、 + // /VMC/Ext/Rcv が送信されなくなる等の形で次のシナリオに影響する) + foreach (var receiver in createdReceivers) + { + if (receiver == null) continue; + Window?.externalMotionReceivers.Remove(receiver); + UnityEngine.Object.DestroyImmediate(receiver.gameObject); + } + createdReceivers.Clear(); + if (Window != null) + { + Window.externalMotionReceivers.RemoveAll(d => d == null); + if (Sender != null) Sender.externalReceiver = Window.externalMotionReceivers.FirstOrDefault(); + } + + RestoreCommonSettings(); + + ExternalReceiverForVMC.filterStrength = originalFilterStrength; + Time.captureDeltaTime = originalCaptureDeltaTime; + Application.targetFrameRate = originalTargetFrameRate; + //パイプ送信は意図的に戻さない(Initializeのコメント参照) + } + } + + /// + /// テスト用のトラッカー配置。身長1.6m程度の人がIポーズで立っている想定。 + /// トラッカーの割り当ては未指定(自動割り当て)にしているので、 + /// 腰=最も高い位置のトラッカー、足=低い方2つ、という本番と同じ推定ロジックを通る。 + /// + public static class VMCTestTrackerRig + { + public struct Entry + { + public string Name; + public ETrackedDeviceClass DeviceClass; + public Vector3 Position; + public Quaternion Rotation; + } + + public const string Hmd = "VMCTEST_HMD"; + public const string LeftController = "VMCTEST_CON_L"; + public const string RightController = "VMCTEST_CON_R"; + public const string WaistTracker = "VMCTEST_TRA_WAIST"; + public const string LeftFootTracker = "VMCTEST_TRA_FOOT_L"; + public const string RightFootTracker = "VMCTEST_TRA_FOOT_R"; + + /// Iポーズ(両手を体の横に下ろした姿勢) + public static IReadOnlyList IPose { get; } = new[] + { + New(Hmd, ETrackedDeviceClass.HMD, new Vector3( 0.00f, 1.60f, 0.00f)), + New(LeftController, ETrackedDeviceClass.Controller, new Vector3(-0.20f, 0.95f, 0.05f)), + New(RightController, ETrackedDeviceClass.Controller, new Vector3( 0.20f, 0.95f, 0.05f)), + New(WaistTracker, ETrackedDeviceClass.GenericTracker, new Vector3( 0.00f, 1.00f, -0.10f)), + New(LeftFootTracker, ETrackedDeviceClass.GenericTracker, new Vector3(-0.10f, 0.10f, 0.00f)), + New(RightFootTracker, ETrackedDeviceClass.GenericTracker, new Vector3( 0.10f, 0.10f, 0.00f)), + }; + + /// 両腕を横に伸ばした姿勢(動きを与えたときの追従確認用) + public static IReadOnlyList TPose { get; } = new[] + { + New(Hmd, ETrackedDeviceClass.HMD, new Vector3( 0.00f, 1.60f, 0.00f)), + New(LeftController, ETrackedDeviceClass.Controller, new Vector3(-0.70f, 1.40f, 0.00f)), + New(RightController, ETrackedDeviceClass.Controller, new Vector3( 0.70f, 1.40f, 0.00f)), + New(WaistTracker, ETrackedDeviceClass.GenericTracker, new Vector3( 0.00f, 1.00f, -0.10f)), + New(LeftFootTracker, ETrackedDeviceClass.GenericTracker, new Vector3(-0.10f, 0.10f, 0.00f)), + New(RightFootTracker, ETrackedDeviceClass.GenericTracker, new Vector3( 0.10f, 0.10f, 0.00f)), + }; + + private static Entry New(string name, ETrackedDeviceClass deviceClass, Vector3 position) + => new Entry { Name = name, DeviceClass = deviceClass, Position = position, Rotation = Quaternion.identity }; + } +} diff --git a/Assets/Tests/VMCTestContext.cs.meta b/Assets/Tests/VMCTestContext.cs.meta new file mode 100644 index 00000000..17e13a08 --- /dev/null +++ b/Assets/Tests/VMCTestContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d6aae976656b84f48bf52e9759da26b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestObjectComparer.cs b/Assets/Tests/VMCTestObjectComparer.cs new file mode 100644 index 00000000..280be5bf --- /dev/null +++ b/Assets/Tests/VMCTestObjectComparer.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// 設定オブジェクトなどを再帰的に比較する。 + /// JSON文字列の比較だと -0 と 0 の表記差や、色空間変換による最下位ビットの揺れで + /// 落ちてしまうため、数値は許容誤差つきで比較し、違いは項目名で報告する。 + /// + public static class VMCTestObjectComparer + { + private const int MaxDepth = 12; + + public static List Compare(object expected, object actual, float tolerance, int maxDifferences = 30) + { + var differences = new List(); + CompareValue("", expected, actual, tolerance, differences, maxDifferences, 0); + return differences; + } + + private static void CompareValue(string path, object expected, object actual, float tolerance, + List differences, int maxDifferences, int depth) + { + if (differences.Count >= maxDifferences) return; + + if (expected == null && actual == null) return; + if (expected == null || actual == null) + { + differences.Add($"{Name(path)}: {Describe(expected)} -> {Describe(actual)}"); + return; + } + + var type = expected.GetType(); + if (type != actual.GetType()) + { + differences.Add($"{Name(path)}: 型が違います {type.Name} -> {actual.GetType().Name}"); + return; + } + + if (type == typeof(float)) + { + if (NearlyEqual((float)expected, (float)actual, tolerance) == false) + { + differences.Add($"{Name(path)}: {(float)expected:R} -> {(float)actual:R}"); + } + return; + } + if (type == typeof(double)) + { + if (NearlyEqual((float)(double)expected, (float)(double)actual, tolerance) == false) + { + differences.Add($"{Name(path)}: {(double)expected:R} -> {(double)actual:R}"); + } + return; + } + if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal)) + { + if (expected.Equals(actual) == false) + { + differences.Add($"{Name(path)}: {expected} -> {actual}"); + } + return; + } + + //Unityの構造体はプロパティに派生値(Quaternion.eulerAngles等)を持つので、成分だけを比べる + if (type == typeof(Vector2)) { CompareFloats(path, new[] { "x", "y" }, ToFloats((Vector2)expected), ToFloats((Vector2)actual), tolerance, differences); return; } + if (type == typeof(Vector3)) { CompareFloats(path, new[] { "x", "y", "z" }, ToFloats((Vector3)expected), ToFloats((Vector3)actual), tolerance, differences); return; } + if (type == typeof(Vector4)) { CompareFloats(path, new[] { "x", "y", "z", "w" }, ToFloats((Vector4)expected), ToFloats((Vector4)actual), tolerance, differences); return; } + if (type == typeof(Quaternion)) { CompareFloats(path, new[] { "x", "y", "z", "w" }, ToFloats((Quaternion)expected), ToFloats((Quaternion)actual), tolerance, differences); return; } + if (type == typeof(Color)) { CompareFloats(path, new[] { "r", "g", "b", "a" }, ToFloats((Color)expected), ToFloats((Color)actual), tolerance, differences); return; } + + if (depth >= MaxDepth) return; + + //Tuple はプロパティ(Item1, Item2, ...)で値を持つ + if (type.IsGenericType && type.FullName != null && type.FullName.StartsWith("System.Tuple`")) + { + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.Name.StartsWith("Item")) + .OrderBy(p => p.Name, StringComparer.Ordinal)) + { + CompareValue($"{path}.{property.Name}", property.GetValue(expected), property.GetValue(actual), + tolerance, differences, maxDifferences, depth + 1); + } + return; + } + + if (expected is IEnumerable expectedEnumerable) + { + var expectedItems = expectedEnumerable.Cast().ToList(); + var actualItems = ((IEnumerable)actual).Cast().ToList(); + if (expectedItems.Count != actualItems.Count) + { + differences.Add($"{Name(path)}: 要素数 {expectedItems.Count} -> {actualItems.Count}"); + return; + } + for (int i = 0; i < expectedItems.Count; i++) + { + CompareValue($"{path}[{i}]", expectedItems[i], actualItems[i], + tolerance, differences, maxDifferences, depth + 1); + } + return; + } + + //Settingsは public フィールド、PipeCommands は public プロパティで値を持つので両方見る。 + //読み取り専用プロパティは派生値のことが多いので対象外にする。 + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance) + .OrderBy(f => f.Name, StringComparer.Ordinal)) + { + if (field.IsStatic) continue; + CompareValue($"{path}.{field.Name}", field.GetValue(expected), field.GetValue(actual), + tolerance, differences, maxDifferences, depth + 1); + } + + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0) + .OrderBy(p => p.Name, StringComparer.Ordinal)) + { + object expectedValue; + object actualValue; + try + { + expectedValue = property.GetValue(expected); + actualValue = property.GetValue(actual); + } + catch (Exception) + { + continue; //取得できないプロパティは比較対象外 + } + CompareValue($"{path}.{property.Name}", expectedValue, actualValue, + tolerance, differences, maxDifferences, depth + 1); + } + } + + private static float[] ToFloats(Vector2 v) => new[] { v.x, v.y }; + private static float[] ToFloats(Vector3 v) => new[] { v.x, v.y, v.z }; + private static float[] ToFloats(Vector4 v) => new[] { v.x, v.y, v.z, v.w }; + private static float[] ToFloats(Quaternion q) => new[] { q.x, q.y, q.z, q.w }; + private static float[] ToFloats(Color c) => new[] { c.r, c.g, c.b, c.a }; + + private static void CompareFloats(string path, string[] names, float[] expected, float[] actual, + float tolerance, List differences) + { + for (int i = 0; i < names.Length; i++) + { + if (NearlyEqual(expected[i], actual[i], tolerance) == false) + { + differences.Add($"{Name(path)}.{names[i]}: {expected[i]:R} -> {actual[i]:R}"); + } + } + } + + private static bool NearlyEqual(float a, float b, float tolerance) + { + if (float.IsNaN(a) && float.IsNaN(b)) return true; + if (a == b) return true; //-0 と 0 もここで一致扱いになる + var scale = Math.Max(1f, Math.Max(Math.Abs(a), Math.Abs(b))); + return Math.Abs(a - b) <= tolerance * scale; + } + + private static string Name(string path) => string.IsNullOrEmpty(path) ? "(ルート)" : path.TrimStart('.'); + + private static string Describe(object value) => value == null ? "null" : value.ToString(); + } +} diff --git a/Assets/Tests/VMCTestObjectComparer.cs.meta b/Assets/Tests/VMCTestObjectComparer.cs.meta new file mode 100644 index 00000000..89891cef --- /dev/null +++ b/Assets/Tests/VMCTestObjectComparer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 633c111973bce5847bfe0ba1c295bfd8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestObjectFiller.cs b/Assets/Tests/VMCTestObjectFiller.cs new file mode 100644 index 00000000..6f806c95 --- /dev/null +++ b/Assets/Tests/VMCTestObjectFiller.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// リフレクションでオブジェクトの全メンバーに「既定値ではない値」を詰める。 + /// シリアライズ往復テストで、既定値のままだと欠落に気付けないため、 + /// 全項目に別々の値を入れてから往復させるのに使う。 + /// + public static class VMCTestObjectFiller + { + private const int MaxDepth = 6; + + public static object CreateFilled(Type type, int seed) + { + return CreateValue(type, ref seed, 0); + } + + private static object CreateValue(Type type, ref int seed, int depth) + { + seed++; + + if (type == typeof(string)) return $"vmctest_{seed}"; + if (type == typeof(bool)) return seed % 2 == 0; + if (type == typeof(int)) return 1000 + seed; + if (type == typeof(uint)) return (uint)(1000 + seed); + if (type == typeof(long)) return 100000L + seed; + if (type == typeof(short)) return (short)(100 + seed); + if (type == typeof(byte)) return (byte)(seed % 200 + 1); + if (type == typeof(float)) return 1.5f + seed * 0.25f; + if (type == typeof(double)) return 2.5d + seed * 0.5d; + if (type == typeof(decimal)) return 3.5m + seed; + if (type == typeof(char)) return (char)('A' + seed % 26); + if (type == typeof(DateTime)) return new DateTime(2020, 1, 1).AddDays(seed); + if (type == typeof(Guid)) return Guid.Empty; + + if (type.IsEnum) + { + var values = Enum.GetValues(type); + if (values.Length == 0) return Activator.CreateInstance(type); + //既定値(先頭)以外を選ぶことで「初期化されていない」との区別を付ける + return values.GetValue(values.Length > 1 ? 1 : 0); + } + + var nullableUnderlying = Nullable.GetUnderlyingType(type); + if (nullableUnderlying != null) + { + return CreateValue(nullableUnderlying, ref seed, depth); + } + + if (type == typeof(Vector2)) return new Vector2(0.1f * seed, 0.2f * seed); + if (type == typeof(Vector3)) return new Vector3(0.1f * seed, 0.2f * seed, 0.3f * seed); + if (type == typeof(Vector4)) return new Vector4(0.1f * seed, 0.2f * seed, 0.3f * seed, 0.4f * seed); + if (type == typeof(Quaternion)) return Quaternion.Euler(10f + seed, 20f + seed, 30f + seed); + if (type == typeof(Color)) return new Color(0.1f, 0.2f, 0.3f, 1f); + + if (depth >= MaxDepth) return null; + + if (type.IsArray) + { + var elementType = type.GetElementType(); + var array = Array.CreateInstance(elementType, 2); + for (int i = 0; i < 2; i++) + { + array.SetValue(CreateValue(elementType, ref seed, depth + 1), i); + } + return array; + } + + if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + var arguments = type.GetGenericArguments(); + + if (definition == typeof(List<>) || definition == typeof(IList<>) || definition == typeof(IEnumerable<>)) + { + var listType = typeof(List<>).MakeGenericType(arguments[0]); + var list = (IList)Activator.CreateInstance(listType); + for (int i = 0; i < 2; i++) + { + list.Add(CreateValue(arguments[0], ref seed, depth + 1)); + } + return list; + } + + if (definition == typeof(Dictionary<,>)) + { + var dictionary = (IDictionary)Activator.CreateInstance(type); + for (int i = 0; i < 2; i++) + { + var key = CreateValue(arguments[0], ref seed, depth + 1); + if (key == null) continue; + dictionary[key] = CreateValue(arguments[1], ref seed, depth + 1); + } + return dictionary; + } + + if (type.FullName != null && type.FullName.StartsWith("System.Tuple`")) + { + var values = new object[arguments.Length]; + for (int i = 0; i < arguments.Length; i++) + { + values[i] = CreateValue(arguments[i], ref seed, depth + 1); + } + return Activator.CreateInstance(type, values); + } + } + + if (type.IsAbstract || type.IsInterface) return null; + + object instance; + try + { + instance = Activator.CreateInstance(type); + } + catch (Exception) + { + return null; + } + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (field.IsInitOnly) continue; + var value = CreateValue(field.FieldType, ref seed, depth + 1); + if (value != null) field.SetValue(instance, value); + } + + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.CanRead == false || property.CanWrite == false) continue; + if (property.GetIndexParameters().Length > 0) continue; + var value = CreateValue(property.PropertyType, ref seed, depth + 1); + if (value != null) property.SetValue(instance, value); + } + + return instance; + } + } +} diff --git a/Assets/Tests/VMCTestObjectFiller.cs.meta b/Assets/Tests/VMCTestObjectFiller.cs.meta new file mode 100644 index 00000000..2b024ab1 --- /dev/null +++ b/Assets/Tests/VMCTestObjectFiller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5eb31c56a9619654784a9f478bfc53f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestOsc.cs b/Assets/Tests/VMCTestOsc.cs new file mode 100644 index 00000000..8f9302f5 --- /dev/null +++ b/Assets/Tests/VMCTestOsc.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// VMCProtocolの「送信側アプリ」が送ってくるOSCメッセージを組み立てる。 + /// 実機のVR機器の代わりにこれを流し込むことで、HMD/コントローラ/トラッカーの動きを再現する。 + /// アドレスと引数の並びは protocol.vmc.info の仕様に合わせている。 + /// + public static class VMCTestOscBuilder + { + public static uOSC.Message Hmd(string serial, Vector3 position, Quaternion rotation) + => Transform("/VMC/Ext/Hmd/Pos", serial, position, rotation); + + public static uOSC.Message Controller(string serial, Vector3 position, Quaternion rotation) + => Transform("/VMC/Ext/Con/Pos", serial, position, rotation); + + public static uOSC.Message Tracker(string serial, Vector3 position, Quaternion rotation) + => Transform("/VMC/Ext/Tra/Pos", serial, position, rotation); + + public static uOSC.Message Bone(string boneName, Vector3 localPosition, Quaternion localRotation) + => Transform("/VMC/Ext/Bone/Pos", boneName, localPosition, localRotation); + + public static uOSC.Message Root(Vector3 position, Quaternion rotation) + => Transform("/VMC/Ext/Root/Pos", "root", position, rotation); + + private static uOSC.Message Transform(string address, string name, Vector3 position, Quaternion rotation) + { + //受信側は values[n] is float で型チェックしているため、必ずfloatとしてボックス化する + return new uOSC.Message(address, name, + position.x, position.y, position.z, + rotation.x, rotation.y, rotation.z, rotation.w); + } + + public static uOSC.Message BlendShapeValue(string name, float value) + => new uOSC.Message("/VMC/Ext/Blend/Val", name, value); + + public static uOSC.Message BlendShapeApply() + => new uOSC.Message("/VMC/Ext/Blend/Apply"); + + /// 外部アイトラッキング。位置は頭ボーンからの相対位置 + public static uOSC.Message Eye(bool enable, Vector3 localPositionFromHead) + => new uOSC.Message("/VMC/Ext/Set/Eye", enable ? 1 : 0, + localPositionFromHead.x, localPositionFromHead.y, localPositionFromHead.z); + + /// 表情の一括送信(Val×n + Apply) + public static IEnumerable BlendShapes(IEnumerable> values) + { + foreach (var pair in values) + { + yield return BlendShapeValue(pair.Key, pair.Value); + } + yield return BlendShapeApply(); + } + } + + /// + /// 組み立てたOSCメッセージをExternalReceiverForVMCへ流し込む。 + /// uOscServerのonDataReceivedをそのまま叩くため、UDPの到着タイミングに左右されず + /// フレーム単位で決定論的に再現できる(実機VR機器もネットワークも不要)。 + /// + public static class VMCTestOscInjector + { + public static void Inject(ExternalReceiverForVMC receiver, uOSC.Message message) + { + var server = receiver.GetComponent(); + if (server == null || server.onDataReceived == null) + { + Debug.LogError("[VMCTest] uOscServerが見つかりません"); + return; + } + server.onDataReceived.Invoke(message); + } + + public static void Inject(ExternalReceiverForVMC receiver, IEnumerable messages) + { + foreach (var message in messages) + { + Inject(receiver, message); + } + } + } + + /// + /// ExternalSenderの送信内容をキャプチャする。 + /// Bundleは実際にUDPへ書き出すのと同じバイト列にシリアライズしてから + /// uOSCのParserで読み戻すため、OSCのエンコード/デコードも含めて検証できる。 + /// + public sealed class VMCTestSendCapture : IDisposable + { + private readonly List messages = new List(); + private readonly uOSC.Parser parser = new uOSC.Parser(); + private bool disposed; + + public IReadOnlyList Messages => messages; + + public VMCTestSendCapture() + { + ExternalSender.SendHook += OnSend; + } + + public void Clear() + { + messages.Clear(); + } + + private void OnSend(object packet) + { + try + { + using (var stream = new MemoryStream()) + { + if (packet is uOSC.Bundle bundle) + { + bundle.Write(stream); + } + else if (packet is uOSC.Message message) + { + message.Write(stream); + } + else + { + return; + } + + var buffer = stream.ToArray(); + if (buffer.Length == 0) return; + int position = 0; + parser.Parse(buffer, ref position, buffer.Length); + } + + while (parser.messageCount > 0) + { + messages.Add(parser.Dequeue()); + } + } + catch (Exception ex) + { + Debug.LogError($"[VMCTest] 送信キャプチャに失敗しました: {ex}"); + } + } + + public void Dispose() + { + if (disposed) return; + disposed = true; + ExternalSender.SendHook -= OnSend; + } + } +} diff --git a/Assets/Tests/VMCTestOsc.cs.meta b/Assets/Tests/VMCTestOsc.cs.meta new file mode 100644 index 00000000..bd3310fb --- /dev/null +++ b/Assets/Tests/VMCTestOsc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc2d4d339dc30374a8ba0d5317cf27b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestRunner.cs b/Assets/Tests/VMCTestRunner.cs new file mode 100644 index 00000000..f9d1bfce --- /dev/null +++ b/Assets/Tests/VMCTestRunner.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace VMC.Tests +{ + /// + /// Editorメニュー / コマンドラインからランナーへ渡す実行要求 + /// + [Serializable] + public class VMCTestRequest + { + public List Scenarios = new List(); + public List Models = new List(); + public bool UpdateGolden; + public string ConfigPath = VMCTestConfig.DefaultConfigPath; + public bool QuitWhenFinished; + + /// Editorメニューから実行要求を渡すための一時ファイル(ランナーが読んだら消す) + public const string RequestFilePath = "Temp/vmctest.request.json"; + + public static string FullRequestFilePath => VMCTestConfig.ResolvePath(RequestFilePath); + + public void Save() + { + var path = FullRequestFilePath; + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllText(path, JsonUtility.ToJson(this, true)); + } + + public static VMCTestRequest ConsumeFile() + { + var path = FullRequestFilePath; + if (File.Exists(path) == false) return null; + try + { + var request = JsonUtility.FromJson(File.ReadAllText(path)); + File.Delete(path); + return request; + } + catch (Exception ex) + { + Debug.LogError($"[VMCTest] 実行要求の読み込みに失敗しました: {ex}"); + return null; + } + } + + /// + /// コマンドライン引数から実行要求を作る。 + /// -vmctest テストを実行する + /// -vmctest-scenarios A,B 実行するシナリオ名(省略時は全部) + /// -vmctest-models vrm0,vrm10 対象モデル(省略時は全部) + /// -vmctest-updategolden 比較せずゴールデンを更新する + /// -vmctest-config 設定ファイルのパス + /// -vmctest-noquit 終了後にアプリを終了しない + /// + public static VMCTestRequest FromCommandLine() + { + var args = Environment.GetCommandLineArgs(); + if (args.Any(d => string.Equals(d, "-vmctest", StringComparison.OrdinalIgnoreCase)) == false) return null; + + var request = new VMCTestRequest { QuitWhenFinished = true }; + + string GetValue(string key) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], key, StringComparison.OrdinalIgnoreCase)) return args[i + 1]; + } + return null; + } + + var scenarios = GetValue("-vmctest-scenarios"); + if (string.IsNullOrWhiteSpace(scenarios) == false) + { + request.Scenarios = scenarios.Split(',').Select(d => d.Trim()).Where(d => d.Length > 0).ToList(); + } + + var models = GetValue("-vmctest-models"); + if (string.IsNullOrWhiteSpace(models) == false) + { + request.Models = models.Split(',').Select(d => d.Trim()).Where(d => d.Length > 0).ToList(); + } + + var configPath = GetValue("-vmctest-config"); + if (string.IsNullOrWhiteSpace(configPath) == false) request.ConfigPath = configPath; + + request.UpdateGolden = args.Any(d => string.Equals(d, "-vmctest-updategolden", StringComparison.OrdinalIgnoreCase)); + if (args.Any(d => string.Equals(d, "-vmctest-noquit", StringComparison.OrdinalIgnoreCase))) request.QuitWhenFinished = false; + + return request; + } + } + + /// + /// シナリオを順番に実行するランナー。 + /// 実機のVR機器もコントロールパネル(WPF)も無しで、本番のシーンをそのまま動かして検証する。 + /// + public class VMCTestRunner : MonoBehaviour + { + public static bool IsRunning { get; private set; } + + private VMCTestRequest request; + + /// + /// シーン読み込み後、実行要求があればランナーを起動する。 + /// (コマンドライン -vmctest / Editorメニューが書いた一時ファイル) + /// + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + private static void AutoStart() + { + var request = VMCTestRequest.FromCommandLine() ?? VMCTestRequest.ConsumeFile(); + if (request == null) return; + Start(request); + } + + public static VMCTestRunner Start(VMCTestRequest request) + { + if (IsRunning) + { + Debug.LogWarning("[VMCTest] すでにテストが実行中です"); + return null; + } + var runnerObject = new GameObject("VMCTestRunner"); + DontDestroyOnLoad(runnerObject); + var runner = runnerObject.AddComponent(); + runner.request = request; + return runner; + } + + /// 登録済みのシナリオ一覧 + public static IReadOnlyList AllScenarios { get; } = new VMCTestScenario[] + { + new Scenario_BasicVMCProtocol(), + new Scenario_VMCProtocolBoneRoundTrip(), + new Scenario_VMCProtocolSendCoverage(), + new Scenario_VMCProtocolSpecCompliance(), + new Scenario_VMCProtocolControlMessages(), + new Scenario_MotionVrmaRoundTrip(), + new Scenario_SettingsSaveLoad(), + new Scenario_ModelSwitch(), + new Scenario_PipeCommandsSerialization(), + new Scenario_SettingsMigration(), + new Scenario_DeviceInfoTracking(), + new Scenario_Robustness(), + new Scenario_KeyActions(), + new Scenario_FaceMixing(), + new Scenario_BlinkFrameDrop(), + new Scenario_MultipleReceivers(), + new Scenario_BvhExport(), + new Scenario_RenderingAndStability(), + new Scenario_FaceHardwareInputs(), + new Scenario_VMTSend(), + }; + + private IEnumerator Start() + { + IsRunning = true; + var results = new List(); + + try + { + //アプリ側の初期化(ControlWPFWindowのAwake/Start、ExternalSenderのStart)を待つ + yield return WaitForApplicationReady(); + + var config = VMCTestConfig.Load(request.ConfigPath); + if (request.UpdateGolden) config.UpdateGolden = true; + + var scenarios = AllScenarios + .Where(d => request.Scenarios == null || request.Scenarios.Count == 0 || request.Scenarios.Contains(d.Name)) + .ToList(); + + if (scenarios.Count == 0) + { + Debug.LogError($"[VMCTest] 実行対象のシナリオがありません: {string.Join(",", request.Scenarios ?? new List())}"); + } + + foreach (var scenario in scenarios) + { + var models = scenario.Models + .Where(d => request.Models == null || request.Models.Count == 0 || request.Models.Contains(d)) + .ToList(); + + foreach (var model in models) + { + yield return RunOne(scenario, model, config, results); + } + } + + WriteReport(config, results); + } + finally + { + IsRunning = false; + } + + if (request.QuitWhenFinished) + { + var failed = results.Any(d => d.Passed == false && d.Skipped == false); + Debug.Log($"[VMCTest] 終了します (exit code {(failed ? 1 : 0)})"); + Application.Quit(failed ? 1 : 0); + } + } + + private static IEnumerator WaitForApplicationReady() + { + //ControlWPFWindowが現れるまで、最大10秒待つ + for (int i = 0; i < 600; i++) + { + if (GameObject.Find("ControlWPFWindow") != null) break; + yield return null; + } + //各コンポーネントのStartが一巡するのを待つ + for (int i = 0; i < 10; i++) yield return null; + } + + private IEnumerator RunOne(VMCTestScenario scenario, string model, VMCTestConfig config, List results) + { + var result = new VMCTestResult { Scenario = scenario.Name, Model = model }; + results.Add(result); + + if (scenario.RequiresModel && config.GetModelPath(model) == null) + { + result.Skipped = true; + result.SkipReason = $"{model} のVRMが見つかりません。{VMCTestConfig.DefaultConfigPath} にパスを設定してください"; + Debug.LogWarning($"[VMCTest] SKIP {result.Title}: {result.SkipReason}"); + yield break; + } + + Debug.Log($"[VMCTest] ---- 開始: {result.Title} ----"); + + var context = new VMCTestContext(config) + { + ScenarioName = scenario.Name, + ModelKey = model, + }; + + if (context.Initialize() == false) + { + result.Error = "テストコンテキストの初期化に失敗しました"; + context.Dispose(); + yield break; + } + + var deadline = Time.realtimeSinceStartup + Mathf.Max(1f, config.TimeoutSeconds); + yield return Drive(scenario.Run(context, result), deadline, () => context.CurrentStep, ex => + { + result.Error = ex.ToString(); + Debug.LogError($"[VMCTest] {result.Title} で中断しました (工程: {context.CurrentStep})\n{ex}"); + }); + + context.Dispose(); + + var verdict = result.Skipped ? "SKIP" : (result.Passed ? "PASS" : "FAIL"); + Debug.Log($"[VMCTest] ---- 終了: {result.Title} : {verdict} ----"); + } + + /// + /// コルーチンを自前で回す。 + /// Unityに任せると入れ子のコルーチン内の例外を捕まえられないため、 + /// IEnumeratorのスタックを自分で管理して全階層の例外を1箇所で受ける。 + /// あわせて実時間の上限を監視し、進まなくなったら中断する + /// (awaitが返らない等でUnityごと固まるのを防ぐ)。 + /// + private static IEnumerator Drive(IEnumerator routine, float deadlineRealtime, Func currentStep, Action onError) + { + var stack = new Stack(); + stack.Push(routine); + + while (stack.Count > 0) + { + if (Time.realtimeSinceStartup > deadlineRealtime) + { + onError(new TimeoutException($"制限時間を超えたため中断しました。止まった工程: {currentStep()}")); + yield break; + } + + var current = stack.Peek(); + bool moved; + object yielded = null; + try + { + moved = current.MoveNext(); + if (moved) yielded = current.Current; + } + catch (Exception ex) + { + onError(ex); + yield break; + } + + if (moved == false) + { + stack.Pop(); + continue; + } + + if (yielded is IEnumerator nested) + { + stack.Push(nested); + continue; + } + + yield return yielded; + } + } + + private static void WriteReport(VMCTestConfig config, IReadOnlyList results) + { + var report = VMCTestReport.Build(results); + Debug.Log(report); + + try + { + var directory = config.ResolvedOutputDirectory; + Directory.CreateDirectory(directory); + File.WriteAllText(Path.Combine(directory, "report.txt"), report); + Debug.Log($"[VMCTest] レポートを書き出しました: {Path.Combine(directory, "report.txt")}"); + } + catch (Exception ex) + { + Debug.LogWarning($"[VMCTest] レポートの書き出しに失敗しました: {ex.Message}"); + } + } + } +} diff --git a/Assets/Tests/VMCTestRunner.cs.meta b/Assets/Tests/VMCTestRunner.cs.meta new file mode 100644 index 00000000..de5fef94 --- /dev/null +++ b/Assets/Tests/VMCTestRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c79579955bd423641952d237e1bf2a61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestScenario.cs b/Assets/Tests/VMCTestScenario.cs new file mode 100644 index 00000000..9c0e5c67 --- /dev/null +++ b/Assets/Tests/VMCTestScenario.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace VMC.Tests +{ + /// 1つの検証項目(スナップショット1件の比較)の結果 + public sealed class VMCTestCheck + { + public string Label; + public bool Passed; + public bool GoldenCreated; + public List Differences = new List(); + } + + /// シナリオ×モデル1回分の実行結果 + public sealed class VMCTestResult + { + public string Scenario; + public string Model; + public bool Skipped; + public string SkipReason; + public string Error; + public readonly List Checks = new List(); + + public bool Passed => Skipped == false && Error == null && Checks.All(d => d.Passed); + + public string Title => $"{Scenario} [{Model}]"; + + /// + /// ゴールデンに依存しない不変条件の検査。 + /// 「そもそも動いているか」はゴールデン比較では検出できない + /// (壊れた状態がそのまま期待値として保存されてしまう)ので、これで担保する。 + /// + public VMCTestCheck CheckThat(string label, bool condition, string failureMessage) + { + var check = new VMCTestCheck { Label = label, Passed = condition }; + if (condition == false) + { + check.Differences.Add(failureMessage); + Debug.LogError($"[VMCTest] {Title} {label}: {failureMessage}"); + } + Checks.Add(check); + return check; + } + + /// + /// スナップショットをゴールデンと比較する。 + /// ゴールデンが無い場合、または UpdateGolden 指定時は、現在の値をゴールデンとして保存する。 + /// + public VMCTestCheck CheckSnapshot(VMCTestContext context, VMCTestSnapshot actual) + { + var check = new VMCTestCheck { Label = actual.Label }; + Checks.Add(check); + + var config = context.Config; + + //実行結果は毎回残す(差分調査用) + try + { + actual.Save(config.ResolvedOutputDirectory); + } + catch (Exception ex) + { + Debug.LogWarning($"[VMCTest] 実行結果の保存に失敗しました: {ex.Message}"); + } + + var goldenDirectory = config.ResolvedGoldenDirectory; + var expected = VMCTestSnapshot.Load(goldenDirectory, actual.FileName); + + if (config.UpdateGolden || expected == null) + { + actual.Save(goldenDirectory); + check.Passed = true; + check.GoldenCreated = true; + Debug.Log($"[VMCTest] ゴールデンを保存しました: {Path.Combine(goldenDirectory, actual.FileName)}"); + return check; + } + + check.Differences = actual.CompareTo(expected, config); + check.Passed = check.Differences.Count == 0; + return check; + } + } + + /// + /// テストシナリオの基底。 + /// Runはコルーチンで、途中でthrowすると失敗として記録される。 + /// + public abstract class VMCTestScenario + { + public abstract string Name { get; } + + public virtual string Description => ""; + + /// このシナリオを実行するモデル種別 + public virtual IReadOnlyList Models => new[] { VMCTestModels.Vrm0, VMCTestModels.Vrm10 }; + + /// + /// アバターを必要とするか。falseにすると Models を 1つにして、 + /// VRMが未設定でもスキップせずに実行する。 + /// + public virtual bool RequiresModel => true; + + public abstract IEnumerator Run(VMCTestContext context, VMCTestResult result); + } + + /// 実行結果全体のレポート + public static class VMCTestReport + { + public static string Build(IReadOnlyList results) + { + var builder = new StringBuilder(); + var passed = results.Count(d => d.Passed); + var skipped = results.Count(d => d.Skipped); + var failed = results.Count - passed - skipped; + + builder.AppendLine("==== VMC 自動テスト結果 ===="); + builder.AppendLine($"合計 {results.Count} / 成功 {passed} / 失敗 {failed} / スキップ {skipped}"); + builder.AppendLine(); + + foreach (var result in results) + { + if (result.Skipped) + { + builder.AppendLine($"[SKIP] {result.Title} : {result.SkipReason}"); + continue; + } + + builder.AppendLine($"[{(result.Passed ? "PASS" : "FAIL")}] {result.Title}"); + + if (result.Error != null) + { + builder.AppendLine($" 例外: {result.Error}"); + } + + foreach (var check in result.Checks) + { + if (check.GoldenCreated) + { + builder.AppendLine($" - {check.Label}: ゴールデンを新規作成"); + continue; + } + if (check.Passed) + { + builder.AppendLine($" - {check.Label}: 一致"); + continue; + } + + builder.AppendLine($" - {check.Label}: {check.Differences.Count}件の差分"); + foreach (var difference in check.Differences.Take(20)) + { + builder.AppendLine($" {difference}"); + } + if (check.Differences.Count > 20) + { + builder.AppendLine($" ... 他 {check.Differences.Count - 20} 件"); + } + } + } + + return builder.ToString(); + } + } +} diff --git a/Assets/Tests/VMCTestScenario.cs.meta b/Assets/Tests/VMCTestScenario.cs.meta new file mode 100644 index 00000000..21dad53b --- /dev/null +++ b/Assets/Tests/VMCTestScenario.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8e7b362aa9c23b4885920ca7b821005 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/VMCTestSnapshot.cs b/Assets/Tests/VMCTestSnapshot.cs new file mode 100644 index 00000000..3da57870 --- /dev/null +++ b/Assets/Tests/VMCTestSnapshot.cs @@ -0,0 +1,602 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using UniVRM10; + +namespace VMC.Tests +{ + [Serializable] + public class VMCTestPose + { + public string Name; + public Vector3 Position; + public Quaternion Rotation; + } + + [Serializable] + public class VMCTestWeight + { + public string Name; + public float Value; + } + + /// OSCの引数1個。TはOSCの型タグ(i/f/s/b) + [Serializable] + public class VMCTestOscArg + { + public string T; + public float F; + public int I; + public string S; + + public static VMCTestOscArg From(object value) + { + if (value is float f) return new VMCTestOscArg { T = "f", F = f }; + if (value is int i) return new VMCTestOscArg { T = "i", I = i }; + if (value is string s) return new VMCTestOscArg { T = "s", S = s }; + if (value is byte[] b) return new VMCTestOscArg { T = "b", I = b.Length }; + return new VMCTestOscArg { T = "?", S = value?.ToString() }; + } + + public override string ToString() + { + switch (T) + { + case "f": return F.ToString("F6"); + case "i": return I.ToString(); + case "s": return S; + default: return $"{T}:{S}"; + } + } + } + + [Serializable] + public class VMCTestOscMessage + { + public string Address; + public List Args = new List(); + + public static VMCTestOscMessage From(uOSC.Message message) + { + var result = new VMCTestOscMessage { Address = message.address }; + if (message.values != null) + { + foreach (var value in message.values) + { + result.Args.Add(VMCTestOscArg.From(value)); + } + } + return result; + } + + /// 同じ対象を指すメッセージかどうか(アドレス + 先頭のstring引数) + public string Key => Args.Count > 0 && Args[0].T == "s" ? $"{Address}/{Args[0].S}" : Address; + + public override string ToString() => $"{Address} [{string.Join(", ", Args.Select(d => d.ToString()))}]"; + } + + /// + /// ある時点のアバター状態と、その時点でVMCProtocolとして送信された内容のスナップショット。 + /// ボーン姿勢・表情・視線・送信OSCを1つの形式にまとめることで、 + /// 「受信 → 内部状態 → 送信 → VRMA書き出し」を同じ比較器で検証できるようにしている。 + /// + [Serializable] + public class VMCTestSnapshot + { + public string Scenario; + public string Model; + public string Label; + public int Frame; + + public VMCTestPose RootPose; + + /// 正規化(ControlRig)ボーン。VMC内部の処理はこちらで統一されている + public List Bones = new List(); + + /// + /// オリジナル(非正規化)ボーン。VMCProtocolが送受信するのはこちら。 + /// VRM0.x由来のモデルでは Bones と一致する。 + /// + public List OriginalBones = new List(); + public List Expressions = new List(); + + public bool HasLookAt; + public float LookAtYaw; + public float LookAtPitch; + + /// この区間にExternalSenderが送信したOSCメッセージ(アドレス+先頭引数で重複排除済み、最後の値) + public List Sent = new List(); + + public string FileName => $"{Scenario}.{Model}.{Label}.json"; + + #region Capture + + /// + /// 現在のアバターの状態をスナップショットに取る。 + /// ExternalSenderが送っている情報と同じものを、同じ経路(Animator.GetBoneTransform / ActualWeights)から取得する。 + /// + public static VMCTestSnapshot Capture(string scenario, string model, string label, int frame, GameObject currentModel) + { + var snapshot = new VMCTestSnapshot + { + Scenario = scenario, + Model = model, + Label = label, + Frame = frame, + }; + + if (currentModel == null) return snapshot; + + var animator = currentModel.GetComponent(); + if (animator != null) + { + snapshot.RootPose = new VMCTestPose + { + Name = "root", + Position = animator.transform.position, + Rotation = animator.transform.rotation, + }; + + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + var boneTransform = animator.GetBoneTransform(bone); + if (boneTransform == null) continue; + snapshot.Bones.Add(new VMCTestPose + { + Name = bone.ToString(), + Position = boneTransform.localPosition, + Rotation = boneTransform.localRotation, + }); + } + } + + var vrm10Instance = currentModel.GetComponent(); + if (vrm10Instance != null) + { + //VMCProtocolが送受信するオリジナル(非正規化)ボーン + if (vrm10Instance.Humanoid != null) + { + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + var boneTransform = vrm10Instance.Humanoid.GetBoneTransform(bone); + if (boneTransform == null) continue; + snapshot.OriginalBones.Add(new VMCTestPose + { + Name = bone.ToString(), + Position = boneTransform.localPosition, + Rotation = boneTransform.localRotation, + }); + } + } + + Vrm10Runtime runtime = null; + try { runtime = vrm10Instance.Runtime; } + catch (Exception) { /* 初期化前は取得できない */ } + + if (runtime != null) + { + if (runtime.Expression != null) + { + //VMCProtocolの送信と同じくVRM0.x互換名で記録する(VRM0.x/VRM1.0で同じゴールデンを使うため) + foreach (var pair in runtime.Expression.ActualWeights.OrderBy(d => d.Key.ToString(), StringComparer.Ordinal)) + { + snapshot.Expressions.Add(new VMCTestWeight + { + Name = VRM10CompatibleNames.GetVRM0CompatibleName(pair.Key), + Value = pair.Value, + }); + } + } + + if (runtime.LookAt != null) + { + snapshot.HasLookAt = true; + snapshot.LookAtYaw = runtime.LookAt.Yaw; + snapshot.LookAtPitch = runtime.LookAt.Pitch; + } + } + } + + return snapshot; + } + + /// + /// 実行のたびに必ず変わる(または環境依存の)送信内容。比較対象から外す。 + /// + public static readonly HashSet IgnoredSentAddresses = new HashSet + { + "/VMC/Ext/T", //起動からの経過秒 + "/VMC/Ext/VRM", //VRMの絶対パス + "/VMC/Ext/Config", //設定ファイルの絶対パス + "/VMC/Ext/Remote", + "/VMC/Ext/Opt", + }; + + public void SetSentMessages(IEnumerable messages) + { + //同じ対象に対する複数フレーム分の送信は最後の値だけを残す(フレーム境界のゆらぎを吸収する) + var latest = new Dictionary(); + var order = new List(); + foreach (var message in messages) + { + if (IgnoredSentAddresses.Contains(message.address)) continue; + var converted = VMCTestOscMessage.From(message); + if (latest.ContainsKey(converted.Key) == false) order.Add(converted.Key); + latest[converted.Key] = converted; + } + Sent = order.Select(d => latest[d]).OrderBy(d => d.Key, StringComparer.Ordinal).ToList(); + } + + #endregion + + #region IO + + public void Save(string directory) + { + Directory.CreateDirectory(directory); + File.WriteAllText(Path.Combine(directory, FileName), JsonUtility.ToJson(this, true)); + } + + public static VMCTestSnapshot Load(string directory, string fileName) + { + var path = Path.Combine(directory, fileName); + if (File.Exists(path) == false) return null; + return JsonUtility.FromJson(File.ReadAllText(path)); + } + + #endregion + + #region 検査用ヘルパー + + public VMCTestPose GetBone(string boneName) => Bones.FirstOrDefault(d => d.Name == boneName); + + /// + /// VMCProtocolが送受信するボーン(オリジナル)。 + /// 記録されていない場合(VRM1.0でないなど)は正規化ボーンで代用する。 + /// + public VMCTestPose GetProtocolBone(string boneName) + => OriginalBones.FirstOrDefault(d => d.Name == boneName) ?? GetBone(boneName); + + public float GetExpression(string name) + { + var entry = Expressions.FirstOrDefault(d => d.Name == name); + return entry != null ? entry.Value : 0f; + } + + /// + /// 送信された /VMC/Ext/Bone/Pos が、実際のアバターのボーン姿勢と一致しているかを検査する。 + /// (VMCProtocolの受信結果と送信内容が食い違っていないことの確認) + /// 戻り値は食い違いの一覧。空なら一致。 + /// + public List VerifySentBonesMatchState(float positionTolerance, float rotationToleranceDegrees) + { + var differences = new List(); + int compared = 0; + + foreach (var message in Sent) + { + if (message.Address != "/VMC/Ext/Bone/Pos") continue; + if (message.Args.Count != 8 || message.Args[0].T != "s") continue; + + //仕様上、送信されるのはオリジナル(非正規化)ボーンなのでそちらと比べる + var bone = GetProtocolBone(message.Args[0].S); + if (bone == null) + { + differences.Add($"送信された {message.Args[0].S} がアバターに存在しません"); + continue; + } + + var sentPosition = new Vector3(message.Args[1].F, message.Args[2].F, message.Args[3].F); + var sentRotation = new Quaternion(message.Args[4].F, message.Args[5].F, message.Args[6].F, message.Args[7].F); + + var distance = Vector3.Distance(bone.Position, sentPosition); + if (distance > positionTolerance) + { + differences.Add($"{bone.Name} の送信位置が状態と不一致 距離{distance:F5}"); + } + var angle = Quaternion.Angle(bone.Rotation, sentRotation); + if (angle > rotationToleranceDegrees) + { + differences.Add($"{bone.Name} の送信回転が状態と不一致 角度{angle:F3}度"); + } + compared++; + } + + if (compared == 0) + { + differences.Add("/VMC/Ext/Bone/Pos が1件も送信されていません"); + } + return differences; + } + + /// + /// ボーンの「回転だけ」を比較する。 + /// VRMA/BVHの往復はマッスル空間とglTFの量子化を通るため位置は一致しない。 + /// + public static List CompareBoneRotations(VMCTestSnapshot expected, VMCTestSnapshot actual, + float toleranceDegrees, out float maxAngle, out string worstBone) + => CompareBoneRotations(expected, actual, toleranceDegrees, null, out maxAngle, out worstBone); + + /// + /// 末端ボーンまでの親子チェーン。 + /// ローカル回転を掛け合わせるとルートから見た向きが得られる。 + /// 個々のボーンの回転が違っても末端の向きが同じなら、見た目の姿勢は同じ。 + /// (Humanoidのリターゲットは腕のツイストを上腕と手の間で配分し直すため、 + /// ボーン単位の比較だけでは「見た目が同じか」を判定できない) + /// + public static readonly (string Name, string[] Chain)[] EndEffectorChains = + { + ("頭", new[]{ "Hips","Spine","Chest","UpperChest","Neck","Head" }), + ("左手", new[]{ "Hips","Spine","Chest","UpperChest","LeftShoulder","LeftUpperArm","LeftLowerArm","LeftHand" }), + ("右手", new[]{ "Hips","Spine","Chest","UpperChest","RightShoulder","RightUpperArm","RightLowerArm","RightHand" }), + ("左足", new[]{ "Hips","LeftUpperLeg","LeftLowerLeg","LeftFoot" }), + ("右足", new[]{ "Hips","RightUpperLeg","RightLowerLeg","RightFoot" }), + }; + + /// チェーン上のローカル回転を掛け合わせて、ルートから見た向きを求める(無いボーンは飛ばす) + public Quaternion GetAccumulatedRotation(string[] chain) + { + var rotation = Quaternion.identity; + foreach (var boneName in chain) + { + var bone = GetBone(boneName); + if (bone == null) continue; + rotation = rotation * bone.Rotation; + } + return rotation; + } + + /// 末端ボーンの向き(=見た目の姿勢)を比較する + public static List CompareEndEffectors(VMCTestSnapshot expected, VMCTestSnapshot actual, + float toleranceDegrees, out float maxAngle, out string worst) + { + var differences = new List(); + maxAngle = 0f; + worst = null; + + foreach (var (name, chain) in EndEffectorChains) + { + var angle = Quaternion.Angle(expected.GetAccumulatedRotation(chain), actual.GetAccumulatedRotation(chain)); + if (angle > maxAngle) + { + maxAngle = angle; + worst = name; + } + if (angle > toleranceDegrees) + { + differences.Add($"{name} {angle:F2}度"); + } + } + return differences; + } + + /// 指かどうか(マッスル空間の表現力が特に低いので別枠で扱う) + public static bool IsFingerBone(string boneName) + => boneName.Contains("Thumb") || boneName.Contains("Index") || boneName.Contains("Middle") + || boneName.Contains("Ring") || boneName.Contains("Little"); + + public static List CompareBoneRotations(VMCTestSnapshot expected, VMCTestSnapshot actual, + float toleranceDegrees, Func boneFilter, out float maxAngle, out string worstBone) + { + var differences = new List(); + maxAngle = 0f; + worstBone = null; + + foreach (var boneExpected in expected.Bones) + { + if (boneFilter != null && boneFilter(boneExpected.Name) == false) continue; + var boneActual = actual.GetBone(boneExpected.Name); + if (boneActual == null) + { + differences.Add($"{boneExpected.Name} が存在しません"); + continue; + } + var angle = Quaternion.Angle(boneExpected.Rotation, boneActual.Rotation); + if (angle > maxAngle) + { + maxAngle = angle; + worstBone = boneExpected.Name; + } + if (angle > toleranceDegrees) + { + differences.Add($"{boneExpected.Name} {angle:F2}度"); + } + } + return differences; + } + + /// 表情の重みを比較する + public static List CompareExpressions(VMCTestSnapshot expected, VMCTestSnapshot actual, + float tolerance, out float maxDelta, out string worstKey) + { + var differences = new List(); + maxDelta = 0f; + worstKey = null; + + foreach (var entry in expected.Expressions) + { + var value = actual.GetExpression(entry.Name); + var delta = Mathf.Abs(entry.Value - value); + if (delta > maxDelta) + { + maxDelta = delta; + worstKey = entry.Name; + } + if (delta > tolerance) + { + differences.Add($"{entry.Name} {entry.Value:F3} -> {value:F3}"); + } + } + return differences; + } + + /// 2つのスナップショットの間で最も大きく回転したボーンの角度(度) + public static float MaxBoneRotationDelta(VMCTestSnapshot a, VMCTestSnapshot b, out string boneName) + { + boneName = null; + float max = 0f; + if (a == null || b == null) return 0f; + + foreach (var boneA in a.Bones) + { + var boneB = b.GetBone(boneA.Name); + if (boneB == null) continue; + var angle = Quaternion.Angle(boneA.Rotation, boneB.Rotation); + if (angle > max) + { + max = angle; + boneName = boneA.Name; + } + } + return max; + } + + #endregion + + #region Compare + + /// + /// ゴールデンとの差分を列挙する。空リストなら一致。 + /// + public List CompareTo(VMCTestSnapshot expected, VMCTestConfig config) + { + var differences = new List(); + if (expected == null) + { + differences.Add("期待値(ゴールデン)が存在しません"); + return differences; + } + + ComparePose(differences, "Root", expected.RootPose, RootPose, config); + + CompareByName(differences, "Bone", expected.Bones, Bones, d => d.Name, + (diffs, name, e, a) => ComparePose(diffs, $"Bone[{name}]", e, a, config)); + + CompareByName(differences, "Expression", expected.Expressions, Expressions, d => d.Name, + (diffs, name, e, a) => + { + if (Mathf.Abs(e.Value - a.Value) > config.WeightTolerance) + { + diffs.Add($"Expression[{name}] weight {e.Value:F4} -> {a.Value:F4}"); + } + }); + + if (expected.HasLookAt != HasLookAt) + { + differences.Add($"LookAt の有無が異なります {expected.HasLookAt} -> {HasLookAt}"); + } + else if (HasLookAt) + { + if (Mathf.Abs(Mathf.DeltaAngle(expected.LookAtYaw, LookAtYaw)) > config.RotationToleranceDegrees) + { + differences.Add($"LookAt.Yaw {expected.LookAtYaw:F3} -> {LookAtYaw:F3}"); + } + if (Mathf.Abs(Mathf.DeltaAngle(expected.LookAtPitch, LookAtPitch)) > config.RotationToleranceDegrees) + { + differences.Add($"LookAt.Pitch {expected.LookAtPitch:F3} -> {LookAtPitch:F3}"); + } + } + + CompareByName(differences, "Sent", expected.Sent, Sent, d => d.Key, + (diffs, key, e, a) => CompareOsc(diffs, key, e, a, config)); + + return differences; + } + + private static void CompareByName(List differences, string category, List expected, List actual, + Func keySelector, Action, string, T, T> compare) + { + expected = expected ?? new List(); + actual = actual ?? new List(); + + var expectedMap = new Dictionary(); + foreach (var item in expected) expectedMap[keySelector(item)] = item; + var actualMap = new Dictionary(); + foreach (var item in actual) actualMap[keySelector(item)] = item; + + foreach (var pair in expectedMap) + { + if (actualMap.TryGetValue(pair.Key, out var actualItem) == false) + { + differences.Add($"{category}[{pair.Key}] が無くなりました"); + continue; + } + compare(differences, pair.Key, pair.Value, actualItem); + } + foreach (var pair in actualMap) + { + if (expectedMap.ContainsKey(pair.Key) == false) + { + differences.Add($"{category}[{pair.Key}] が増えました"); + } + } + } + + private static void ComparePose(List differences, string label, VMCTestPose expected, VMCTestPose actual, VMCTestConfig config) + { + if (expected == null && actual == null) return; + if (expected == null || actual == null) + { + differences.Add($"{label} の有無が異なります"); + return; + } + + var distance = Vector3.Distance(expected.Position, actual.Position); + if (distance > config.PositionTolerance) + { + differences.Add($"{label}.Position 距離{distance:F5} {Format(expected.Position)} -> {Format(actual.Position)}"); + } + + var angle = Quaternion.Angle(expected.Rotation, actual.Rotation); + if (angle > config.RotationToleranceDegrees) + { + differences.Add($"{label}.Rotation 角度{angle:F3}度 {Format(expected.Rotation)} -> {Format(actual.Rotation)}"); + } + } + + private static void CompareOsc(List differences, string key, VMCTestOscMessage expected, VMCTestOscMessage actual, VMCTestConfig config) + { + if (expected.Args.Count != actual.Args.Count) + { + differences.Add($"Sent[{key}] 引数の数 {expected.Args.Count} -> {actual.Args.Count}"); + return; + } + + for (int i = 0; i < expected.Args.Count; i++) + { + var e = expected.Args[i]; + var a = actual.Args[i]; + if (e.T != a.T) + { + differences.Add($"Sent[{key}] 引数{i} の型 {e.T} -> {a.T}"); + continue; + } + switch (e.T) + { + case "f": + //位置と回転が混在するため、より緩い方(位置)の許容誤差で比較する + if (Mathf.Abs(e.F - a.F) > config.PositionTolerance) + { + differences.Add($"Sent[{key}] 引数{i} {e.F:F6} -> {a.F:F6}"); + } + break; + case "i": + if (e.I != a.I) differences.Add($"Sent[{key}] 引数{i} {e.I} -> {a.I}"); + break; + case "s": + if (e.S != a.S) differences.Add($"Sent[{key}] 引数{i} \"{e.S}\" -> \"{a.S}\""); + break; + } + } + } + + private static string Format(Vector3 v) => $"({v.x:F4}, {v.y:F4}, {v.z:F4})"; + private static string Format(Quaternion q) => $"({q.x:F4}, {q.y:F4}, {q.z:F4}, {q.w:F4})"; + + #endregion + } +} diff --git a/Assets/Tests/VMCTestSnapshot.cs.meta b/Assets/Tests/VMCTestSnapshot.cs.meta new file mode 100644 index 00000000..3e884a09 --- /dev/null +++ b/Assets/Tests/VMCTestSnapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f696196c3799a14d99add91bcebf13d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Textures/icon1024.png.meta b/Assets/Textures/icon1024.png.meta index dddbb2f9..81311792 100644 --- a/Assets/Textures/icon1024.png.meta +++ b/Assets/Textures/icon1024.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 5cc10efa0ccf3f846b15433cd86eee19 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon128.png.meta b/Assets/Textures/icon128.png.meta index 90f8be87..dd615f4f 100644 --- a/Assets/Textures/icon128.png.meta +++ b/Assets/Textures/icon128.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 5d8c222f3ab749d41847123745793c3f TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon16.png.meta b/Assets/Textures/icon16.png.meta index 45da1a65..08cd2f39 100644 --- a/Assets/Textures/icon16.png.meta +++ b/Assets/Textures/icon16.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 70fec002528b9b641ab37f237b9e31a5 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon256.png.meta b/Assets/Textures/icon256.png.meta index 5863ed4c..99ed0c5c 100644 --- a/Assets/Textures/icon256.png.meta +++ b/Assets/Textures/icon256.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 1a4d5cd4a18efc343a0439918dbd61b9 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon32.png.meta b/Assets/Textures/icon32.png.meta index 43bf6f6a..a651cc82 100644 --- a/Assets/Textures/icon32.png.meta +++ b/Assets/Textures/icon32.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 99b913cc082599c45bf38eab21df9d19 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon48.png.meta b/Assets/Textures/icon48.png.meta index c00ebd43..e451bcb7 100644 --- a/Assets/Textures/icon48.png.meta +++ b/Assets/Textures/icon48.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: ec6d7147fa5bf9743ad60ca4d98cf682 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon512.png.meta b/Assets/Textures/icon512.png.meta index cb68c6f4..4b47f17b 100644 --- a/Assets/Textures/icon512.png.meta +++ b/Assets/Textures/icon512.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 04e42e353fdd91945a24e8e5f5631ee5 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark1024.png.meta b/Assets/Textures/icon_dark1024.png.meta index 0f301ed2..152a86ca 100644 --- a/Assets/Textures/icon_dark1024.png.meta +++ b/Assets/Textures/icon_dark1024.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 17f8827c8ffb44148999cf5f02f8d83f TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark128.png.meta b/Assets/Textures/icon_dark128.png.meta index 09ea44c2..dc71875a 100644 --- a/Assets/Textures/icon_dark128.png.meta +++ b/Assets/Textures/icon_dark128.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 16d0e141ae472904ea249b59a03609e3 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark16.png.meta b/Assets/Textures/icon_dark16.png.meta index 1ea7637b..7c8235b6 100644 --- a/Assets/Textures/icon_dark16.png.meta +++ b/Assets/Textures/icon_dark16.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 5ccf05c7cef860e49927b034e9bd1221 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark256.png.meta b/Assets/Textures/icon_dark256.png.meta index f903de4b..dcb316d6 100644 --- a/Assets/Textures/icon_dark256.png.meta +++ b/Assets/Textures/icon_dark256.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 54f2018fa40f8e845b5be293321811d8 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark32.png.meta b/Assets/Textures/icon_dark32.png.meta index 19a53d34..a9f14d01 100644 --- a/Assets/Textures/icon_dark32.png.meta +++ b/Assets/Textures/icon_dark32.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 7bf6060de4d9bd84cac4870556999a8b TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark48.png.meta b/Assets/Textures/icon_dark48.png.meta index ebc48845..3259ab8f 100644 --- a/Assets/Textures/icon_dark48.png.meta +++ b/Assets/Textures/icon_dark48.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 3bbf70ba636a2c2418263399c5dc277a TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Textures/icon_dark512.png.meta b/Assets/Textures/icon_dark512.png.meta index 6be8f95c..cd75d0eb 100644 --- a/Assets/Textures/icon_dark512.png.meta +++ b/Assets/Textures/icon_dark512.png.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 guid: 9ae5e133237bf284ab4f1ae7dc9534b1 TextureImporter: - fileIDToRecycleName: {} + internalIDToNameTable: [] externalObjects: {} - serializedVersion: 5 + serializedVersion: 12 mipmaps: mipMapMode: 0 enableMipMap: 1 @@ -20,7 +20,12 @@ TextureImporter: externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 + flipGreenChannel: 0 isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 grayScaleToAlpha: 0 generateCubemap: 6 cubemapConvolution: 0 @@ -29,12 +34,12 @@ TextureImporter: maxTextureSize: 2048 textureSettings: serializedVersion: 2 - filterMode: -1 - aniso: -1 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 nPOTScale: 1 lightmap: 0 compressionQuality: 50 @@ -52,11 +57,17 @@ TextureImporter: textureType: 0 textureShape: 1 singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 maxTextureSizeSet: 0 compressionQualitySet: 0 textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 platformSettings: - - serializedVersion: 2 + - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -66,7 +77,35 @@ TextureImporter: crunchedCompression: 0 allowsAlphaSplitting: 0 overridden: 0 + ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 spriteSheet: serializedVersion: 2 sprites: [] @@ -74,11 +113,15 @@ TextureImporter: physicsShape: [] bones: [] spriteID: + internalID: 0 vertices: [] indices: edges: [] weights: [] - spritePackingTag: + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/VMCMOD/VMCEvents.cs b/Assets/VMCMOD/VMCEvents.cs index 51647692..50c45816 100644 --- a/Assets/VMCMOD/VMCEvents.cs +++ b/Assets/VMCMOD/VMCEvents.cs @@ -5,10 +5,13 @@ namespace VMC { public class VMCEvents { + public static Action OnCurrentModelChanged = null; public static Action OnModelLoaded = null; public static Action OnModelUnloading = null; public static Action OnCameraChanged = null; public static Action OnLightChanged = null; public static Action OnLoadedConfigPathChanged = null; + public static Action BeforeApplyMotion = null; + public static Action AfterApplyMotion = null; } } \ No newline at end of file diff --git a/Assets/VMCPlugin.meta b/Assets/VMCPlugin.meta new file mode 100644 index 00000000..cc6026f3 --- /dev/null +++ b/Assets/VMCPlugin.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d13d8fb522cb1cb4a999a8cd264a6e12 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IFaceControl.cs b/Assets/VMCPlugin/IFaceControl.cs new file mode 100644 index 00000000..7113052c --- /dev/null +++ b/Assets/VMCPlugin/IFaceControl.cs @@ -0,0 +1,44 @@ +using System; +using UnityEngine; + +namespace VMC.Plugin +{ + /// + /// 表情・視線の制御。本体の FaceController を抽象化したもの。 + /// + /// UniVRM の型を露出させないため、視線は「見る先のワールド座標」を渡す形にしている。 + /// (ボーン目線・Expression目線のどちらかは本体側が面倒を見る) + /// + public interface IFaceControl + { + /// + /// 表情がモデルへ適用される直前に呼ばれる。 + /// 視線の上書き(SetLookAtPosition)はこのタイミングで行うこと。 + /// + event Action BeforeApply; + + /// 左まぶたを閉じる量(0=開き, 1=閉じ) + void SetBlink_L(float value); + + /// 右まぶたを閉じる量(0=開き, 1=閉じ) + void SetBlink_R(float value); + + /// + /// 表情キー名と重みの組を混ぜ込む。 + /// presetName は混ぜ込み元の識別名で、同じ名前での再呼び出しは上書きになる。 + /// + void MixPresets(string presetName, string[] keys, float[] values); + + /// + /// 目線を指定したワールド座標へ向ける。BeforeApply の中から呼ぶこと。 + /// (LookAtTarget 未使用時のみ有効) + /// + void SetLookAtPosition(Vector3 worldPosition); + + /// + /// まぶたを外部デバイスが制御していることを本体に伝える。 + /// true の間、本体の自動まばたきは抑制される。 + /// + bool ExternalEyelidControlEnabled { get; set; } + } +} diff --git a/Assets/VMCPlugin/IFaceControl.cs.meta b/Assets/VMCPlugin/IFaceControl.cs.meta new file mode 100644 index 00000000..069c94a9 --- /dev/null +++ b/Assets/VMCPlugin/IFaceControl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18f0d85cab5d7254c9c5c38c71d1e5ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IMotionSource.cs b/Assets/VMCPlugin/IMotionSource.cs new file mode 100644 index 00000000..c2e3768b --- /dev/null +++ b/Assets/VMCPlugin/IMotionSource.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace VMC.Plugin +{ + /// + /// 外部デバイスのモーションをアバターへ流し込むための入口。 + /// + public interface IMotionSourceFactory + { + /// + /// boneParentTransform 以下のボーン階層を「外部デバイス由来のモーション」として + /// アバターへ適用する VirtualAvatar を作り、本体へ登録する。 + /// 適用優先度は VRIK より後、VMCProtocol より前。 + /// + /// 返された VirtualAvatar の Enable と Apply* で反映を制御する。 + /// 使い終わったら Remove を呼ぶこと。 + /// + VirtualAvatar Create(Transform boneParentTransform); + + /// Create で作った VirtualAvatar の登録を解除する + void Remove(VirtualAvatar virtualAvatar); + } +} diff --git a/Assets/VMCPlugin/IMotionSource.cs.meta b/Assets/VMCPlugin/IMotionSource.cs.meta new file mode 100644 index 00000000..338e777d --- /dev/null +++ b/Assets/VMCPlugin/IMotionSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a8a59de033a99a4c9d54d099709adc6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IPluginHost.cs b/Assets/VMCPlugin/IPluginHost.cs new file mode 100644 index 00000000..905bdccf --- /dev/null +++ b/Assets/VMCPlugin/IPluginHost.cs @@ -0,0 +1,33 @@ +using System; +using UnityEngine; + +namespace VMC.Plugin +{ + /// + /// プラグインから本体機能へアクセスするための窓口。 + /// 本体側(Assembly-CSharp)が実装し、Initialize でプラグインへ渡される。 + /// + public interface IPluginHost + { + /// 表情・視線の制御 + IFaceControl FaceControl { get; } + + /// モーションソースの登録 + IMotionSourceFactory MotionSource { get; } + + /// コントロールパネルとの通信 + IPluginIpc Ipc { get; } + + /// 現在読み込まれているモデル(未読み込みなら null) + GameObject CurrentModel { get; } + + /// プラグイン単位の設定領域を取得する + IPluginSettings GetSettings(string pluginId); + + /// + /// 本体の設定(プロファイル)が読み込まれ、各機能へ適用されるタイミング。 + /// プラグインは保存済み設定をここで自身へ反映する。 + /// + event Action SettingsApplied; + } +} diff --git a/Assets/VMCPlugin/IPluginHost.cs.meta b/Assets/VMCPlugin/IPluginHost.cs.meta new file mode 100644 index 00000000..4a97dbd4 --- /dev/null +++ b/Assets/VMCPlugin/IPluginHost.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 406120092cdb496479a74231c27678ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IPluginIpc.cs b/Assets/VMCPlugin/IPluginIpc.cs new file mode 100644 index 00000000..b4ddadb3 --- /dev/null +++ b/Assets/VMCPlugin/IPluginIpc.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using UnityMemoryMappedFile; + +namespace VMC.Plugin +{ + /// + /// コントロールパネル(WPF)との通信。 + /// + /// プラグイン独自のコマンドはプラグイン側のDLLに定義し、IVMCPlugin.CommandTypes で + /// 登録する。対応付けは型の単純名で行われるので、コントロールパネル側のプラグインと + /// 同じ名前・同じ名前空間の型を用意すること(共有ソースをリンクするのが確実)。 + /// + public interface IPluginIpc + { + /// + /// コントロールパネルからコマンドを受信したときに呼ばれる。 + /// Unityのメインスレッドとは限らないため、Unity APIを触る場合は Post を使うこと。 + /// + event EventHandler Received; + + /// コントロールパネルへコマンドを送る。応答を返す場合は requestId を指定する。 + Task SendCommandAsync(object command, string requestId = null); + + /// Unityのメインスレッドで処理を実行する + void Post(Action action); + } +} diff --git a/Assets/VMCPlugin/IPluginIpc.cs.meta b/Assets/VMCPlugin/IPluginIpc.cs.meta new file mode 100644 index 00000000..69bd2e5d --- /dev/null +++ b/Assets/VMCPlugin/IPluginIpc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac576efce3de2354d930b1bb71e47c13 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IPluginSettings.cs b/Assets/VMCPlugin/IPluginSettings.cs new file mode 100644 index 00000000..ec73e966 --- /dev/null +++ b/Assets/VMCPlugin/IPluginSettings.cs @@ -0,0 +1,18 @@ +namespace VMC.Plugin +{ + /// + /// プラグイン単位の設定領域。 + /// + /// 本体の設定ファイル(プロファイル)の中に保存されるため、 + /// ユーザーが設定プロファイルを切り替えるとプラグインの設定も一緒に切り替わる。 + /// 値は型ごとにJSONへ直列化して保持される。 + /// + public interface IPluginSettings + { + /// 保存済みの値を取得する。無い場合や読めない場合は defaultValue を返す。 + T Get(string key, T defaultValue = default); + + /// 値を保存する。ファイルへの書き出しは本体の保存タイミングで行われる。 + void Set(string key, T value); + } +} diff --git a/Assets/VMCPlugin/IPluginSettings.cs.meta b/Assets/VMCPlugin/IPluginSettings.cs.meta new file mode 100644 index 00000000..c69fabc8 --- /dev/null +++ b/Assets/VMCPlugin/IPluginSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2eaa45fe67543c945840878b5d6b46e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/IVMCPlugin.cs b/Assets/VMCPlugin/IVMCPlugin.cs new file mode 100644 index 00000000..e5bffbf9 --- /dev/null +++ b/Assets/VMCPlugin/IVMCPlugin.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace VMC.Plugin +{ + /// + /// 公式プラグインのエントリポイント。Plugins/ 配下のDLLから実装クラスが探される。 + /// 実装クラスは MonoBehaviour を継承すること(PluginManager が AddComponent する)。 + /// + /// ユーザー製作のMod(Mods/配下・VMCMod.VMCPlugin属性で識別)とは別系統。 + /// 違いは documents/plugins.md を参照。 + /// + public interface IVMCPlugin + { + /// + /// プラグインを一意に識別するID。設定の保存キーや、コントロールパネル側の + /// プラグインとの対応付けに使うため、両者で同じ文字列にすること。 + /// 例: "mocopi" / "ViveSR.Eye" / "Tobii" + /// + string Id { get; } + + /// ログ表示用の名前(コントロールパネルの表示名はWPF側が持つ) + string DisplayName { get; } + + /// プラグインのバージョン + string Version { get; } + + /// + /// コントロールパネルとやりとりする独自コマンドの型。 + /// 本体の共有アセンブリには入っていないので、受信時の型解決に使えるよう + /// PluginManager が PipeCommands へ登録する。無ければ null か空でよい。 + /// + IEnumerable CommandTypes { get; } + + /// + /// 本体の初期化中(設定の読み込み・適用より前)に一度だけ呼ばれる。 + /// 拡張点への登録はここで行う。 + /// + /// AddComponent の時点ではまだ host を受け取っていないため、 + /// 初期化を Awake に書かないこと。 + /// + void Initialize(IPluginHost host); + } +} diff --git a/Assets/VMCPlugin/IVMCPlugin.cs.meta b/Assets/VMCPlugin/IVMCPlugin.cs.meta new file mode 100644 index 00000000..3477be39 --- /dev/null +++ b/Assets/VMCPlugin/IVMCPlugin.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b92eb5189eee0cb4d81284362103d182 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/NativeLibraryLoader.cs b/Assets/VMCPlugin/NativeLibraryLoader.cs new file mode 100644 index 00000000..6523e876 --- /dev/null +++ b/Assets/VMCPlugin/NativeLibraryLoader.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace VMC.Plugin +{ + /// + /// プラグインが同梱するネイティブDLLを読み込めるようにするヘルパー。 + /// + /// Unity の DllImport はネイティブDLLを exe 直下や Plugins フォルダから探すため、 + /// プラグインのフォルダに置いたDLLはそのままでは解決できない。 + /// 先に絶対パスでプロセスへ読み込んでおけば、以降の DllImport は同じモジュールを使う。 + /// + /// ネイティブDLLは Plugins/<プラグイン名>/native/ に置く決まりにしている。 + /// マネージドDLLと同じ場所に混ぜないことで、 + /// 「どちらなのかをファイルの中身から判別する」処理が不要になる。 + /// + public static class NativeLibraryLoader + { + /// ネイティブDLLを置くサブフォルダ名 + public const string NativeDirectoryName = "native"; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadLibraryW(string lpFileName); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetDllDirectoryW(string lpPathName); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern uint GetDllDirectoryW(uint nBufferLength, System.Text.StringBuilder lpBuffer); + + /// + /// プラグインフォルダ直下の native/ にあるネイティブDLLをすべて先読みする。 + /// native/ が無ければ何もしない(ネイティブDLLを使わないプラグイン)。 + /// + /// プラグインのフォルダ(native/ の親) + /// 読み込めたDLLの数 + public static int PreloadFrom(string pluginDirectory) + { + var nativeDirectory = Path.Combine(pluginDirectory, NativeDirectoryName); + if (Directory.Exists(nativeDirectory) == false) return 0; + + //ネイティブDLL同士の依存を解決できるよう、検索パスにも追加しておく。 + //プロセス全体に効く設定なので、先読みが終わったら必ず元へ戻す + //(先読みしたDLLの依存はLoadLibraryWの時点で解決済みなので、 + // 戻した後の DllImport は同じモジュールを使える) + var previousDllDirectory = GetDllDirectory(); + SetDllDirectoryW(nativeDirectory); + try + { + var loaded = 0; + foreach (var dll in Directory.GetFiles(nativeDirectory, "*.dll", SearchOption.TopDirectoryOnly)) + { + if (LoadLibraryW(dll) != IntPtr.Zero) + { + loaded++; + } + else + { + Debug.LogWarning($"[Plugin] ネイティブDLLを読み込めませんでした: {dll} " + + $"(Win32エラー {Marshal.GetLastWin32Error()})"); + } + } + return loaded; + } + finally + { + //nullを渡すと既定の探索順に戻る + SetDllDirectoryW(previousDllDirectory); + } + } + + /// 現在のDLL探索ディレクトリ。設定されていなければ null + private static string GetDllDirectory() + { + //終端のnull分を含めた必要サイズが返るので、2回呼んで取得する + var length = GetDllDirectoryW(0, null); + if (length == 0) return null; + + var buffer = new System.Text.StringBuilder((int)length); + if (GetDllDirectoryW(length, buffer) == 0) return null; + + var path = buffer.ToString(); + return string.IsNullOrEmpty(path) ? null : path; + } + } +} diff --git a/Assets/VMCPlugin/NativeLibraryLoader.cs.meta b/Assets/VMCPlugin/NativeLibraryLoader.cs.meta new file mode 100644 index 00000000..474a38c6 --- /dev/null +++ b/Assets/VMCPlugin/NativeLibraryLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 642bf7a2939595d49a29c455c7104a39 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VMCPlugin/VMC.PluginAPI.asmdef b/Assets/VMCPlugin/VMC.PluginAPI.asmdef new file mode 100644 index 00000000..5854b3d5 --- /dev/null +++ b/Assets/VMCPlugin/VMC.PluginAPI.asmdef @@ -0,0 +1,15 @@ +{ + "name": "VMC.PluginAPI", + "references": [ + "VMCMod" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef.meta b/Assets/VMCPlugin/VMC.PluginAPI.asmdef.meta similarity index 76% rename from Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef.meta rename to Assets/VMCPlugin/VMC.PluginAPI.asmdef.meta index cc10b2b9..8e32b083 100644 --- a/Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef.meta +++ b/Assets/VMCPlugin/VMC.PluginAPI.asmdef.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: cb1c6da5597f6c14cb40b2a9899914ec +guid: 9061fc7e0f0cb1e4693a0a0bcb770b50 AssemblyDefinitionImporter: externalObjects: {} userData: diff --git a/Assets/VMCPlugin/VirtualAvatar.cs b/Assets/VMCPlugin/VirtualAvatar.cs new file mode 100644 index 00000000..ed992806 --- /dev/null +++ b/Assets/VMCPlugin/VirtualAvatar.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace VMC +{ + /// + /// 1つのモーション入力源から受けた姿勢を、部位ごとの適用設定に従ってアバターへ流し込む。 + /// + /// 外部デバイスプラグインが直接扱う型なので、本体(Assembly-CSharp)ではなく + /// VMC.PluginAPI 側に置いている(依存はUnityEngineのみ)。 + /// + [Serializable] + public class VirtualAvatar + { + private Transform parent; + private GameObject currentModel; + private Avatar avatar; + + public MotionSource MotionSource; + public Transform RootTransform; + public Transform transform => RootTransform; + public Animator animator; + + public Vector3 CenterOffsetPosition; + public float CenterOffsetRotationY; + + public bool ApplyRootRotation; + public bool ApplyRootPosition; + public bool ApplySpine; + public bool ApplyChest; + public bool ApplyHead; + public bool ApplyLeftArm; + public bool ApplyRightArm; + public bool ApplyLeftHand; + public bool ApplyRightHand; + public bool ApplyLeftLeg; + public bool ApplyRightLeg; + public bool ApplyLeftFoot; + public bool ApplyRightFoot; + public bool ApplyEye; + public bool ApplyLeftFinger; + public bool ApplyRightFinger; + + private bool enable = true; + + /// false の間はアバターへ反映されない + public bool Enable + { + get => enable; + set + { + if (enable == value) return; + enable = value; + EnableChanged?.Invoke(this); + } + } + + /// + /// Enable が変わったときに呼ばれる。 + /// 全身が動いているかどうかで挙動を変える箇所(カメラの注視点など)が使う。 + /// + public static event Action EnableChanged; + + public bool IgnoreDefaultBone = true; + + public bool CorrectHipBone = false; + + public Dictionary boneTransformCache; + public Dictionary BoneTransformCache => InitializeBoneTransformCache(); + + public Dictionary isPoseChanged; + + //クローン生成時(=BuildHumanAvatarでアバターのTポーズ基準として使われた姿勢)のローカルTRS。 + //モーション書き出し時にレスト姿勢へ戻すために使用する + private List<(Transform transform, Vector3 localPosition, Quaternion localRotation)> bindPose; + + + public const HumanBodyBones HumanBodyBonesRoot = (HumanBodyBones)(-1); + public static HumanBodyBones[] ReverseBodyBones = new HumanBodyBones[] { + HumanBodyBones.Head , + HumanBodyBones.Neck , + HumanBodyBones.LeftEye , + HumanBodyBones.RightEye , + HumanBodyBones.Jaw , + HumanBodyBones.LeftShoulder , + HumanBodyBones.RightShoulder , + HumanBodyBones.LeftUpperArm , + HumanBodyBones.RightUpperArm , + HumanBodyBones.LeftLowerArm , + HumanBodyBones.RightLowerArm , + HumanBodyBones.UpperChest , + HumanBodyBones.LeftHand , + HumanBodyBones.RightHand , + HumanBodyBones.LeftThumbProximal , + HumanBodyBones.LeftThumbIntermediate , + HumanBodyBones.LeftThumbDistal , + HumanBodyBones.LeftIndexProximal , + HumanBodyBones.LeftIndexIntermediate , + HumanBodyBones.LeftIndexDistal , + HumanBodyBones.LeftMiddleProximal , + HumanBodyBones.LeftMiddleIntermediate , + HumanBodyBones.LeftMiddleDistal , + HumanBodyBones.LeftRingProximal , + HumanBodyBones.LeftRingIntermediate , + HumanBodyBones.LeftRingDistal , + HumanBodyBones.LeftLittleProximal , + HumanBodyBones.LeftLittleIntermediate , + HumanBodyBones.LeftLittleDistal , + HumanBodyBones.RightThumbProximal , + HumanBodyBones.RightThumbIntermediate , + HumanBodyBones.RightThumbDistal , + HumanBodyBones.RightIndexProximal , + HumanBodyBones.RightIndexIntermediate , + HumanBodyBones.RightIndexDistal , + HumanBodyBones.RightMiddleProximal , + HumanBodyBones.RightMiddleIntermediate , + HumanBodyBones.RightMiddleDistal , + HumanBodyBones.RightRingProximal , + HumanBodyBones.RightRingIntermediate , + HumanBodyBones.RightRingDistal , + HumanBodyBones.RightLittleProximal , + HumanBodyBones.RightLittleIntermediate , + HumanBodyBones.RightLittleDistal , + HumanBodyBones.Chest , + HumanBodyBones.Spine , + HumanBodyBones.Hips , + HumanBodyBones.LeftUpperLeg , + HumanBodyBones.RightUpperLeg , + HumanBodyBones.LeftLowerLeg , + HumanBodyBones.RightLowerLeg , + HumanBodyBones.LeftFoot , + HumanBodyBones.RightFoot , + HumanBodyBones.LeftToes , + HumanBodyBones.RightToes + }; + + public VirtualAvatar(Transform boneParentTransform, MotionSource motionSource) + { + parent = boneParentTransform; + MotionSource = motionSource; + } + public (Transform cloneBone, Transform modelBone) GetBoneTransformPair(HumanBodyBones bone) + { + if (BoneTransformCache == null) return (null, null); + if (BoneTransformCache.ContainsKey(bone) == false) return (null, null); + return BoneTransformCache[bone]; + } + + public Transform GetCloneBoneTransform(HumanBodyBones bone) => GetBoneTransformPair(bone).cloneBone; + public Transform GetModelBoneTransform(HumanBodyBones bone) => GetBoneTransformPair(bone).modelBone; + + public void SetPoseChanged(HumanBodyBones bone) => isPoseChanged[bone] = true; + public bool GetPoseChanged(HumanBodyBones bone) => isPoseChanged.ContainsKey(bone) ? isPoseChanged[bone] : false; + + public void ImportAvatar(GameObject model) + { + if (avatar != null) + { + if(animator != null) GameObject.DestroyImmediate(animator); + if (RootTransform != null) GameObject.DestroyImmediate(RootTransform.gameObject); + // Destroy SkeletonRoot + foreach (Transform child in parent) + { + GameObject.DestroyImmediate(child.gameObject); + } + } + + currentModel = model; + + var (cloneAvatar, cloneRoot) = CreateCopyAvatar(model, parent); + avatar = cloneAvatar; + RootTransform = cloneRoot; + animator = parent.gameObject.AddComponent(); + animator.avatar = avatar; + + InitializeBoneTransformCache(true); + + //この時点のクローンボーンのローカルTRSがアバターのレスト(Tポーズ)基準 + CaptureBindPose(); + } + + private void CaptureBindPose() + { + bindPose = new List<(Transform, Vector3, Quaternion)>(); + if (RootTransform == null) return; + foreach (var t in RootTransform.GetComponentsInChildren(true)) + { + bindPose.Add((t, t.localPosition, t.localRotation)); + } + } + + /// + /// クローンスケルトンを生成時のバインドポーズ(アバターのTポーズ基準)に戻す + /// + public void RestoreBindPose() + { + if (bindPose == null) return; + foreach (var (t, localPosition, localRotation) in bindPose) + { + if (t == null) continue; + t.localPosition = localPosition; + t.localRotation = localRotation; + } + } + + private Dictionary InitializeBoneTransformCache(bool force = false) + { + if (boneTransformCache != null && boneTransformCache.Count != 0 && force == false) return boneTransformCache; + + if (currentModel == null) return null; + + if (boneTransformCache == null) + { + boneTransformCache = new Dictionary(); + isPoseChanged = new Dictionary(); + } + else + { + boneTransformCache.Clear(); + isPoseChanged.Clear(); + } + + var modelAnimator = currentModel.GetComponent(); + + if (modelAnimator == null) return null; + + boneTransformCache.Add(HumanBodyBonesRoot, (animator.transform, modelAnimator.transform)); + isPoseChanged.Add(HumanBodyBonesRoot, false); + + foreach (HumanBodyBones bone in ReverseBodyBones) + { + if (bone == HumanBodyBones.LastBone) continue; + + var cloneBone = animator.GetBoneTransform(bone); + if (cloneBone == null) continue; + + var modelBone = modelAnimator.GetBoneTransform(bone); + if (modelBone == null) continue; + + boneTransformCache.Add(bone, (cloneBone, modelBone)); + isPoseChanged.Add(bone, false); + } + return boneTransformCache.Count == 0 ? null : boneTransformCache; + } + + public void Recenter() + { + if (animator == null) return; + + var hipBone = animator.GetBoneTransform(HumanBodyBones.Hips); + CenterOffsetPosition = -new Vector3(hipBone.position.x, 0, hipBone.position.z); + CenterOffsetRotationY = -hipBone.rotation.eulerAngles.y; + } + + /// + /// 骨だけコピーしたAvatarを作成する + /// + /// コピー元モデル + /// コピー先の親 + /// + private (Avatar avatar, Transform root) CreateCopyAvatar(GameObject model, Transform parent) + { + var skeletonBones = new List(); + var humanBones = new List(); + var animator = model.GetComponent(); + + //同じボーン構造のスケルトンをクローンしてSkeletonBoneのマッピングをする + var root = animator.GetBoneTransform(HumanBodyBones.Hips).parent; + var rootClone = CloneTransform(root, parent); + CopySkeleton(root, rootClone, ref skeletonBones); + + //HumanBoneと実際のボーンの名称のマッピングをする + GetHumanBones(animator, ref humanBones); + + HumanDescription humanDescription = new HumanDescription + { + human = humanBones.ToArray(), + skeleton = skeletonBones.ToArray(), + upperArmTwist = 0.5f, + lowerArmTwist = 0.5f, + upperLegTwist = 0.5f, + lowerLegTwist = 0.5f, + armStretch = 0.05f, + legStretch = 0.05f, + feetSpacing = 0.0f, + hasTranslationDoF = false + }; + + var avatar = AvatarBuilder.BuildHumanAvatar(parent.gameObject, humanDescription); + + return (avatar, rootClone); + } + + private void GetHumanBones(Animator animator, ref List humanBones) + { + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) continue; + + var boneTransform = animator.GetBoneTransform(bone); + if (boneTransform == null) continue; + + var humanBone = new HumanBone() + { + humanName = HumanTrait.BoneName[(int)bone], + boneName = boneTransform.name, + }; + humanBone.limit.useDefaultValues = true; + + humanBones.Add(humanBone); + } + } + + private void CopySkeleton(Transform current, Transform cloneCurrent, ref List skeletons) + { + SkeletonBone skeletonBone = new SkeletonBone() + { + name = cloneCurrent.name, + position = cloneCurrent.localPosition, + rotation = cloneCurrent.localRotation, + scale = cloneCurrent.localScale, + }; + skeletons.Add(skeletonBone); + + foreach (Transform child in current) + { + var childClone = CloneTransform(child, cloneCurrent); + CopySkeleton(child, childClone, ref skeletons); + } + } + + private Transform CloneTransform(Transform source, Transform parent) + { + var clone = new GameObject(source.name).transform; + clone.parent = parent; + clone.localPosition = source.localPosition; + clone.localRotation = source.localRotation; + clone.localScale = source.localScale; + + return clone; + } + + public T AddComponent() where T : Component => parent.gameObject.AddComponent(); + public T GetComponent() where T : Component => parent.gameObject.GetComponent(); + } + public enum MotionSource + { + VRIK, + ExternalDevice, //外部デバイスプラグイン(mocopi等) + VMCProtocol, + MotionPlayback, //モーションファイル再生(最優先) + } +} diff --git a/Assets/VMCPlugin/VirtualAvatar.cs.meta b/Assets/VMCPlugin/VirtualAvatar.cs.meta new file mode 100644 index 00000000..774d65c2 --- /dev/null +++ b/Assets/VMCPlugin/VirtualAvatar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: caaec59c59b54834d8adf237647b4efe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRIKSpineOffset.cs b/Assets/VRIKSpineOffset.cs new file mode 100644 index 00000000..ae2cf146 --- /dev/null +++ b/Assets/VRIKSpineOffset.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using RootMotion.FinalIK; +using VMC; +using System; + +public class VRIKSpineOffset : MonoBehaviour +{ + public VRIK ik; + public Vector3 spineFix; + private Guid? eventId = null; + + private void OnEnable() + { + eventId = IKManager.Instance.AddOnPostUpdate(50, AfterVRIK); + } + + private void OnDisable() + { + if (eventId != null) IKManager.Instance.RemoveOnPostUpdate(eventId.Value); + } + + private void AfterVRIK() + { + if (IKManager.Instance.vrik == null) return; + Vector3 headPos = ik.references.head.position; + Quaternion headRot = ik.references.head.rotation; + + ik.references.spine.localRotation *= Quaternion.Euler(spineFix); + ik.references.chest.localRotation *= Quaternion.Euler(-spineFix); + + ik.references.chest.rotation = Quaternion.FromToRotation(ik.references.head.position - ik.references.chest.position, headPos - ik.references.chest.position) * ik.references.chest.rotation; + ik.references.head.rotation = headRot; + } +} diff --git a/Assets/VRIKSpineOffset.cs.meta b/Assets/VRIKSpineOffset.cs.meta new file mode 100644 index 00000000..b744462f --- /dev/null +++ b/Assets/VRIKSpineOffset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a300bb2fa6b87b4a995095bf24d5682 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRoidHubSetup.meta b/Assets/VRoidHubSetup.meta new file mode 100644 index 00000000..31406a4f --- /dev/null +++ b/Assets/VRoidHubSetup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 097d134020b1415419fc2e6420b37ddb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRoidHubSetup/Editor.meta b/Assets/VRoidHubSetup/Editor.meta new file mode 100644 index 00000000..66ec4bb6 --- /dev/null +++ b/Assets/VRoidHubSetup/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b539591465bb0144987661a0d0d90932 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef b/Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef similarity index 61% rename from Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef rename to Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef index 7ad4804b..58cc173d 100644 --- a/Assets/ExternalPlugins/DVRSDK/Editor/DVRSDKEditorScript.asmdef +++ b/Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef @@ -1,19 +1,16 @@ { - "name": "DVRSDKEditorScript", - "references": [ - "GUID:05dd262a0c0a2f841b8252c8c3815582" - ], + "name": "VMC.VRoidHubSetup.Editor", + "rootNamespace": "VMC.EditorTools", + "references": [], "includePlatforms": [ "Editor" ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, - "precompiledReferences": [ - "" - ], + "precompiledReferences": [], "autoReferenced": false, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef.meta b/Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef.meta new file mode 100644 index 00000000..e55d4d93 --- /dev/null +++ b/Assets/VRoidHubSetup/Editor/VMC.VRoidHubSetup.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2914f7d850b6a6046b97db1a89f35fc9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs b/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs new file mode 100644 index 00000000..3e0fe8f6 --- /dev/null +++ b/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs @@ -0,0 +1,52 @@ +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Build; +using UnityEngine; + +namespace VMC.EditorTools +{ + /// + /// VRoid SDK(Assets/VRoidSDK)の有無を検出し、スクリプト定義シンボル VMC_VROIDSDK を自動でON/OFFする。 + /// + /// - SDKあり → VMC_VROIDSDK 定義 → VRoidSDKConnector 等が有効 + /// - SDKなし → VMC_VROIDSDK 未定義 → SDK依存コードが除外されビルドが通る + /// + /// このアセンブリはSDLにもAssembly-CSharpにも依存しない独立Editorアセンブリ(VMC.VRoidSDKSetup.Editor.asmdef)に + /// 置いてあるため、Assembly-CSharpがSDK欠如で一時的にコンパイルエラーになっても本スクリプトは動作し、 + /// 定義を修正して自己修復できる。 + /// + [InitializeOnLoad] + public static class VRoidSDKDefineConfigurator + { + private const string Define = "VMC_VROIDSDK"; + // SDK同梱の目印となるDLL(再配布しないSDK本体の一部) + private const string MarkerRelativePath = "VRoidSDK/Bin/Pixiv.VroidSdk.dll"; + + static VRoidSDKDefineConfigurator() + { + var present = File.Exists(Path.Combine(Application.dataPath, MarkerRelativePath)); + // このプロジェクトはWindowsスタンドアロン。念のため現在選択中グループも合わせて更新する。 + Apply(NamedBuildTarget.Standalone, present); + var selected = EditorUserBuildSettings.selectedBuildTargetGroup; + if (selected != BuildTargetGroup.Standalone && selected != BuildTargetGroup.Unknown) + { + Apply(NamedBuildTarget.FromBuildTargetGroup(selected), present); + } + } + + private static void Apply(NamedBuildTarget target, bool present) + { + var defines = PlayerSettings.GetScriptingDefineSymbols(target) + .Split(';') + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToList(); + var has = defines.Contains(Define); + if (present == has) return; // 変更不要 + if (present) defines.Add(Define); + else defines.Remove(Define); + PlayerSettings.SetScriptingDefineSymbols(target, string.Join(";", defines)); + Debug.Log($"[VRoidSDK] {(present ? "detected" : "not found")}. Scripting define '{Define}' -> {(present ? "ON" : "OFF")} ({target.TargetName})."); + } + } +} diff --git a/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs.meta b/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs.meta new file mode 100644 index 00000000..d7ba7d6c --- /dev/null +++ b/Assets/VRoidHubSetup/Editor/VRoidSDKDefineConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba67de2af8c6ebb47b0c97c1390ed144 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR.meta b/Assets/XR.meta new file mode 100644 index 00000000..871e816c --- /dev/null +++ b/Assets/XR.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d43ff6aac6286a041b8775d93cc1966b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR/Loaders.meta b/Assets/XR/Loaders.meta new file mode 100644 index 00000000..46fa19ea --- /dev/null +++ b/Assets/XR/Loaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 51197c2bc31cb7546bb7eef8fc3e2f9a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR/Loaders/OpenVRLoader.asset b/Assets/XR/Loaders/OpenVRLoader.asset new file mode 100644 index 00000000..b575e9ec --- /dev/null +++ b/Assets/XR/Loaders/OpenVRLoader.asset @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5685c6ec3d77efd409c20e6da27a4c72, type: 3} + m_Name: OpenVRLoader + m_EditorClassIdentifier: diff --git a/Assets/XR/Loaders/OpenVRLoader.asset.meta b/Assets/XR/Loaders/OpenVRLoader.asset.meta new file mode 100644 index 00000000..b8622bc0 --- /dev/null +++ b/Assets/XR/Loaders/OpenVRLoader.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c59799aa08d517441b1ac4c64c7c94c3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR/Settings.meta b/Assets/XR/Settings.meta new file mode 100644 index 00000000..f443a4a0 --- /dev/null +++ b/Assets/XR/Settings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ad85b0aee274af4692a4dab7620d225 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR/Settings/OpenVRSettings.asset b/Assets/XR/Settings/OpenVRSettings.asset new file mode 100644 index 00000000..bda1c4b9 --- /dev/null +++ b/Assets/XR/Settings/OpenVRSettings.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7a32bfe54d957ec4581fa4a630f1647a, type: 3} + m_Name: OpenVRSettings + m_EditorClassIdentifier: + PromptToUpgradePackage: 1 + PromptToUpgradePreviewPackages: 1 + SkipPromptForVersion: + StereoRenderingMode: 1 + InitializationType: 2 + EditorAppKey: application.generated.unity.virtualmotioncapture.exe + ActionManifestFileRelativeFilePath: StreamingAssets\SteamVR\actions.json + MirrorView: 2 + HasCopiedDefaults: 0 diff --git a/Assets/XR/Settings/OpenVRSettings.asset.meta b/Assets/XR/Settings/OpenVRSettings.asset.meta new file mode 100644 index 00000000..ded1ecb1 --- /dev/null +++ b/Assets/XR/Settings/OpenVRSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f97434f9e28810478ecfb6e303827ae +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/XR/XRGeneralSettingsPerBuildTarget.asset b/Assets/XR/XRGeneralSettingsPerBuildTarget.asset new file mode 100644 index 00000000..4646e966 --- /dev/null +++ b/Assets/XR/XRGeneralSettingsPerBuildTarget.asset @@ -0,0 +1,47 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-9124541562382421390 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d236b7d11115f2143951f1e14045df39, type: 3} + m_Name: Standalone Settings + m_EditorClassIdentifier: + m_LoaderManagerInstance: {fileID: -5625074774028346209} + m_InitManagerOnStart: 0 +--- !u!114 &-5625074774028346209 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4c3631f5e58749a59194e0cf6baf6d5, type: 3} + m_Name: Standalone Providers + m_EditorClassIdentifier: + m_RequiresSettingsUpdate: 0 + m_AutomaticLoading: 0 + m_AutomaticRunning: 0 + m_Loaders: [] +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d2dc886499c26824283350fa532d087d, type: 3} + m_Name: XRGeneralSettingsPerBuildTarget + m_EditorClassIdentifier: + Keys: 01000000 + Values: + - {fileID: -9124541562382421390} diff --git a/Assets/XR/XRGeneralSettingsPerBuildTarget.asset.meta b/Assets/XR/XRGeneralSettingsPerBuildTarget.asset.meta new file mode 100644 index 00000000..e5115b0e --- /dev/null +++ b/Assets/XR/XRGeneralSettingsPerBuildTarget.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 894c3efeefc551f478fe6b611508aa41 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter32bit.dll b/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter32bit.dll index f84d206d..9cacdb0a 100644 Binary files a/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter32bit.dll and b/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter32bit.dll differ diff --git a/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter64bit.dll b/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter64bit.dll index f14e5427..53e8f8cc 100644 Binary files a/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter64bit.dll and b/BuildRootFiles/ControlPanel/VMC_Camera/VMC_CameraFilter64bit.dll differ diff --git a/BuildRootFiles/LICENSE b/BuildRootFiles/LICENSE index 1a05c6a0..a71fe343 100644 --- a/BuildRootFiles/LICENSE +++ b/BuildRootFiles/LICENSE @@ -337,3 +337,69 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +EasyDeviceDiscoveryProtocol + +MIT License + +Copyright (c) 2020 gpsnmeajp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Minis + +Minis - MIDI input plugin for the Unity Input System +Copyright (c) Keijiro Takahashi + +The Minis source code is released into the public domain. +Minis bundles the RtMidi library ("RtMidi for Unity"), which is distributed +under the following license. + +RtMidi + +RtMidi: realtime MIDI i/o C++ classes +Copyright (c) 2003-2023 Gary P. Scavone + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Any person wishing to distribute modifications to the Software is +asked to send the modifications to the original developer so that +they can be incorporated into the canonical version. This is, +however, not a binding provision of this license. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/BuildRootFiles/RemoveZoneID.bat b/BuildRootFiles/RemoveZoneID.bat new file mode 100644 index 00000000..aac1de00 --- /dev/null +++ b/BuildRootFiles/RemoveZoneID.bat @@ -0,0 +1 @@ +powershell -Command "Get-ChildItem -Recurse -File | Unblock-File" \ No newline at end of file diff --git a/BuildRootFiles/VirtualMotionCapture.bat b/BuildRootFiles/VirtualMotionCapture.bat new file mode 100644 index 00000000..50a50a26 --- /dev/null +++ b/BuildRootFiles/VirtualMotionCapture.bat @@ -0,0 +1,15 @@ +@echo off + +echo Running VirtualMotionCapture... / o[`[VLv`[NEEE + +set pipeName=VMCpipe%RANDOM%%RANDOM% + +start VirtualMotionCapture.exe /pipeName %pipeName% + +echo Waiting... + +timeout /t 5 > nul + +echo Running ControlPanel... / Rg[plNEEE + +start ControlPanel\VirtualMotionCaptureControlPanel.exe /pipeName %pipeName% \ No newline at end of file diff --git a/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/XRSDKOpenVR.dll b/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/XRSDKOpenVR.dll new file mode 100644 index 00000000..1bda158f Binary files /dev/null and b/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/XRSDKOpenVR.dll differ diff --git a/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/openvr_api.dll b/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/openvr_api.dll new file mode 100644 index 00000000..ef336cba Binary files /dev/null and b/BuildRootFiles/VirtualMotionCapture_Data/Plugins/x86_64/openvr_api.dll differ diff --git a/BuildRootFiles/default.json b/BuildRootFiles/default.json index f6f73c76..e614c3e8 100644 --- a/BuildRootFiles/default.json +++ b/BuildRootFiles/default.json @@ -3,7 +3,7 @@ "AAA_1":" Virtual Motion Capture Setting File", "AAA_2":" See more : vmc.info", "AAA_3":"========================================", - "AAA_SavedVersion":"v0.50", + "AAA_SavedVersion":"v0.59", "AntiAliasing":4, "AutoBlinkEnable":true, "BackCameraLookTargetSettings":null, @@ -19,6 +19,10 @@ "CameraMirrorEnable":false, "CameraSmooth":0, "CameraType":1, + "Chest":{ + "m_Item1":0, + "m_Item2":null + }, "CloseAnimationTime":0.06, "ClosingTime":0.1, "CustomBackgroundColor":{ @@ -28,12 +32,20 @@ "r":0.68235296 }, "DefaultFace":"通常(NEUTRAL)", - "DeleteHairNormalMap":true, - "EnableNormalMapFix":true, + "EnableOverrideBodyHeight":false, "EnableSkeletal":true, "ExternalBonesReceiverEnable":false, + "ExternalMotionReceiverDelayMsList":[ + 0,0 + ], "ExternalMotionReceiverEnable":false, + "ExternalMotionReceiverEnableList":[ + false,false + ], "ExternalMotionReceiverPort":39540, + "ExternalMotionReceiverPortList":[ + 39540,39541 + ], "ExternalMotionReceiverRequesterEnable":true, "ExternalMotionSenderAddress":"127.0.0.1", "ExternalMotionSenderEnable":false, @@ -59,6 +71,7 @@ "EyeTracking_ViveProEyeScaleHorizontal":2, "EyeTracking_ViveProEyeScaleVertical":1.5, "EyeTracking_ViveProEyeUseEyelidMovements":false, + "FixElbowRotation":true, "FixKneeRotation":true, "FreeCameraTransform":{ "localPosition":{ @@ -614,6 +627,7 @@ ], "MidiEnable":false, "OpenAnimationTime":0.03, + "OverrideBodyHeight":1.7, "PPS_Bloom_Color_a":1, "PPS_Bloom_Color_b":1, "PPS_Bloom_Color_g":1, @@ -651,6 +665,8 @@ "m_Item1":3, "m_Item2":null }, + "PelvisOffsetAdjustY":0, + "PelvisOffsetAdjustZ":0, "PositionFixedCameraTransform":{ "localPosition":{ "x":-2.82886076, @@ -747,6 +763,81 @@ "TrackingFilterHmdEnable":true, "TrackingFilterTrackerEnable":true, "TurnOffAmbientLight":false, + "VMCProtocolReceiverSettingsList":[ + { + "ApplyBlendShape":true, + "ApplyCamera":true, + "ApplyChest":true, + "ApplyControl":true, + "ApplyControllerInput":true, + "ApplyEye":true, + "ApplyHead":true, + "ApplyKeyboardInput":false, + "ApplyLeftArm":true, + "ApplyLeftFinger":true, + "ApplyLeftFoot":true, + "ApplyLeftHand":true, + "ApplyLeftLeg":true, + "ApplyLight":true, + "ApplyLookAt":true, + "ApplyMidi":true, + "ApplyRightArm":true, + "ApplyRightFinger":true, + "ApplyRightFoot":true, + "ApplyRightHand":true, + "ApplyRightLeg":true, + "ApplyRootPosition":true, + "ApplyRootRotation":true, + "ApplySetting":true, + "ApplySpine":true, + "ApplyStatus":true, + "ApplyTracker":true, + "CorrectHipBone":false, + "DelayMs":0, + "Enable":false, + "FixHandBone":true, + "IgnoreDefaultBone":true, + "Name":"Receiver 1", + "Port":39540, + "UseBonePosition":false + },{ + "ApplyBlendShape":true, + "ApplyCamera":true, + "ApplyChest":true, + "ApplyControl":true, + "ApplyControllerInput":true, + "ApplyEye":true, + "ApplyHead":true, + "ApplyKeyboardInput":false, + "ApplyLeftArm":true, + "ApplyLeftFinger":true, + "ApplyLeftFoot":true, + "ApplyLeftHand":true, + "ApplyLeftLeg":true, + "ApplyLight":true, + "ApplyLookAt":true, + "ApplyMidi":true, + "ApplyRightArm":true, + "ApplyRightFinger":true, + "ApplyRightFoot":true, + "ApplyRightHand":true, + "ApplyRightLeg":true, + "ApplyRootPosition":true, + "ApplyRootRotation":true, + "ApplySetting":true, + "ApplySpine":true, + "ApplyStatus":true, + "ApplyTracker":true, + "CorrectHipBone":false, + "DelayMs":0, + "Enable":false, + "FixHandBone":true, + "IgnoreDefaultBone":true, + "Name":"Receiver 2", + "Port":39541, + "UseBonePosition":false + } + ], "VRMPath":null, "VirtualMotionTrackerEnable":false, "VirtualMotionTrackerNo":50, @@ -756,6 +847,7 @@ "WebCamResize":false, "WindowClickThrough":false, "bodyTracker":null, + "chestTracker":null, "headTracker":{ "localPosition":{ "x":-0.362957, @@ -837,6 +929,22 @@ } }, "leftKneeTracker":null, + "mocopi_ApplyChest":true, + "mocopi_ApplyHead":true, + "mocopi_ApplyLeftArm":true, + "mocopi_ApplyLeftFoot":true, + "mocopi_ApplyLeftHand":true, + "mocopi_ApplyLeftLeg":true, + "mocopi_ApplyRightArm":true, + "mocopi_ApplyRightFoot":true, + "mocopi_ApplyRightHand":true, + "mocopi_ApplyRightLeg":true, + "mocopi_ApplyRootPosition":true, + "mocopi_ApplyRootRotation":true, + "mocopi_ApplySpine":true, + "mocopi_CorrectHipBone":false, + "mocopi_Enable":true, + "mocopi_Port":12351, "rightElbowTracker":null, "rightFootTracker":null, "rightHandTracker":{ diff --git "a/BuildRootFiles/mocopi\343\201\256\343\201\244\343\201\213\343\201\204\343\201\213\343\201\237.url" "b/BuildRootFiles/mocopi\343\201\256\343\201\244\343\201\213\343\201\204\343\201\213\343\201\237.url" new file mode 100644 index 00000000..9dbbd82e --- /dev/null +++ "b/BuildRootFiles/mocopi\343\201\256\343\201\244\343\201\213\343\201\204\343\201\213\343\201\237.url" @@ -0,0 +1,2 @@ +[InternetShortcut] +URL=https://vmc.info/manual/mocopi%E3%81%AE%E3%81%A4%E3%81%8B%E3%81%84%E3%81%8B%E3%81%9F.html diff --git "a/BuildRootFiles/\350\265\267\345\213\225\343\201\227\343\201\252\343\201\204\346\231\202\343\201\257(If VMC does not start).txt" "b/BuildRootFiles/\350\265\267\345\213\225\343\201\227\343\201\252\343\201\204\346\231\202\343\201\257(If VMC does not start).txt" new file mode 100644 index 00000000..bb901e22 --- /dev/null +++ "b/BuildRootFiles/\350\265\267\345\213\225\343\201\227\343\201\252\343\201\204\346\231\202\343\201\257(If VMC does not start).txt" @@ -0,0 +1,34 @@ +バーチャルモーションキャプチャーのコントロールパネルが起動しない場合、 +WindowsのSmartScreenによってブロックされています。 + +1. まずはRemoveZoneID.batをダブルクリックしてから、VirtualMotionCapture.exeを再度起動するか試してください。 +2. それでも起動しない場合はVirtualMotionCapture.batをダブルクリックして起動するか試してください。 +3. どちらでも起動しない場合は、Windowsのスタートメニューの設定で「評価ベースの保護」と検索し、アプリとファイルの確認をオフにしてから、VirtualMotionCapture.exeを再度起動するか試してください。 + +--- + +If the Virtual Motion Capture control panel does not start up, +it is being blocked by Windows SmartScreen. + +1. First, try double-clicking RemoveZoneID.bat and then restart VirtualMotionCapture.exe. +2. If it still doesn't start, try double-clicking VirtualMotionCapture.bat to launch it. +3. If neither method works, search for "reputation-based protection" in Windows Start menu settings, turn off "Check apps and files", and then try restarting VirtualMotionCapture.exe. + +--- + +버추얼 모션 캡처의 컨트롤 패널이 시작되지 않는 경우, +Windows SmartScreen에 의해 차단되었습니다. + +1. 먼저 RemoveZoneID.bat를 더블클릭한 후, VirtualMotionCapture.exe를 다시 시작해보세요. +2. 그래도 시작되지 않으면 VirtualMotionCapture.bat를 더블클릭하여 시작해보세요. +3. 둘 다 시작되지 않으면, Windows 시작 메뉴의 설정에서 "평판 기반 보호"를 검색하고, 앱 및 파일 확인을 끈 다음 VirtualMotionCapture.exe를 다시 시작해보세요. + +--- + +如果Virtual Motion Capture控制面板无法启动, +这是被Windows SmartScreen阻止了。 + +1. 首先双击RemoveZoneID.bat,然后尝试重新启动VirtualMotionCapture.exe。 +2. 如果仍然无法启动,请尝试双击VirtualMotionCapture.bat来启动。 +3. 如果两种方法都无法启动,请在Windows开始菜单的设置中搜索"基于声誉的保护",关闭"检查应用和文件",然后尝试重新启动VirtualMotionCapture.exe。 + diff --git a/ColorPickerWPF b/ColorPickerWPF index c6cd9113..78ed34a0 160000 --- a/ColorPickerWPF +++ b/ColorPickerWPF @@ -1 +1 @@ -Subproject commit c6cd9113069cb805a7d37cd43cbc4341d5a834fb +Subproject commit 78ed34a022aeb0ad7a24871beef7c7ea34d461c7 diff --git a/ControlWindowWPF/ControlWindowWPF.sln b/ControlWindowWPF/ControlWindowWPF.sln index ec886006..f6d8b031 100644 --- a/ControlWindowWPF/ControlWindowWPF.sln +++ b/ControlWindowWPF/ControlWindowWPF.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityMemoryMappedFile", ".. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "akr.WPF.Controls.ColorPicker", "..\ColorPickerWPF\akr.WPF.Controls.ColorPicker\akr.WPF.Controls.ColorPicker.csproj", "{214EBE94-3AC3-4260-8664-35D803139E89}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VMC.ControlPanel.PluginAPI", "VMC.ControlPanel.PluginAPI\VMC.ControlPanel.PluginAPI.csproj", "{8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution BETA|Any CPU = BETA|Any CPU @@ -48,6 +50,16 @@ Global {214EBE94-3AC3-4260-8664-35D803139E89}.FREE|Any CPU.Build.0 = Debug|Any CPU {214EBE94-3AC3-4260-8664-35D803139E89}.Release|Any CPU.ActiveCfg = Release|Any CPU {214EBE94-3AC3-4260-8664-35D803139E89}.Release|Any CPU.Build.0 = Release|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.BETA|Any CPU.ActiveCfg = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.BETA|Any CPU.Build.0 = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.FANBOX|Any CPU.ActiveCfg = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.FANBOX|Any CPU.Build.0 = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.FREE|Any CPU.ActiveCfg = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.FREE|Any CPU.Build.0 = Debug|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B1E4D2A-5F63-4C71-9A02-1D5E7C8B4A10}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ControlWindowWPF/ControlWindowWPF/AdminExecute.cs b/ControlWindowWPF/ControlWindowWPF/AdminExecute.cs new file mode 100644 index 00000000..6e0c8083 --- /dev/null +++ b/ControlWindowWPF/ControlWindowWPF/AdminExecute.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace VirtualMotionCaptureControlPanel +{ + /// + /// 管理者権限の管理をします + /// + public static class AdminExecute + { + /// + /// 現在管理者権限で実行中か取得 + /// + public static bool IsAdmin => (new WindowsPrincipal(WindowsIdentity.GetCurrent())).IsInRole(WindowsBuiltInRole.Administrator); + + /// + /// 自身を管理者権限で実行します + /// + /// 実行時引数 + /// 実行結果 + public static (bool successExecute, int exitCode) RestartAsAdmin(string[] args) + { + var startInfo = new ProcessStartInfo(Assembly.GetEntryAssembly().Location, CreateArgs(args)) + { + UseShellExecute = true, + Verb = "runas", + }; + + try + { + // 管理者権限で実行 + var process = Process.Start(startInfo); + process.WaitForExit(); + return (true, process.ExitCode); + } + catch (Win32Exception ex) + { + // UACダイアログに"いいえ"を選択すると例外 + Console.WriteLine(ex.Message); + } + return (false, -1); + } + + /// + /// 実行時引数様にスペースエスケープします + /// + /// 実行時引数 + /// 変換結果 + private static string CreateArgs(string[] args) + { + return string.Join(" ", args.Select(s => s.Contains(" ") ? $"\"{s}\"" : s)); + } + } +} diff --git a/ControlWindowWPF/ControlWindowWPF/App.config b/ControlWindowWPF/ControlWindowWPF/App.config index 787dcbec..1b2ed0af 100644 --- a/ControlWindowWPF/ControlWindowWPF/App.config +++ b/ControlWindowWPF/ControlWindowWPF/App.config @@ -1,6 +1,16 @@  - + + + + + \ No newline at end of file diff --git a/ControlWindowWPF/ControlWindowWPF/App.xaml.cs b/ControlWindowWPF/ControlWindowWPF/App.xaml.cs index a36e753c..6d8b03bc 100644 --- a/ControlWindowWPF/ControlWindowWPF/App.xaml.cs +++ b/ControlWindowWPF/ControlWindowWPF/App.xaml.cs @@ -17,6 +17,8 @@ public partial class App : Application private void Application_Startup(object sender, StartupEventArgs e) { if (e.Args.Length == 0) return; CommandLineArgs = e.Args; + //プラグインのリソース辞書を言語切り替えより先に登録しておく + ControlPanelPluginManager.Load(); LanguageSelector.SetAutoLanguage(); } } diff --git a/ControlWindowWPF/ControlWindowWPF/CalibrationResultWindow.xaml b/ControlWindowWPF/ControlWindowWPF/CalibrationResultWindow.xaml new file mode 100644 index 00000000..27caec04 --- /dev/null +++ b/ControlWindowWPF/ControlWindowWPF/CalibrationResultWindow.xaml @@ -0,0 +1,21 @@ + + + + + + + + Detail Message + + + +