Skip to content

Instantly share code, notes, and snippets.

@ryan-at-melcher
Last active July 5, 2024 21:18
Show Gist options
  • Save ryan-at-melcher/773d3073329134c43a60110e101f0557 to your computer and use it in GitHub Desktop.
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.
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
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