//======= Copyright (c) Valve Corporation, All rights reserved. =============== using System; using System.Collections; using UnityEngine; using Valve.VR; using System.Collections.Generic; using System.Linq; namespace Valve.VR { public class SteamVR_Skeleton_Poser : MonoBehaviour { #region Editor Storage public bool poseEditorExpanded = true; public bool blendEditorExpanded = true; public string[] poseNames; #endregion public GameObject previewLeftHandPrefab; public GameObject previewRightHandPrefab; public SteamVR_Skeleton_Pose skeletonMainPose; public List skeletonAdditionalPoses = new List(); [SerializeField] protected bool showLeftPreview = false; [SerializeField] protected bool showRightPreview = true; //show the right hand by default [SerializeField] protected GameObject previewLeftInstance; [SerializeField] protected GameObject previewRightInstance; [SerializeField] protected int previewPoseSelection = 0; public int blendPoseCount { get { return blendPoses.Length; } } public List blendingBehaviours = new List(); public SteamVR_Skeleton_PoseSnapshot blendedSnapshotL; public SteamVR_Skeleton_PoseSnapshot blendedSnapshotR; private SkeletonBlendablePose[] blendPoses; private int boneCount; private bool poseUpdatedThisFrame; public float scale; protected void Awake() { if (previewLeftInstance != null) DestroyImmediate(previewLeftInstance); if (previewRightInstance != null) DestroyImmediate(previewRightInstance); blendPoses = new SkeletonBlendablePose[skeletonAdditionalPoses.Count + 1]; for (int i = 0; i < blendPoseCount; i++) { blendPoses[i] = new SkeletonBlendablePose(GetPoseByIndex(i)); blendPoses[i].PoseToSnapshots(); } boneCount = skeletonMainPose.leftHand.bonePositions.Length; // NOTE: Is there a better way to get the bone count? idk blendedSnapshotL = new SteamVR_Skeleton_PoseSnapshot(boneCount, SteamVR_Input_Sources.LeftHand); blendedSnapshotR = new SteamVR_Skeleton_PoseSnapshot(boneCount, SteamVR_Input_Sources.RightHand); } /// /// Set the blending value of a blendingBehaviour. Works best on Manual type behaviours. /// public void SetBlendingBehaviourValue(string behaviourName, float value) { PoseBlendingBehaviour behaviour = blendingBehaviours.Find(b => b.name == behaviourName); if(behaviour == null) { Debug.LogError("[SteamVR] Blending Behaviour: " + behaviourName + " not found on Skeleton Poser: " + gameObject.name); return; } if(behaviour.type != PoseBlendingBehaviour.BlenderTypes.Manual) { Debug.LogWarning("[SteamVR] Blending Behaviour: " + behaviourName + " is not a manual behaviour. Its value will likely be overriden."); } behaviour.value = value; } /// /// Get the blending value of a blendingBehaviour. /// public float GetBlendingBehaviourValue(string behaviourName) { PoseBlendingBehaviour behaviour = blendingBehaviours.Find(b => b.name == behaviourName); if (behaviour == null) { Debug.LogError("[SteamVR] Blending Behaviour: " + behaviourName + " not found on Skeleton Poser: " + gameObject.name); return 0; } return behaviour.value; } /// /// Enable or disable a blending behaviour. /// public void SetBlendingBehaviourEnabled(string behaviourName, bool value) { PoseBlendingBehaviour behaviour = blendingBehaviours.Find(b => b.name == behaviourName); if (behaviour == null) { Debug.LogError("[SteamVR] Blending Behaviour: " + behaviourName + " not found on Skeleton Poser: " + gameObject.name); return; } behaviour.enabled = value; } /// /// Check if a blending behaviour is enabled. /// /// /// public bool GetBlendingBehaviourEnabled(string behaviourName) { PoseBlendingBehaviour behaviour = blendingBehaviours.Find(b => b.name == behaviourName); if (behaviour == null) { Debug.LogError("[SteamVR] Blending Behaviour: " + behaviourName + " not found on Skeleton Poser: " + gameObject.name); return false; } return behaviour.enabled; } /// /// Get a blending behaviour by name. /// public PoseBlendingBehaviour GetBlendingBehaviour(string behaviourName) { PoseBlendingBehaviour behaviour = blendingBehaviours.Find(b => b.name == behaviourName); if (behaviour == null) { Debug.LogError("[SteamVR] Blending Behaviour: " + behaviourName + " not found on Skeleton Poser: " + gameObject.name); return null; } return behaviour; } public SteamVR_Skeleton_Pose GetPoseByIndex(int index) { if (index == 0) { return skeletonMainPose; } else { return skeletonAdditionalPoses[index - 1]; } } private SteamVR_Skeleton_PoseSnapshot GetHandSnapshot(SteamVR_Input_Sources inputSource) { if (inputSource == SteamVR_Input_Sources.LeftHand) return blendedSnapshotL; else return blendedSnapshotR; } /// /// Retrieve the final animated pose, to be applied to a hand skeleton /// /// The skeleton action you want to blend between /// If this is for the left or right hand public SteamVR_Skeleton_PoseSnapshot GetBlendedPose(SteamVR_Action_Skeleton skeletonAction, SteamVR_Input_Sources handType) { UpdatePose(skeletonAction, handType); return GetHandSnapshot(handType); } /// /// Retrieve the final animated pose, to be applied to a hand skeleton /// /// The skeleton behaviour you want to get the action/input source from to blend between public SteamVR_Skeleton_PoseSnapshot GetBlendedPose(SteamVR_Behaviour_Skeleton skeletonBehaviour) { return GetBlendedPose(skeletonBehaviour.skeletonAction, skeletonBehaviour.inputSource); } /// /// Updates all pose animation and blending. Can be called from different places without performance concerns, as it will only let itself run once per frame. /// public void UpdatePose(SteamVR_Action_Skeleton skeletonAction, SteamVR_Input_Sources inputSource) { // only allow this function to run once per frame if (poseUpdatedThisFrame) return; poseUpdatedThisFrame = true; // always do additive animation on main pose blendPoses[0].UpdateAdditiveAnimation(skeletonAction, inputSource); //copy from main pose as a base SteamVR_Skeleton_PoseSnapshot snap = GetHandSnapshot(inputSource); snap.CopyFrom(blendPoses[0].GetHandSnapshot(inputSource)); ApplyBlenderBehaviours(skeletonAction, inputSource, snap); if (inputSource == SteamVR_Input_Sources.RightHand) blendedSnapshotR = snap; if (inputSource == SteamVR_Input_Sources.LeftHand) blendedSnapshotL = snap; } protected void ApplyBlenderBehaviours(SteamVR_Action_Skeleton skeletonAction, SteamVR_Input_Sources inputSource, SteamVR_Skeleton_PoseSnapshot snapshot) { // apply blending for each behaviour for (int behaviourIndex = 0; behaviourIndex < blendingBehaviours.Count; behaviourIndex++) { blendingBehaviours[behaviourIndex].Update(Time.deltaTime, inputSource); // if disabled or very low influence, skip for perf if (blendingBehaviours[behaviourIndex].enabled && blendingBehaviours[behaviourIndex].influence * blendingBehaviours[behaviourIndex].value > 0.01f) { if (blendingBehaviours[behaviourIndex].pose != 0) { // update additive animation only as needed blendPoses[blendingBehaviours[behaviourIndex].pose].UpdateAdditiveAnimation(skeletonAction, inputSource); } blendingBehaviours[behaviourIndex].ApplyBlending(snapshot, blendPoses, inputSource); } } } protected void LateUpdate() { // let the pose be updated again the next frame poseUpdatedThisFrame = false; } /// Weighted average of n vector3s protected Vector3 BlendVectors(Vector3[] vectors, float[] weights) { Vector3 blendedVector = Vector3.zero; for (int i = 0; i < vectors.Length; i++) { blendedVector += vectors[i] * weights[i]; } return blendedVector; } /// Weighted average of n quaternions protected Quaternion BlendQuaternions(Quaternion[] quaternions, float[] weights) { Quaternion outquat = Quaternion.identity; for (int i = 0; i < quaternions.Length; i++) { outquat *= Quaternion.Slerp(Quaternion.identity, quaternions[i], weights[i]); } return outquat; } /// /// A SkeletonBlendablePose holds a reference to a Skeleton_Pose scriptableObject, and also contains some helper functions. /// Also handles pose-specific animation like additive finger motion. /// public class SkeletonBlendablePose { public SteamVR_Skeleton_Pose pose; public SteamVR_Skeleton_PoseSnapshot snapshotR; public SteamVR_Skeleton_PoseSnapshot snapshotL; /// /// Get the snapshot of this pose with effects such as additive finger animation applied. /// public SteamVR_Skeleton_PoseSnapshot GetHandSnapshot(SteamVR_Input_Sources inputSource) { if (inputSource == SteamVR_Input_Sources.LeftHand) { return snapshotL; } else { return snapshotR; } } public void UpdateAdditiveAnimation(SteamVR_Action_Skeleton skeletonAction, SteamVR_Input_Sources inputSource) { SteamVR_Skeleton_PoseSnapshot snapshot = GetHandSnapshot(inputSource); SteamVR_Skeleton_Pose_Hand poseHand = pose.GetHand(inputSource); for (int boneIndex = 0; boneIndex < snapshotL.bonePositions.Length; boneIndex++) { int fingerIndex = SteamVR_Skeleton_JointIndexes.GetFingerForBone(boneIndex); SteamVR_Skeleton_FingerExtensionTypes extensionType = poseHand.GetMovementTypeForBone(boneIndex); if (extensionType == SteamVR_Skeleton_FingerExtensionTypes.Free) { snapshot.bonePositions[boneIndex] = skeletonAction.bonePositions[boneIndex]; snapshot.boneRotations[boneIndex] = skeletonAction.boneRotations[boneIndex]; } if (extensionType == SteamVR_Skeleton_FingerExtensionTypes.Extend) { // lerp to open pose by fingercurl snapshot.bonePositions[boneIndex] = Vector3.Lerp(poseHand.bonePositions[boneIndex], skeletonAction.bonePositions[boneIndex], 1 - skeletonAction.fingerCurls[fingerIndex]); snapshot.boneRotations[boneIndex] = Quaternion.Lerp(poseHand.boneRotations[boneIndex], skeletonAction.boneRotations[boneIndex], 1 - skeletonAction.fingerCurls[fingerIndex]); } if (extensionType == SteamVR_Skeleton_FingerExtensionTypes.Contract) { // lerp to closed pose by fingercurl snapshot.bonePositions[boneIndex] = Vector3.Lerp(poseHand.bonePositions[boneIndex], skeletonAction.bonePositions[boneIndex], skeletonAction.fingerCurls[fingerIndex]); snapshot.boneRotations[boneIndex] = Quaternion.Lerp(poseHand.boneRotations[boneIndex], skeletonAction.boneRotations[boneIndex], skeletonAction.fingerCurls[fingerIndex]); } } } /// /// Init based on an existing Skeleton_Pose /// public SkeletonBlendablePose(SteamVR_Skeleton_Pose p) { pose = p; snapshotR = new SteamVR_Skeleton_PoseSnapshot(p.rightHand.bonePositions.Length, SteamVR_Input_Sources.RightHand); snapshotL = new SteamVR_Skeleton_PoseSnapshot(p.leftHand.bonePositions.Length, SteamVR_Input_Sources.LeftHand); } /// /// Copy the base pose into the snapshots. /// public void PoseToSnapshots() { snapshotR.position = pose.rightHand.position; snapshotR.rotation = pose.rightHand.rotation; pose.rightHand.bonePositions.CopyTo(snapshotR.bonePositions, 0); pose.rightHand.boneRotations.CopyTo(snapshotR.boneRotations, 0); snapshotL.position = pose.leftHand.position; snapshotL.rotation = pose.leftHand.rotation; pose.leftHand.bonePositions.CopyTo(snapshotL.bonePositions, 0); pose.leftHand.boneRotations.CopyTo(snapshotL.boneRotations, 0); } public SkeletonBlendablePose() { } } /// /// A filter applied to the base pose. Blends to a secondary pose by a certain weight. Can be masked per-finger /// [System.Serializable] public class PoseBlendingBehaviour { public string name; public bool enabled = true; public float influence = 1; public int pose = 1; public float value = 0; public SteamVR_Action_Single action_single; public SteamVR_Action_Boolean action_bool; public float smoothingSpeed = 0; public BlenderTypes type; public bool useMask; public SteamVR_Skeleton_HandMask mask = new SteamVR_Skeleton_HandMask(); public bool previewEnabled; /// /// Performs smoothing based on deltaTime parameter. /// public void Update(float deltaTime, SteamVR_Input_Sources inputSource) { if (type == BlenderTypes.AnalogAction) { if (smoothingSpeed == 0) value = action_single.GetAxis(inputSource); else value = Mathf.Lerp(value, action_single.GetAxis(inputSource), deltaTime * smoothingSpeed); } if (type == BlenderTypes.BooleanAction) { if (smoothingSpeed == 0) value = action_bool.GetState(inputSource) ? 1 : 0; else value = Mathf.Lerp(value, action_bool.GetState(inputSource) ? 1 : 0, deltaTime * smoothingSpeed); } } /// /// Apply blending to this behaviour's pose to an existing snapshot. /// /// Snapshot to modify /// List of blend poses to get the target pose /// Which hand to receive input from public void ApplyBlending(SteamVR_Skeleton_PoseSnapshot snapshot, SkeletonBlendablePose[] blendPoses, SteamVR_Input_Sources inputSource) { SteamVR_Skeleton_PoseSnapshot targetSnapshot = blendPoses[pose].GetHandSnapshot(inputSource); if (mask.GetFinger(0) || useMask == false) { snapshot.position = Vector3.Lerp(snapshot.position, targetSnapshot.position, influence * value); snapshot.rotation = Quaternion.Slerp(snapshot.rotation, targetSnapshot.rotation, influence * value); } for (int boneIndex = 0; boneIndex < snapshot.bonePositions.Length; boneIndex++) { // verify the current finger is enabled in the mask, or if no mask is used. if (mask.GetFinger(SteamVR_Skeleton_JointIndexes.GetFingerForBone(boneIndex) + 1) || useMask == false) { snapshot.bonePositions[boneIndex] = Vector3.Lerp(snapshot.bonePositions[boneIndex], targetSnapshot.bonePositions[boneIndex], influence * value); snapshot.boneRotations[boneIndex] = Quaternion.Slerp(snapshot.boneRotations[boneIndex], targetSnapshot.boneRotations[boneIndex], influence * value); } } } public PoseBlendingBehaviour() { enabled = true; influence = 1; } public enum BlenderTypes { Manual, AnalogAction, BooleanAction } } } /// /// PoseSnapshots hold a skeleton pose for one hand, as well as storing which hand they contain. /// They have several functions for combining BlendablePoses. /// public class SteamVR_Skeleton_PoseSnapshot { public SteamVR_Input_Sources inputSource; public Vector3 position; public Quaternion rotation; public Vector3[] bonePositions; public Quaternion[] boneRotations; public SteamVR_Skeleton_PoseSnapshot(int boneCount, SteamVR_Input_Sources source) { inputSource = source; bonePositions = new Vector3[boneCount]; boneRotations = new Quaternion[boneCount]; position = Vector3.zero; rotation = Quaternion.identity; } /// /// Perform a deep copy from one poseSnapshot to another. /// public void CopyFrom(SteamVR_Skeleton_PoseSnapshot source) { inputSource = source.inputSource; position = source.position; rotation = source.rotation; for (int i = 0; i < bonePositions.Length; i++) { bonePositions[i] = source.bonePositions[i]; boneRotations[i] = source.boneRotations[i]; } } } /// /// Simple mask for fingers /// [System.Serializable] public class SteamVR_Skeleton_HandMask { public bool palm; public bool thumb; public bool index; public bool middle; public bool ring; public bool pinky; public bool[] values = new bool[6]; public void SetFinger(int i, bool value) { values[i] = value; Apply(); } public bool GetFinger(int i) { return values[i]; } public SteamVR_Skeleton_HandMask() { values = new bool[6]; Reset(); } /// /// All elements on /// public void Reset() { values = new bool[6]; for (int i = 0; i < 6; i++) { values[i] = true; } Apply(); } protected void Apply() { palm = values[0]; thumb = values[1]; index = values[2]; middle = values[3]; ring = values[4]; pinky = values[5]; } public static readonly SteamVR_Skeleton_HandMask fullMask = new SteamVR_Skeleton_HandMask(); }; }