// Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. using Meta.XR.Movement.FaceTracking.Samples; using Object = UnityEngine.Object; using System; using System.IO; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif using UnityEngine; using UnityEngine.Assertions; using static Meta.XR.Movement.FaceTracking.Samples.FaceDriver; namespace Oculus.Movement.Utils { /// /// Has functions that allow adding components via the editor or runtime. /// These functions detect if they are being called in the editor and if so, /// affect the undo state. /// public class AddComponentsHelper { /// /// Prefix for movement samples one-click menus. /// public const string _MOVEMENT_SAMPLES_MENU = "GameObject/Movement SDK/"; /// /// Sets up a character for A2E face tracking. /// /// GameObject to add to. /// Allow duplicates mapping or not. /// If activated from runtime code. We want to possibly /// support one-click during playmode, so we can't necessarily use Application.isPlaying. public static void SetUpCharacterForA2EFace( GameObject gameObject, bool allowDuplicates = true, bool runtimeInvocation = false) { try { ValidateChildGameObjectsForFaceMapping(gameObject); } catch (InvalidOperationException e) { #if UNITY_EDITOR if (runtimeInvocation) { Debug.LogWarning($"Face tracking setup error: {e.Message}"); } else { EditorUtility.DisplayDialog("Face Tracking setup error.", e.Message, "Ok"); } #else Debug.LogWarning($"Face tracking setup error: {e.Message}"); #endif return; } // Get reference for source weights provider. var ovrWeightsProvider = EnsureProperOVRWeightsProvider(runtimeInvocation); // Get reference for retargeter component. var faceRetargeter = EnsureFaceRetargeterComponent( gameObject, ovrWeightsProvider, runtimeInvocation, "retargeting_vr_to_aura_a2e_v9.json"); // ensure final component. EnsureFaceDriverComponent(gameObject, faceRetargeter, runtimeInvocation, RigType.XRTech); } /// /// Sets up an ARKit character for A2E face tracking. /// /// GameObject to add to. /// Allow duplicates mapping or not. /// If activated from runtime code. We want to possibly /// support one-click during playmode, so we can't necessarily use Application.isPlaying. public static void SetUpCharacterForA2EARKitFace(GameObject gameObject, bool allowDuplicates = true, bool runtimeInvocation = false) { try { AddComponentsHelper.ValidateChildGameObjectsForFaceMapping(gameObject); } catch (InvalidOperationException e) { #if UNITY_EDITOR if (runtimeInvocation) { Debug.LogWarning($"Face tracking setup error: {e.Message}"); } else { EditorUtility.DisplayDialog("Face Tracking setup error.", e.Message, "Ok"); } #else Debug.LogWarning($"Face tracking setup error: {e.Message}"); #endif return; } // Get reference for source weights provider. var ovrWeightsProvider = EnsureProperOVRWeightsProvider(runtimeInvocation); // Get reference for retargeter component. var faceRetargeter = EnsureFaceRetargeterComponent( gameObject, ovrWeightsProvider, runtimeInvocation, "arkit_retarget_a2e_v10.json"); // ensure final component. EnsureFaceDriverComponent(gameObject, faceRetargeter, runtimeInvocation, RigType.Simple); } private static OVRWeightsProvider EnsureProperOVRWeightsProvider(bool runtimeInvocation) { OVRWeightsProvider ovrWeightsProvider = GameObject.FindFirstObjectByType(); if (ovrWeightsProvider == null) { GameObject ovrExpressionsProviderObject = new GameObject("OVRExpressionsProvider"); ovrWeightsProvider = ovrExpressionsProviderObject.AddComponent(); #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RegisterCreatedObjectUndo(ovrExpressionsProviderObject, "Create OVRExpressionsProvider object"); Undo.RegisterCompleteObjectUndo(ovrExpressionsProviderObject, "OVRExpressionsProvider init"); } #endif } if (ovrWeightsProvider.OVRFaceExpressionComp == null) { OVRFaceExpressions ovrFaceExpressions = GameObject.FindFirstObjectByType(); if (ovrFaceExpressions == null) { ovrFaceExpressions = ovrWeightsProvider.gameObject.AddComponent(); #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RegisterCreatedObjectUndo(ovrFaceExpressions, "Add OVRFaceExpressions"); } #endif } ovrWeightsProvider.OVRFaceExpressionComp = ovrFaceExpressions; #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(ovrWeightsProvider, "Assign to OVRFaceExpressionComp field"); } #endif } return ovrWeightsProvider; } private static FaceRetargeterComponent EnsureFaceRetargeterComponent( GameObject parentObject, OVRWeightsProvider ovrWeightsProvider, bool runtimeInvocation, string presetName) { FaceRetargeterComponent retargeterComponent = parentObject.GetComponentInChildren(); if (retargeterComponent == null) { GameObject retargeterObject = new GameObject("FaceRetargeter"); retargeterComponent = retargeterObject.AddComponent(); #if UNITY_EDITOR if (runtimeInvocation) { retargeterObject.transform.SetParent(parentObject.transform); } else { Undo.RegisterCreatedObjectUndo(retargeterObject, "Create RetargeterComponent object"); Undo.SetTransformParent(retargeterObject.transform, parentObject.transform, "Add face retargeter component to parent"); Undo.RegisterCompleteObjectUndo(retargeterObject, "RetargeterComponent init"); } #else retargeterObject.transform.SetParent(parentObject.transform); #endif } if (retargeterComponent.RetargeterConfig == null) { // This code can only be done via the editor. At runtime the user // will have to man #if UNITY_EDITOR var packagesPath = Path.Combine(new string[] { "Packages", "com.meta.xr.sdk.movement", "Shared", "Data", "TrackingPresets"}); var presetPath = Path.Combine(packagesPath, presetName); var textAsset = AssetDatabase.LoadAssetAtPath(presetPath, typeof(TextAsset)); Assert.IsNotNull(textAsset, "Config text asset should exist."); retargeterComponent.RetargeterConfig = textAsset as TextAsset; #endif #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(retargeterComponent, "Assign text asset field"); } #endif } if (retargeterComponent.WeightsProvider == null) { retargeterComponent.WeightsProvider = ovrWeightsProvider; #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(ovrWeightsProvider, "Assign source weights provider"); } #endif } return retargeterComponent; } public static void EnsureFaceDriverComponent( GameObject parentObject, FaceRetargeterComponent faceRetargeter, bool runtimeInvocation, RigType rigType) { FaceDriver faceDriver = parentObject.GetComponent(); if (faceDriver == null) { faceDriver = parentObject.GetComponent(); if (faceDriver == null) { faceDriver = parentObject.AddComponent(); #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RegisterCreatedObjectUndo(faceDriver, "Add FaceDriver"); } #endif } } if (faceDriver.Meshes == null || faceDriver.Meshes.Length == 0) { var skinnedMeshRenderers = parentObject.GetComponentsInChildren(); List blendshapeMeshRenders = new List(); foreach (var mesh in skinnedMeshRenderers) { if (mesh.sharedMesh.blendShapeCount > 0) { blendshapeMeshRenders.Add(mesh); } } faceDriver.Meshes = blendshapeMeshRenders.ToArray(); #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(faceDriver, "Assign skinned mesh renderers"); } #endif } faceDriver.WeightsProvider = faceRetargeter; #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(faceDriver, "Assign face retargeter as weights provider"); } #endif faceDriver.RigTypeValue = rigType; #if UNITY_EDITOR if (!runtimeInvocation) { Undo.RecordObject(faceDriver, "Assign rig type to facedriver"); } #endif } public static void ValidateChildGameObjectsForFaceMapping(GameObject gameObject) { var childRenderers = gameObject.GetComponentsInChildren(); if (childRenderers == null || childRenderers.Length == 0) { throw new InvalidOperationException( $"Cannot add FaceTracking components to a GameObject that does not contain " + $"{nameof(SkinnedMeshRenderer)}s"); } bool foundBlendshapes = false; foreach (var childRenderer in childRenderers) { if (childRenderer.sharedMesh != null && childRenderer.sharedMesh.blendShapeCount > 0) { foundBlendshapes = true; } } if (!foundBlendshapes) { throw new InvalidOperationException( $"Adding a FaceTracking component requires a {nameof(SkinnedMeshRenderer)} " + $"that contains blendshapes."); } } } }