using UnityEngine; using System.Collections; using System; using Oculus.Avatar; using System.Runtime.InteropServices; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif #if AVATAR_INTERNAL using UnityEngine.Events; #endif [System.Serializable] public class AvatarLayer { public int layerIndex; } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(AvatarLayer))] public class AvatarLayerPropertyDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, GUIContent.none, property); SerializedProperty layerIndex = property.FindPropertyRelative("layerIndex"); position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); layerIndex.intValue = EditorGUI.LayerField(position, layerIndex.intValue); EditorGUI.EndProperty(); } } #endif [System.Serializable] public class PacketRecordSettings { internal bool RecordingFrames = false; public float UpdateRate = 1f / 30f; // 30 hz update of packets internal float AccumulatedTime; }; public class OvrAvatar : MonoBehaviour { [Header("Avatar")] public IntPtr sdkAvatar = IntPtr.Zero; public string oculusUserID; public OvrAvatarDriver Driver; [Header("Capabilities")] public bool EnableBody = true; public bool EnableHands = true; public bool EnableBase = true; public bool EnableExpressive = false; [Header("Network")] public bool RecordPackets; public bool UseSDKPackets = true; public PacketRecordSettings PacketSettings = new PacketRecordSettings(); [Header("Visibility")] public bool StartWithControllers; public AvatarLayer FirstPersonLayer; public AvatarLayer ThirdPersonLayer; public bool ShowFirstPerson = true; public bool ShowThirdPerson; internal ovrAvatarCapabilities Capabilities = ovrAvatarCapabilities.Body; [Header("Performance")] #if UNITY_ANDROID [Tooltip( "LOD mesh complexity and texture resolution. Highest LOD recommended on PC and simple mobile apps." + " Medium LOD recommended on mobile devices or for background characters on PC." + " Lowest LOD recommended for background characters on mobile.")] [SerializeField] internal ovrAvatarAssetLevelOfDetail LevelOfDetail = ovrAvatarAssetLevelOfDetail.Medium; #else [SerializeField] internal ovrAvatarAssetLevelOfDetail LevelOfDetail = ovrAvatarAssetLevelOfDetail.Highest; #endif #if UNITY_ANDROID && UNITY_5_5_OR_NEWER [Tooltip( "Enable to use combined meshes to reduce draw calls. Currently only available on mobile devices. " + "Will be forced to false on PC.")] private bool CombineMeshes = true; #else private bool CombineMeshes = false; #endif [Tooltip( "Enable to use transparent queue, disable to use geometry queue. Requires restart to take effect.")] public bool UseTransparentRenderQueue = true; [Header("Shaders")] public Shader Monochrome_SurfaceShader; public Shader Monochrome_SurfaceShader_SelfOccluding; public Shader Monochrome_SurfaceShader_PBS; public Shader Skinshaded_SurfaceShader_SingleComponent; public Shader Skinshaded_VertFrag_SingleComponent; public Shader Skinshaded_VertFrag_CombinedMesh; public Shader Skinshaded_Expressive_SurfaceShader_SingleComponent; public Shader Skinshaded_Expressive_VertFrag_SingleComponent; public Shader Skinshaded_Expressive_VertFrag_CombinedMesh; public Shader Loader_VertFrag_CombinedMesh; public Shader EyeLens; public Shader ControllerShader; [Header("Other")] public bool CanOwnMicrophone = true; [Tooltip( "Enable laughter detection and animation as part of OVRLipSync.")] public bool EnableLaughter = true; public GameObject MouthAnchor; public Transform LeftHandCustomPose; public Transform RightHandCustomPose; // Avatar asset private HashSet assetLoadingIds = new HashSet(); private bool assetsFinishedLoading = false; // Material manager private OvrAvatarMaterialManager materialManager; private bool waitingForCombinedMesh = false; // Global expressive system initialization private static bool doneExpressiveGlobalInit = false; // Clothing offsets private Vector4 clothingAlphaOffset = new Vector4(0f, 0f, 0f, 1f); private UInt64 clothingAlphaTexture = 0; // Lipsync private OVRLipSyncMicInput micInput = null; private OVRLipSyncContext lipsyncContext = null; private OVRLipSync.Frame currentFrame = new OVRLipSync.Frame(); private float[] visemes = new float[VISEME_COUNT]; private AudioSource audioSource; private ONSPAudioSource spatializedSource; private List voiceUpdates = new List(); private static ovrAvatarVisemes RuntimeVisemes; // Custom hand poses private Transform cachedLeftHandCustomPose; private Transform[] cachedCustomLeftHandJoints; private ovrAvatarTransform[] cachedLeftHandTransforms; private Transform cachedRightHandCustomPose; private Transform[] cachedCustomRightHandJoints; private ovrAvatarTransform[] cachedRightHandTransforms; private bool showLeftController; private bool showRightController; // Consts #if UNITY_ANDROID private const bool USE_MOBILE_TEXTURE_FORMAT = true; #else private const bool USE_MOBILE_TEXTURE_FORMAT = false; #endif private static readonly Vector3 MOUTH_HEAD_OFFSET = new Vector3(0, -0.085f, 0.09f); private const string MOUTH_HELPER_NAME = "MouthAnchor"; // Initial 'silence' score, 14 viseme scores and 1 laughter score as last element private const int VISEME_COUNT = 16; // Lipsync animation speeds private const float ACTION_UNIT_ONSET_SPEED = 30f; private const float ACTION_UNIT_FALLOFF_SPEED = 20f; private const float VISEME_LEVEL_MULTIPLIER = 1.5f; // Internals internal UInt64 oculusUserIDInternal; internal OvrAvatarBase Base = null; internal OvrAvatarTouchController ControllerLeft = null; internal OvrAvatarTouchController ControllerRight = null; internal OvrAvatarBody Body = null; internal OvrAvatarHand HandLeft = null; internal OvrAvatarHand HandRight = null; internal ovrAvatarLookAndFeelVersion LookAndFeelVersion = ovrAvatarLookAndFeelVersion.Two; internal ovrAvatarLookAndFeelVersion FallbackLookAndFeelVersion = ovrAvatarLookAndFeelVersion.Two; #if AVATAR_INTERNAL public AvatarControllerBlend BlendController; public UnityEvent AssetsDoneLoading = new UnityEvent(); #endif // Avatar packets public class PacketEventArgs : EventArgs { public readonly OvrAvatarPacket Packet; public PacketEventArgs(OvrAvatarPacket packet) { Packet = packet; } } private OvrAvatarPacket CurrentUnityPacket; public EventHandler PacketRecorded; public enum HandType { Right, Left, Max }; public enum HandJoint { HandBase, IndexBase, IndexTip, ThumbBase, ThumbTip, Max, } private static string[,] HandJoints = new string[(int)HandType.Max, (int)HandJoint.Max] { { "hands:r_hand_world", "hands:r_hand_world/hands:b_r_hand/hands:b_r_index1", "hands:r_hand_world/hands:b_r_hand/hands:b_r_index1/hands:b_r_index2/hands:b_r_index3/hands:b_r_index_ignore", "hands:r_hand_world/hands:b_r_hand/hands:b_r_thumb1/hands:b_r_thumb2", "hands:r_hand_world/hands:b_r_hand/hands:b_r_thumb1/hands:b_r_thumb2/hands:b_r_thumb3/hands:b_r_thumb_ignore" }, { "hands:l_hand_world", "hands:l_hand_world/hands:b_l_hand/hands:b_l_index1", "hands:l_hand_world/hands:b_l_hand/hands:b_l_index1/hands:b_l_index2/hands:b_l_index3/hands:b_l_index_ignore", "hands:l_hand_world/hands:b_l_hand/hands:b_l_thumb1/hands:b_l_thumb2", "hands:l_hand_world/hands:b_l_hand/hands:b_l_thumb1/hands:b_l_thumb2/hands:b_l_thumb3/hands:b_l_thumb_ignore" } }; static OvrAvatar() { // This size has to match the 'MarshalAs' attribute in the ovrAvatarVisemes declaration. RuntimeVisemes.visemeParams = new float[32]; RuntimeVisemes.visemeParamCount = VISEME_COUNT; } void OnDestroy() { if (sdkAvatar != IntPtr.Zero) { CAPI.ovrAvatar_Destroy(sdkAvatar); } } public void AssetLoadedCallback(OvrAvatarAsset asset) { assetLoadingIds.Remove(asset.assetID); } public void CombinedMeshLoadedCallback(IntPtr assetPtr) { if (!waitingForCombinedMesh) { return; } var meshIDs = CAPI.ovrAvatarAsset_GetCombinedMeshIDs(assetPtr); foreach (var id in meshIDs) { assetLoadingIds.Remove(id); } CAPI.ovrAvatar_GetCombinedMeshAlphaData(sdkAvatar, ref clothingAlphaTexture, ref clothingAlphaOffset); waitingForCombinedMesh = false; } private OvrAvatarSkinnedMeshRenderComponent AddSkinnedMeshRenderComponent(GameObject gameObject, ovrAvatarRenderPart_SkinnedMeshRender skinnedMeshRender) { OvrAvatarSkinnedMeshRenderComponent skinnedMeshRenderer = gameObject.AddComponent(); skinnedMeshRenderer.Initialize(skinnedMeshRender, Monochrome_SurfaceShader, Monochrome_SurfaceShader_SelfOccluding, ThirdPersonLayer.layerIndex, FirstPersonLayer.layerIndex); return skinnedMeshRenderer; } private OvrAvatarSkinnedMeshRenderPBSComponent AddSkinnedMeshRenderPBSComponent(GameObject gameObject, ovrAvatarRenderPart_SkinnedMeshRenderPBS skinnedMeshRenderPBS) { OvrAvatarSkinnedMeshRenderPBSComponent skinnedMeshRenderer = gameObject.AddComponent(); skinnedMeshRenderer.Initialize(skinnedMeshRenderPBS, Monochrome_SurfaceShader_PBS, ThirdPersonLayer.layerIndex, FirstPersonLayer.layerIndex); return skinnedMeshRenderer; } private OvrAvatarSkinnedMeshPBSV2RenderComponent AddSkinnedMeshRenderPBSV2Component( IntPtr renderPart, GameObject go, ovrAvatarRenderPart_SkinnedMeshRenderPBS_V2 skinnedMeshRenderPBSV2, bool isBodyPartZero, bool isControllerModel) { OvrAvatarSkinnedMeshPBSV2RenderComponent skinnedMeshRenderer = go.AddComponent(); skinnedMeshRenderer.Initialize( renderPart, skinnedMeshRenderPBSV2, materialManager, ThirdPersonLayer.layerIndex, FirstPersonLayer.layerIndex, isBodyPartZero && CombineMeshes, LevelOfDetail, isBodyPartZero && EnableExpressive, this, isControllerModel); return skinnedMeshRenderer; } static public IntPtr GetRenderPart(ovrAvatarComponent component, UInt32 renderPartIndex) { return Marshal.ReadIntPtr(component.renderParts, Marshal.SizeOf(typeof(IntPtr)) * (int)renderPartIndex); } private static string GetRenderPartName(ovrAvatarComponent component, uint renderPartIndex) { return component.name + "_renderPart_" + (int)renderPartIndex; } internal static void ConvertTransform(float[] transform, ref ovrAvatarTransform target) { target.position.x = transform[0]; target.position.y = transform[1]; target.position.z = transform[2]; target.orientation.x = transform[3]; target.orientation.y = transform[4]; target.orientation.z = transform[5]; target.orientation.w = transform[6]; target.scale.x = transform[7]; target.scale.y = transform[8]; target.scale.z = transform[9]; } internal static void ConvertTransform(ovrAvatarTransform transform, Transform target) { Vector3 position = transform.position; position.z = -position.z; Quaternion orientation = transform.orientation; orientation.x = -orientation.x; orientation.y = -orientation.y; target.localPosition = position; target.localRotation = orientation; target.localScale = transform.scale; } public static ovrAvatarTransform CreateOvrAvatarTransform(Vector3 position, Quaternion orientation) { return new ovrAvatarTransform { position = new Vector3(position.x, position.y, -position.z), orientation = new Quaternion(-orientation.x, -orientation.y, orientation.z, orientation.w), scale = Vector3.one }; } private static ovrAvatarGazeTarget CreateOvrGazeTarget(uint targetId, Vector3 targetPosition, ovrAvatarGazeTargetType targetType) { return new ovrAvatarGazeTarget { id = targetId, // Do coordinate system switch. worldPosition = new Vector3(targetPosition.x, targetPosition.y, -targetPosition.z), type = targetType }; } private void BuildRenderComponents() { ovrAvatarBaseComponent baseComponnet = new ovrAvatarBaseComponent(); ovrAvatarHandComponent leftHandComponnet = new ovrAvatarHandComponent(); ovrAvatarHandComponent rightHandComponnet = new ovrAvatarHandComponent(); ovrAvatarControllerComponent leftControllerComponent = new ovrAvatarControllerComponent(); ovrAvatarControllerComponent rightControllerComponent = new ovrAvatarControllerComponent(); ovrAvatarBodyComponent bodyComponent = new ovrAvatarBodyComponent(); ovrAvatarComponent dummyComponent = new ovrAvatarComponent(); const bool FetchName = true; if (CAPI.ovrAvatarPose_GetLeftHandComponent(sdkAvatar, ref leftHandComponnet)) { CAPI.ovrAvatarComponent_Get(leftHandComponnet.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref HandLeft, dummyComponent); HandLeft.isLeftHand = true; } if (CAPI.ovrAvatarPose_GetRightHandComponent(sdkAvatar, ref rightHandComponnet)) { CAPI.ovrAvatarComponent_Get(rightHandComponnet.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref HandRight, dummyComponent); HandRight.isLeftHand = false; } if (CAPI.ovrAvatarPose_GetBodyComponent(sdkAvatar, ref bodyComponent)) { CAPI.ovrAvatarComponent_Get(bodyComponent.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref Body, dummyComponent); } if (CAPI.ovrAvatarPose_GetLeftControllerComponent(sdkAvatar, ref leftControllerComponent)) { CAPI.ovrAvatarComponent_Get(leftControllerComponent.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref ControllerLeft, dummyComponent); ControllerLeft.isLeftHand = true; } if (CAPI.ovrAvatarPose_GetRightControllerComponent(sdkAvatar, ref rightControllerComponent)) { CAPI.ovrAvatarComponent_Get(rightControllerComponent.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref ControllerRight, dummyComponent); ControllerRight.isLeftHand = false; } if (CAPI.ovrAvatarPose_GetBaseComponent(sdkAvatar, ref baseComponnet)) { CAPI.ovrAvatarComponent_Get(baseComponnet.renderComponent, FetchName, ref dummyComponent); AddAvatarComponent(ref Base, dummyComponent); } } private void AddAvatarComponent(ref T root, ovrAvatarComponent nativeComponent) where T : OvrAvatarComponent { GameObject componentObject = new GameObject(); componentObject.name = nativeComponent.name; componentObject.transform.SetParent(transform); root = componentObject.AddComponent(); root.SetOvrAvatarOwner(this); AddRenderParts(root, nativeComponent, componentObject.transform); } void UpdateCustomPoses() { // Check to see if the pose roots changed if (UpdatePoseRoot(LeftHandCustomPose, ref cachedLeftHandCustomPose, ref cachedCustomLeftHandJoints, ref cachedLeftHandTransforms)) { if (cachedLeftHandCustomPose == null && sdkAvatar != IntPtr.Zero) { CAPI.ovrAvatar_SetLeftHandGesture(sdkAvatar, ovrAvatarHandGesture.Default); } } if (UpdatePoseRoot(RightHandCustomPose, ref cachedRightHandCustomPose, ref cachedCustomRightHandJoints, ref cachedRightHandTransforms)) { if (cachedRightHandCustomPose == null && sdkAvatar != IntPtr.Zero) { CAPI.ovrAvatar_SetRightHandGesture(sdkAvatar, ovrAvatarHandGesture.Default); } } // Check to see if the custom gestures need to be updated if (sdkAvatar != IntPtr.Zero) { if (cachedLeftHandCustomPose != null && UpdateTransforms(cachedCustomLeftHandJoints, cachedLeftHandTransforms)) { CAPI.ovrAvatar_SetLeftHandCustomGesture(sdkAvatar, (uint)cachedLeftHandTransforms.Length, cachedLeftHandTransforms); } if (cachedRightHandCustomPose != null && UpdateTransforms(cachedCustomRightHandJoints, cachedRightHandTransforms)) { CAPI.ovrAvatar_SetRightHandCustomGesture(sdkAvatar, (uint)cachedRightHandTransforms.Length, cachedRightHandTransforms); } } } static bool UpdatePoseRoot(Transform poseRoot, ref Transform cachedPoseRoot, ref Transform[] cachedPoseJoints, ref ovrAvatarTransform[] transforms) { if (poseRoot == cachedPoseRoot) { return false; } if (!poseRoot) { cachedPoseRoot = null; cachedPoseJoints = null; transforms = null; } else { List joints = new List(); OrderJoints(poseRoot, joints); cachedPoseRoot = poseRoot; cachedPoseJoints = joints.ToArray(); transforms = new ovrAvatarTransform[joints.Count]; } return true; } static bool UpdateTransforms(Transform[] joints, ovrAvatarTransform[] transforms) { bool updated = false; for (int i = 0; i < joints.Length; ++i) { Transform joint = joints[i]; ovrAvatarTransform transform = CreateOvrAvatarTransform(joint.localPosition, joint.localRotation); if (transform.position != transforms[i].position || transform.orientation != transforms[i].orientation) { transforms[i] = transform; updated = true; } } return updated; } private static void OrderJoints(Transform transform, List joints) { joints.Add(transform); for (int i = 0; i < transform.childCount; ++i) { Transform child = transform.GetChild(i); OrderJoints(child, joints); } } void AvatarSpecificationCallback(IntPtr avatarSpecification) { sdkAvatar = CAPI.ovrAvatar_Create(avatarSpecification, Capabilities); ShowLeftController(showLeftController); ShowRightController(showRightController); // Pump the Remote driver once to push the controller type through if (Driver != null) { Driver.UpdateTransformsFromPose(sdkAvatar); } //Fetch all the assets that this avatar uses. UInt32 assetCount = CAPI.ovrAvatar_GetReferencedAssetCount(sdkAvatar); for (UInt32 i = 0; i < assetCount; ++i) { UInt64 id = CAPI.ovrAvatar_GetReferencedAsset(sdkAvatar, i); if (OvrAvatarSDKManager.Instance.GetAsset(id) == null) { OvrAvatarSDKManager.Instance.BeginLoadingAsset( id, LevelOfDetail, AssetLoadedCallback); assetLoadingIds.Add(id); } } if (CombineMeshes) { OvrAvatarSDKManager.Instance.RegisterCombinedMeshCallback( sdkAvatar, CombinedMeshLoadedCallback); } } void Start() { if (OvrAvatarSDKManager.Instance == null) { return; } #if !UNITY_ANDROID if (CombineMeshes) { CombineMeshes = false; AvatarLogger.Log("Combined Meshes currently only supported on mobile"); } #endif #if !UNITY_5_5_OR_NEWER if (CombineMeshes) { CombineMeshes = false; AvatarLogger.LogWarning("Combined Meshes requires Unity 5.5.0+"); } #endif materialManager = gameObject.AddComponent(); try { oculusUserIDInternal = UInt64.Parse(oculusUserID); } catch (Exception) { oculusUserIDInternal = 0; AvatarLogger.LogWarning("Invalid Oculus User ID Format"); } // If no oculus ID is supplied then turn off combine meshes to prevent the texture arrays // being populated by invalid textures. if (oculusUserIDInternal == 0) { AvatarLogger.LogWarning("Oculus User ID set to 0. Provide actual user ID: " + gameObject.name); CombineMeshes = false; } AvatarLogger.Log("Starting OvrAvatar " + gameObject.name); AvatarLogger.Log(AvatarLogger.Tab + "LOD: " + LevelOfDetail.ToString()); AvatarLogger.Log(AvatarLogger.Tab + "Combine Meshes: " + CombineMeshes); AvatarLogger.Log(AvatarLogger.Tab + "Force Mobile Textures: " + USE_MOBILE_TEXTURE_FORMAT); AvatarLogger.Log(AvatarLogger.Tab + "Oculus User ID: " + oculusUserIDInternal); Capabilities = 0; bool is3Dof = false; var headsetType = OVRPlugin.GetSystemHeadsetType(); switch (headsetType) { case OVRPlugin.SystemHeadset.GearVR_R320: case OVRPlugin.SystemHeadset.GearVR_R321: case OVRPlugin.SystemHeadset.GearVR_R322: case OVRPlugin.SystemHeadset.GearVR_R323: case OVRPlugin.SystemHeadset.GearVR_R324: case OVRPlugin.SystemHeadset.GearVR_R325: case OVRPlugin.SystemHeadset.Oculus_Go: is3Dof = true; break; case OVRPlugin.SystemHeadset.Oculus_Quest: case OVRPlugin.SystemHeadset.Rift_S: case OVRPlugin.SystemHeadset.Rift_DK1: case OVRPlugin.SystemHeadset.Rift_DK2: case OVRPlugin.SystemHeadset.Rift_CV1: default: break; } // The SDK 3 DOF Arm Model requires the body skeleton to pose itself. It will crash without it // The likely use case here is trying to have an invisible body. // T45010595 if (is3Dof && !EnableBody) { AvatarLogger.Log("Forcing the Body component for 3Dof hand tracking, and setting the visibility to 1st person"); EnableBody = true; ShowFirstPerson = true; ShowThirdPerson = false; } if (EnableBody) Capabilities |= ovrAvatarCapabilities.Body; if (EnableHands) Capabilities |= ovrAvatarCapabilities.Hands; if (EnableBase && EnableBody) Capabilities |= ovrAvatarCapabilities.Base; if (EnableExpressive) Capabilities |= ovrAvatarCapabilities.Expressive; // Enable body tilt on 6dof devices if(OVRPlugin.positionSupported) { Capabilities |= ovrAvatarCapabilities.BodyTilt; } ShowLeftController(StartWithControllers); ShowRightController(StartWithControllers); OvrAvatarSDKManager.AvatarSpecRequestParams avatarSpecRequest = new OvrAvatarSDKManager.AvatarSpecRequestParams( oculusUserIDInternal, this.AvatarSpecificationCallback, CombineMeshes, LevelOfDetail, USE_MOBILE_TEXTURE_FORMAT, LookAndFeelVersion, FallbackLookAndFeelVersion, EnableExpressive); OvrAvatarSDKManager.Instance.RequestAvatarSpecification(avatarSpecRequest); OvrAvatarSDKManager.Instance.AddLoadingAvatar(GetInstanceID()); waitingForCombinedMesh = CombineMeshes; if (Driver != null) { Driver.Mode = UseSDKPackets ? OvrAvatarDriver.PacketMode.SDK : OvrAvatarDriver.PacketMode.Unity; } } void Update() { if (!OvrAvatarSDKManager.Instance || sdkAvatar == IntPtr.Zero || materialManager == null) { return; } if (Driver != null) { Driver.UpdateTransforms(sdkAvatar); foreach (float[] voiceUpdate in voiceUpdates) { CAPI.ovrAvatarPose_UpdateVoiceVisualization(sdkAvatar, voiceUpdate); } voiceUpdates.Clear(); #if AVATAR_INTERNAL if (BlendController != null) { BlendController.UpdateBlend(sdkAvatar); } #endif CAPI.ovrAvatarPose_Finalize(sdkAvatar, Time.deltaTime); } if (RecordPackets) { RecordFrame(); } if (assetLoadingIds.Count == 0) { if (!assetsFinishedLoading) { try { BuildRenderComponents(); } catch (Exception e) { assetsFinishedLoading = true; throw e; // rethrow the original exception to preserve callstack } #if AVATAR_INTERNAL AssetsDoneLoading.Invoke(); #endif InitPostLoad(); assetsFinishedLoading = true; OvrAvatarSDKManager.Instance.RemoveLoadingAvatar(GetInstanceID()); } UpdateVoiceBehavior(); UpdateCustomPoses(); if (EnableExpressive) { UpdateExpressive(); } } } public static ovrAvatarHandInputState CreateInputState(ovrAvatarTransform transform, OvrAvatarDriver.ControllerPose pose) { ovrAvatarHandInputState inputState = new ovrAvatarHandInputState(); inputState.transform = transform; inputState.buttonMask = pose.buttons; inputState.touchMask = pose.touches; inputState.joystickX = pose.joystickPosition.x; inputState.joystickY = pose.joystickPosition.y; inputState.indexTrigger = pose.indexTrigger; inputState.handTrigger = pose.handTrigger; inputState.isActive = pose.isActive; return inputState; } public void ShowControllers(bool show) { ShowLeftController(show); ShowRightController(show); } public void ShowLeftController(bool show) { if (sdkAvatar != IntPtr.Zero) { CAPI.ovrAvatar_SetLeftControllerVisibility(sdkAvatar, show); } showLeftController = show; } public void ShowRightController(bool show) { if (sdkAvatar != IntPtr.Zero) { CAPI.ovrAvatar_SetRightControllerVisibility(sdkAvatar, show); } showRightController = show; } public void UpdateVoiceVisualization(float[] voiceSamples) { voiceUpdates.Add(voiceSamples); } void RecordFrame() { if(UseSDKPackets) { RecordSDKFrame(); } else { RecordUnityFrame(); } } // Meant to be used mutually exclusively with RecordSDKFrame to give user more options to optimize or tweak packet data private void RecordUnityFrame() { var deltaSeconds = Time.deltaTime; var frame = Driver.GetCurrentPose(); // If this is our first packet, store the pose as the initial frame if (CurrentUnityPacket == null) { CurrentUnityPacket = new OvrAvatarPacket(frame); deltaSeconds = 0; } float recordedSeconds = 0; while (recordedSeconds < deltaSeconds) { float remainingSeconds = deltaSeconds - recordedSeconds; float remainingPacketSeconds = PacketSettings.UpdateRate - CurrentUnityPacket.Duration; // If we're not going to fill the packet, just add the frame if (remainingSeconds < remainingPacketSeconds) { CurrentUnityPacket.AddFrame(frame, remainingSeconds); recordedSeconds += remainingSeconds; } // If we're going to fill the packet, interpolate the pose, send the packet, // and open a new one else { // Interpolate between the packet's last frame and our target pose // to compute a pose at the end of the packet time. OvrAvatarDriver.PoseFrame a = CurrentUnityPacket.FinalFrame; OvrAvatarDriver.PoseFrame b = frame; float t = remainingPacketSeconds / remainingSeconds; OvrAvatarDriver.PoseFrame intermediatePose = OvrAvatarDriver.PoseFrame.Interpolate(a, b, t); CurrentUnityPacket.AddFrame(intermediatePose, remainingPacketSeconds); recordedSeconds += remainingPacketSeconds; // Broadcast the recorded packet if (PacketRecorded != null) { PacketRecorded(this, new PacketEventArgs(CurrentUnityPacket)); } // Open a new packet CurrentUnityPacket = new OvrAvatarPacket(intermediatePose); } } } private void RecordSDKFrame() { if (sdkAvatar == IntPtr.Zero) { return; } if (!PacketSettings.RecordingFrames) { CAPI.ovrAvatarPacket_BeginRecording(sdkAvatar); PacketSettings.AccumulatedTime = 0.0f; PacketSettings.RecordingFrames = true; } PacketSettings.AccumulatedTime += Time.deltaTime; if (PacketSettings.AccumulatedTime >= PacketSettings.UpdateRate) { PacketSettings.AccumulatedTime = 0.0f; var packet = CAPI.ovrAvatarPacket_EndRecording(sdkAvatar); CAPI.ovrAvatarPacket_BeginRecording(sdkAvatar); if (PacketRecorded != null) { PacketRecorded(this, new PacketEventArgs(new OvrAvatarPacket { ovrNativePacket = packet })); } CAPI.ovrAvatarPacket_Free(packet); } } private void AddRenderParts( OvrAvatarComponent ovrComponent, ovrAvatarComponent component, Transform parent) { bool isBody = ovrComponent.name == "body"; bool isLeftController = ovrComponent.name == "controller_left"; bool isReftController = ovrComponent.name == "controller_right"; for (UInt32 renderPartIndex = 0; renderPartIndex < component.renderPartCount; renderPartIndex++) { GameObject renderPartObject = new GameObject(); renderPartObject.name = GetRenderPartName(component, renderPartIndex); renderPartObject.transform.SetParent(parent); IntPtr renderPart = GetRenderPart(component, renderPartIndex); ovrAvatarRenderPartType type = CAPI.ovrAvatarRenderPart_GetType(renderPart); OvrAvatarRenderComponent ovrRenderPart = null; switch (type) { case ovrAvatarRenderPartType.SkinnedMeshRender: ovrRenderPart = AddSkinnedMeshRenderComponent(renderPartObject, CAPI.ovrAvatarRenderPart_GetSkinnedMeshRender(renderPart)); break; case ovrAvatarRenderPartType.SkinnedMeshRenderPBS: ovrRenderPart = AddSkinnedMeshRenderPBSComponent(renderPartObject, CAPI.ovrAvatarRenderPart_GetSkinnedMeshRenderPBS(renderPart)); break; case ovrAvatarRenderPartType.SkinnedMeshRenderPBS_V2: { ovrRenderPart = AddSkinnedMeshRenderPBSV2Component( renderPart, renderPartObject, CAPI.ovrAvatarRenderPart_GetSkinnedMeshRenderPBSV2(renderPart), isBody && renderPartIndex == 0, isLeftController || isReftController); } break; default: break; } if (ovrRenderPart != null) { ovrComponent.RenderParts.Add(ovrRenderPart); } } } public void RefreshBodyParts() { if (Body != null) { foreach (var part in Body.RenderParts) { Destroy(part.gameObject); } Body.RenderParts.Clear(); var nativeAvatarComponent = Body.GetNativeAvatarComponent(); if (nativeAvatarComponent.HasValue) { AddRenderParts(Body, nativeAvatarComponent.Value, Body.gameObject.transform); } } } public ovrAvatarBodyComponent? GetBodyComponent() { if (Body != null) { CAPI.ovrAvatarPose_GetBodyComponent(sdkAvatar, ref Body.component); return Body.component; } return null; } public Transform GetHandTransform(HandType hand, HandJoint joint) { if (hand >= HandType.Max || joint >= HandJoint.Max) { return null; } var HandObject = hand == HandType.Left ? HandLeft : HandRight; if (HandObject != null) { var AvatarComponent = HandObject.GetComponent(); if (AvatarComponent != null && AvatarComponent.RenderParts.Count > 0) { var SkinnedMesh = AvatarComponent.RenderParts[0]; return SkinnedMesh.transform.Find(HandJoints[(int)hand, (int)joint]); } } return null; } public void GetPointingDirection(HandType hand, ref Vector3 forward, ref Vector3 up) { Transform handBase = GetHandTransform(hand, HandJoint.HandBase); if (handBase != null) { forward = handBase.forward; up = handBase.up; } } static Vector3 MOUTH_POSITION_OFFSET = new Vector3(0, -0.018f, 0.1051f); static string VOICE_PROPERTY = "_Voice"; static string MOUTH_POSITION_PROPERTY = "_MouthPosition"; static string MOUTH_DIRECTION_PROPERTY = "_MouthDirection"; static string MOUTH_SCALE_PROPERTY = "_MouthEffectScale"; static float MOUTH_SCALE_GLOBAL = 0.007f; static float MOUTH_MAX_GLOBAL = 0.007f; static string NECK_JONT = "root_JNT/body_JNT/chest_JNT/neckBase_JNT/neck_JNT"; public float VoiceAmplitude = 0f; public bool EnableMouthVertexAnimation = false; private void UpdateVoiceBehavior() { if (!EnableMouthVertexAnimation) { return; } if (Body != null) { OvrAvatarComponent component = Body.GetComponent(); VoiceAmplitude = Mathf.Clamp(VoiceAmplitude, 0f, 1f); if (component.RenderParts.Count > 0) { var material = component.RenderParts[0].mesh.sharedMaterial; var neckJoint = component.RenderParts[0].mesh.transform.Find(NECK_JONT); var scaleDiff = neckJoint.TransformPoint(Vector3.up) - neckJoint.position; material.SetFloat(MOUTH_SCALE_PROPERTY, scaleDiff.magnitude); material.SetFloat( VOICE_PROPERTY, Mathf.Min(scaleDiff.magnitude * MOUTH_MAX_GLOBAL, scaleDiff.magnitude * VoiceAmplitude * MOUTH_SCALE_GLOBAL)); material.SetVector( MOUTH_POSITION_PROPERTY, neckJoint.TransformPoint(MOUTH_POSITION_OFFSET)); material.SetVector(MOUTH_DIRECTION_PROPERTY, neckJoint.up); } } } bool IsValidMic() { string[] devices = Microphone.devices; if (devices.Length < 1) { return false; } int selectedDeviceIndex = 0; #if UNITY_STANDALONE_WIN for (int i = 1; i < devices.Length; i++) { if (devices[i].ToUpper().Contains("RIFT")) { selectedDeviceIndex = i; break; } } #endif string selectedDevice = devices[selectedDeviceIndex]; int minFreq; int maxFreq; Microphone.GetDeviceCaps(selectedDevice, out minFreq, out maxFreq); if (maxFreq == 0) { maxFreq = 44100; } AudioClip clip = Microphone.Start(selectedDevice, true, 1, maxFreq); if (clip == null) { return false; } Microphone.End(selectedDevice); return true; } void InitPostLoad() { ExpressiveGlobalInit(); ConfigureHelpers(); if (GetComponent() != null) { // Use mic. lipsyncContext.audioLoopback = false; if (CanOwnMicrophone && IsValidMic()) { micInput = MouthAnchor.gameObject.AddComponent(); micInput.enableMicSelectionGUI = false; micInput.MicFrequency = 44100; micInput.micControl = OVRLipSyncMicInput.micActivation.ConstantSpeak; } // Set lipsync animation parameters in SDK CAPI.ovrAvatar_SetActionUnitOnsetSpeed(sdkAvatar, ACTION_UNIT_ONSET_SPEED); CAPI.ovrAvatar_SetActionUnitFalloffSpeed(sdkAvatar, ACTION_UNIT_FALLOFF_SPEED); CAPI.ovrAvatar_SetVisemeMultiplier(sdkAvatar, VISEME_LEVEL_MULTIPLIER); } } static ovrAvatarLights ovrLights = new ovrAvatarLights(); static void ExpressiveGlobalInit() { if (doneExpressiveGlobalInit) { return; } doneExpressiveGlobalInit = true; // This array size has to match the 'MarshalAs' attribute in the ovrAvatarLights declaration. const int MAXSIZE = 16; ovrLights.lights = new ovrAvatarLight[MAXSIZE]; InitializeLights(); } static void InitializeLights() { // Set light info. Lights are shared across all avatar instances. ovrLights.ambientIntensity = RenderSettings.ambientLight.grayscale * 0.5f; Light[] sceneLights = FindObjectsOfType(typeof(Light)) as Light[]; int i = 0; for (i = 0; i < sceneLights.Length && i < ovrLights.lights.Length; ++i) { Light sceneLight = sceneLights[i]; if (sceneLight && sceneLight.enabled) { uint instanceID = (uint)sceneLight.transform.GetInstanceID(); switch (sceneLight.type) { case LightType.Directional: { CreateLightDirectional(instanceID, sceneLight.transform.forward, sceneLight.intensity, ref ovrLights.lights[i]); break; } case LightType.Point: { CreateLightPoint(instanceID, sceneLight.transform.position, sceneLight.range, sceneLight.intensity, ref ovrLights.lights[i]); break; } case LightType.Spot: { CreateLightSpot(instanceID, sceneLight.transform.position, sceneLight.transform.forward, sceneLight.spotAngle, sceneLight.range, sceneLight.intensity, ref ovrLights.lights[i]); break; } } } } ovrLights.lightCount = (uint)i; CAPI.ovrAvatar_UpdateLights(ovrLights); } static ovrAvatarLight CreateLightDirectional(uint id, Vector3 direction, float intensity, ref ovrAvatarLight light) { light.id = id; light.type = ovrAvatarLightType.Direction; light.worldDirection = new Vector3(direction.x, direction.y, -direction.z); light.intensity = intensity; return light; } static ovrAvatarLight CreateLightPoint(uint id, Vector3 position, float range, float intensity, ref ovrAvatarLight light) { light.id = id; light.type = ovrAvatarLightType.Point; light.worldPosition = new Vector3(position.x, position.y, -position.z); light.range = range; light.intensity = intensity; return light; } static ovrAvatarLight CreateLightSpot(uint id, Vector3 position, Vector3 direction, float spotAngleDeg, float range, float intensity, ref ovrAvatarLight light) { light.id = id; light.type = ovrAvatarLightType.Spot; light.worldPosition = new Vector3(position.x, position.y, -position.z); light.worldDirection = new Vector3(direction.x, direction.y, -direction.z); light.spotAngleDeg = spotAngleDeg; light.range = range; light.intensity = intensity; return light; } void UpdateExpressive() { ovrAvatarTransform baseTransform = OvrAvatar.CreateOvrAvatarTransform(transform.position, transform.rotation); CAPI.ovrAvatar_UpdateWorldTransform(sdkAvatar, baseTransform); UpdateFacewave(); } private void ConfigureHelpers() { Transform head = transform.Find("body/body_renderPart_0/root_JNT/body_JNT/chest_JNT/neckBase_JNT/neck_JNT/head_JNT"); if (head == null) { AvatarLogger.LogError("Avatar helper config failed. Cannot find head transform. All helpers spawning on root avatar transform"); head = transform; } if (MouthAnchor == null) { MouthAnchor = CreateHelperObject(head, MOUTH_HEAD_OFFSET, MOUTH_HELPER_NAME); } if (GetComponent() != null) { if (audioSource == null) { audioSource = MouthAnchor.gameObject.AddComponent(); } spatializedSource = MouthAnchor.GetComponent(); if (spatializedSource == null) { spatializedSource = MouthAnchor.gameObject.AddComponent(); } spatializedSource.UseInvSqr = true; spatializedSource.EnableRfl = false; spatializedSource.EnableSpatialization = true; spatializedSource.Far = 100f; spatializedSource.Near = 0.1f; // Add phoneme context to the mouth anchor lipsyncContext = MouthAnchor.GetComponent(); if (lipsyncContext == null) { lipsyncContext = MouthAnchor.gameObject.AddComponent(); } lipsyncContext.provider = EnableLaughter ? OVRLipSync.ContextProviders.Enhanced_with_Laughter : OVRLipSync.ContextProviders.Enhanced; // Ignore audio callback if microphone is owned by VoIP lipsyncContext.skipAudioSource = !CanOwnMicrophone; StartCoroutine(WaitForMouthAudioSource()); } if (GetComponent() != null) { GazeTarget headTarget = head.gameObject.AddComponent(); headTarget.Type = ovrAvatarGazeTargetType.AvatarHead; AvatarLogger.Log("Added head as gaze target"); Transform hand = transform.Find("hand_left"); if (hand == null) { AvatarLogger.LogWarning("Gaze target helper config failed: Cannot find left hand transform"); } else { GazeTarget handTarget = hand.gameObject.AddComponent(); handTarget.Type = ovrAvatarGazeTargetType.AvatarHand; AvatarLogger.Log("Added left hand as gaze target"); } hand = transform.Find("hand_right"); if (hand == null) { AvatarLogger.Log("Gaze target helper config failed: Cannot find right hand transform"); } else { GazeTarget handTarget = hand.gameObject.AddComponent(); handTarget.Type = ovrAvatarGazeTargetType.AvatarHand; AvatarLogger.Log("Added right hand as gaze target"); } } } private IEnumerator WaitForMouthAudioSource() { while (MouthAnchor.GetComponent() == null) { yield return new WaitForSeconds(0.1f); } AudioSource AS = MouthAnchor.GetComponent(); AS.minDistance = 0.3f; AS.maxDistance = 4f; AS.rolloffMode = AudioRolloffMode.Logarithmic; AS.loop = true; AS.playOnAwake = true; AS.spatialBlend = 1.0f; AS.spatialize = true; AS.spatializePostEffects = true; } public void DestroyHelperObjects() { if (MouthAnchor) { DestroyImmediate(MouthAnchor.gameObject); } } public GameObject CreateHelperObject(Transform parent, Vector3 localPositionOffset, string helperName, string helperTag = "") { GameObject helper = new GameObject(); helper.name = helperName; if (helperTag != "") { helper.tag = helperTag; } helper.transform.SetParent(parent); helper.transform.localRotation = Quaternion.identity; helper.transform.localPosition = localPositionOffset; return helper; } public void UpdateVoiceData(short[] pcmData, int numChannels) { if (lipsyncContext != null && micInput == null) { lipsyncContext.ProcessAudioSamplesRaw(pcmData, numChannels); } } public void UpdateVoiceData(float[] pcmData, int numChannels) { if (lipsyncContext != null && micInput == null) { lipsyncContext.ProcessAudioSamplesRaw(pcmData, numChannels); } } private void UpdateFacewave() { if (lipsyncContext != null && (micInput != null || CanOwnMicrophone == false)) { // Get the current viseme frame currentFrame = lipsyncContext.GetCurrentPhonemeFrame(); // Verify length (-1 for laughter) if (currentFrame.Visemes.Length != (VISEME_COUNT - 1)) { Debug.LogError("Unexpected number of visemes " + currentFrame.Visemes); return; } // Copy to viseme array currentFrame.Visemes.CopyTo(visemes, 0); // Copy laughter as final element visemes[VISEME_COUNT - 1] = EnableLaughter ? currentFrame.laughterScore : 0.0f; // Send visemes to native implementation. for (int i = 0; i < VISEME_COUNT; i++) { RuntimeVisemes.visemeParams[i] = visemes[i]; } CAPI.ovrAvatar_SetVisemes(sdkAvatar, RuntimeVisemes); } } }