Last active
July 5, 2024 21:18
-
-
Save ryan-at-melcher/773d3073329134c43a60110e101f0557 to your computer and use it in GitHub Desktop.
Unity fix for OVRControllerHands prefab incorrectly positioning synthetic hands using a local-pose rather than world-pose.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Oculus.Interaction; | |
using Oculus.Interaction.Input; | |
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
/// <summary> | |
/// Renders the hand. | |
/// <br /><br /> | |
/// | |
/// This is a copy of Oculus' <see cref="HandVisual"/> that has been | |
/// modified to ensure the hand renders using a world-space pose. The | |
/// default script uses a local-space pose which results in the hands | |
/// not accurately tracking the camera rig as it moves and rotates. | |
/// </summary> | |
public class WorldSpaceHandVisual : MonoBehaviour, IHandVisual | |
{ | |
/// <summary> | |
/// The hand to render. | |
/// </summary> | |
[SerializeField, Interface(typeof(IHand))] | |
private UnityEngine.Object _hand; | |
public IHand Hand { get; private set; } | |
/// <summary> | |
/// Determines the appearance of the hand. | |
/// </summary> | |
[SerializeField] | |
private SkinnedMeshRenderer _skinnedMeshRenderer; | |
/// <summary> | |
/// | |
/// </summary> | |
[SerializeField] | |
private bool _updateRootPose = true; | |
/// <summary> | |
/// | |
/// </summary> | |
[SerializeField] | |
private bool _updateRootScale = true; | |
[SerializeField] | |
private TrackingToWorldTransformerOVR _trackingToWorldTransformer = null; | |
/// <summary> | |
/// | |
/// </summary> | |
[SerializeField, Optional] | |
private Transform _root = null; | |
[SerializeField, Optional] | |
private MaterialPropertyBlockEditor _handMaterialPropertyBlockEditor; | |
[HideInInspector] | |
[SerializeField] | |
private List<Transform> _jointTransforms = new List<Transform>(); | |
public event Action WhenHandVisualUpdated = delegate { }; | |
public bool IsVisible => _skinnedMeshRenderer != null && _skinnedMeshRenderer.enabled; | |
private int _wristScalePropertyId; | |
public IList<Transform> Joints => _jointTransforms; | |
public Transform Root => _root; | |
private bool _forceOffVisibility; | |
public bool ForceOffVisibility | |
{ | |
get | |
{ | |
return _forceOffVisibility; | |
} | |
set | |
{ | |
_forceOffVisibility = value; | |
if(_started) | |
{ | |
UpdateVisibility(); | |
} | |
} | |
} | |
private bool _started = false; | |
protected virtual void Awake() | |
{ | |
Hand = _hand as IHand; | |
if (_root == null && _jointTransforms.Count > 0 && _jointTransforms[0] != null) | |
{ | |
_root = _jointTransforms[0].parent; | |
} | |
} | |
protected virtual void Start() | |
{ | |
this.BeginStart(ref _started); | |
this.AssertField(Hand, nameof(Hand)); | |
this.AssertField(_skinnedMeshRenderer, nameof(_skinnedMeshRenderer)); | |
if (_handMaterialPropertyBlockEditor != null) | |
{ | |
_wristScalePropertyId = Shader.PropertyToID("_WristScale"); | |
} | |
UpdateVisibility(); | |
this.EndStart(ref _started); | |
} | |
protected virtual void OnEnable() | |
{ | |
if (_started) | |
{ | |
Hand.WhenHandUpdated += UpdateSkeleton; | |
} | |
} | |
protected virtual void OnDisable() | |
{ | |
if (_started && _hand != null) | |
{ | |
Hand.WhenHandUpdated -= UpdateSkeleton; | |
} | |
} | |
private void UpdateVisibility() | |
{ | |
if (!Hand.IsTrackedDataValid) | |
{ | |
if (IsVisible || ForceOffVisibility) | |
{ | |
_skinnedMeshRenderer.enabled = false; | |
} | |
} | |
else | |
{ | |
if (!IsVisible && !ForceOffVisibility) | |
{ | |
_skinnedMeshRenderer.enabled = true; | |
} | |
else if (IsVisible && ForceOffVisibility) | |
{ | |
_skinnedMeshRenderer.enabled = false; | |
} | |
} | |
} | |
public void UpdateSkeleton() | |
{ | |
UpdateVisibility(); | |
if (!Hand.IsTrackedDataValid) | |
{ | |
WhenHandVisualUpdated.Invoke(); | |
return; | |
} | |
if (_updateRootPose) | |
{ | |
if (_root != null && Hand.GetRootPose(out Pose handRootPose)) | |
{ | |
// Original OVR code. This incorrectly uses the local-space pose | |
// to position the hands. The result is the synthetic hands do not | |
// follow the player's actual controller position as the player | |
// moves and rotates. | |
//_root.position = handRootPose.position; | |
//_root.rotation = handRootPose.rotation; | |
// Fixed code. This transforms the tracking-space values to world | |
// space before assigning them to hands. | |
Pose worldPose = | |
_trackingToWorldTransformer.ToWorldPose(handRootPose); | |
_root.SetPositionAndRotation( | |
worldPose.position, | |
worldPose.rotation | |
); | |
} | |
} | |
if (_updateRootScale) | |
{ | |
if (_root != null) | |
{ | |
float parentScale = _root.parent != null ? _root.parent.lossyScale.x : 1f; | |
_root.localScale = Hand.Scale / parentScale * Vector3.one; | |
} | |
} | |
if (!Hand.GetJointPosesLocal(out ReadOnlyHandJointPoses localJoints)) | |
{ | |
return; | |
} | |
for (var i = 0; i < Constants.NUM_HAND_JOINTS; ++i) | |
{ | |
if (_jointTransforms[i] == null) | |
{ | |
continue; | |
} | |
_jointTransforms[i].SetPose(localJoints[i], Space.Self); | |
} | |
if (_handMaterialPropertyBlockEditor != null) | |
{ | |
_handMaterialPropertyBlockEditor.MaterialPropertyBlock.SetFloat(_wristScalePropertyId, Hand.Scale); | |
_handMaterialPropertyBlockEditor.UpdateMaterialPropertyBlock(); | |
} | |
WhenHandVisualUpdated.Invoke(); | |
} | |
public Transform GetTransformByHandJointId(HandJointId handJointId) | |
{ | |
return _jointTransforms[(int)handJointId]; | |
} | |
public Pose GetJointPose(HandJointId jointId, Space space) | |
{ | |
return GetTransformByHandJointId(jointId).GetPose(space); | |
} | |
#region Inject | |
public void InjectAllHandSkeletonVisual(IHand hand, SkinnedMeshRenderer skinnedMeshRenderer) | |
{ | |
InjectHand(hand); | |
InjectSkinnedMeshRenderer(skinnedMeshRenderer); | |
} | |
public void InjectHand(IHand hand) | |
{ | |
_hand = hand as UnityEngine.Object; | |
Hand = hand; | |
} | |
public void InjectSkinnedMeshRenderer(SkinnedMeshRenderer skinnedMeshRenderer) | |
{ | |
_skinnedMeshRenderer = skinnedMeshRenderer; | |
} | |
public void InjectOptionalUpdateRootPose(bool updateRootPose) | |
{ | |
_updateRootPose = updateRootPose; | |
} | |
public void InjectOptionalUpdateRootScale(bool updateRootScale) | |
{ | |
_updateRootScale = updateRootScale; | |
} | |
public void InjectOptionalRoot(Transform root) | |
{ | |
_root = root; | |
} | |
public void InjectOptionalMaterialPropertyBlockEditor(MaterialPropertyBlockEditor editor) | |
{ | |
_handMaterialPropertyBlockEditor = editor; | |
} | |
#endregion | |
} // class WorldSpaceHandVisual | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Oculus.Interaction.Input; | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
using UnityEngine; | |
[CustomEditor(typeof(WorldSpaceHandVisual))] | |
public class WorldSpaceHandVisualEditor : Editor | |
{ | |
private SerializedProperty _handProperty; | |
private SerializedProperty _rootProperty; | |
private IHand Hand => _handProperty.objectReferenceValue as IHand; | |
private void OnEnable() | |
{ | |
_handProperty = serializedObject.FindProperty("_hand"); | |
_rootProperty = serializedObject.FindProperty("_root"); | |
} | |
public override void OnInspectorGUI() | |
{ | |
DrawPropertiesExcluding(serializedObject); | |
serializedObject.ApplyModifiedProperties(); | |
WorldSpaceHandVisual visual = (WorldSpaceHandVisual)target; | |
InitializeSkeleton(visual); | |
if (Hand == null) | |
{ | |
return; | |
} | |
if (GUILayout.Button("Auto Map Joints")) | |
{ | |
AutoMapJoints(visual); | |
EditorUtility.SetDirty(visual); | |
EditorSceneManager.MarkSceneDirty(visual.gameObject.scene); | |
} | |
EditorGUILayout.LabelField("Joints", EditorStyles.boldLabel); | |
HandJointId start = HandJointId.HandStart; | |
HandJointId end = HandJointId.HandEnd; | |
for (int i = (int)start; i < (int)end; ++i) | |
{ | |
string jointName = HandJointLabelFromJointId((HandJointId)i); | |
visual.Joints[i] = (Transform)EditorGUILayout.ObjectField(jointName, | |
visual.Joints[i], typeof(Transform), true); | |
} | |
} | |
private static readonly string[] _fbxHandSidePrefix = { "l_", "r_" }; | |
private static readonly string _fbxHandBonePrefix = "b_"; | |
private static readonly string[] _fbxHandBoneNames = | |
{ | |
"wrist", | |
"forearm_stub", | |
"thumb0", | |
"thumb1", | |
"thumb2", | |
"thumb3", | |
"index1", | |
"index2", | |
"index3", | |
"middle1", | |
"middle2", | |
"middle3", | |
"ring1", | |
"ring2", | |
"ring3", | |
"pinky0", | |
"pinky1", | |
"pinky2", | |
"pinky3" | |
}; | |
private static readonly string[] _fbxHandFingerNames = | |
{ | |
"thumb", | |
"index", | |
"middle", | |
"ring", | |
"pinky" | |
}; | |
private void InitializeSkeleton(WorldSpaceHandVisual visual) | |
{ | |
if (visual.Joints.Count == 0) | |
{ | |
for (int i = (int)HandJointId.HandStart; i < (int)HandJointId.HandEnd; ++i) | |
{ | |
visual.Joints.Add(null); | |
} | |
} | |
} | |
private void AutoMapJoints(WorldSpaceHandVisual visual) | |
{ | |
if (Hand == null) | |
{ | |
InitializeSkeleton(visual); | |
return; | |
} | |
Transform rootTransform = visual.transform; | |
if (_rootProperty.objectReferenceValue != null) | |
{ | |
rootTransform = _rootProperty.objectReferenceValue as Transform; | |
} | |
for (int i = (int)HandJointId.HandStart; i < (int)HandJointId.HandEnd; ++i) | |
{ | |
string fbxBoneName = FbxBoneNameFromHandJointId(visual, (HandJointId)i); | |
Transform t = rootTransform.FindChildRecursive(fbxBoneName); | |
visual.Joints[i] = t; | |
} | |
} | |
private string FbxBoneNameFromHandJointId(WorldSpaceHandVisual visual, HandJointId handJointId) | |
{ | |
if (handJointId >= HandJointId.HandThumbTip && handJointId <= HandJointId.HandPinkyTip) | |
{ | |
return _fbxHandSidePrefix[(int)Hand.Handedness] + _fbxHandFingerNames[(int)handJointId - (int)HandJointId.HandThumbTip] + "_finger_tip_marker"; | |
} | |
else | |
{ | |
return _fbxHandBonePrefix + _fbxHandSidePrefix[(int)Hand.Handedness] + _fbxHandBoneNames[(int)handJointId]; | |
} | |
} | |
// force aliased enum values to the more appropriate value | |
private static string HandJointLabelFromJointId(HandJointId handJointId) | |
{ | |
switch (handJointId) | |
{ | |
case HandJointId.HandWristRoot: | |
return "HandWristRoot"; | |
case HandJointId.HandForearmStub: | |
return "HandForearmStub"; | |
case HandJointId.HandThumb0: | |
return "HandThumb0"; | |
case HandJointId.HandThumb1: | |
return "HandThumb1"; | |
case HandJointId.HandThumb2: | |
return "HandThumb2"; | |
case HandJointId.HandThumb3: | |
return "HandThumb3"; | |
case HandJointId.HandIndex1: | |
return "HandIndex1"; | |
case HandJointId.HandIndex2: | |
return "HandIndex2"; | |
case HandJointId.HandIndex3: | |
return "HandIndex3"; | |
case HandJointId.HandMiddle1: | |
return "HandMiddle1"; | |
case HandJointId.HandMiddle2: | |
return "HandMiddle2"; | |
case HandJointId.HandMiddle3: | |
return "HandMiddle3"; | |
case HandJointId.HandRing1: | |
return "HandRing1"; | |
case HandJointId.HandRing2: | |
return "HandRing2"; | |
case HandJointId.HandRing3: | |
return "HandRing3"; | |
case HandJointId.HandPinky0: | |
return "HandPinky0"; | |
case HandJointId.HandPinky1: | |
return "HandPinky1"; | |
case HandJointId.HandPinky2: | |
return "HandPinky2"; | |
case HandJointId.HandPinky3: | |
return "HandPinky3"; | |
case HandJointId.HandThumbTip: | |
return "HandThumbTip"; | |
case HandJointId.HandIndexTip: | |
return "HandIndexTip"; | |
case HandJointId.HandMiddleTip: | |
return "HandMiddleTip"; | |
case HandJointId.HandRingTip: | |
return "HandRingTip"; | |
case HandJointId.HandPinkyTip: | |
return "HandPinkyTip"; | |
default: | |
return "HandUnknown"; | |
} | |
} | |
} // class WorldSpaceHandVisualEditor |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment