Skip to content

Instantly share code, notes, and snippets.

@fishtopher
Last active November 24, 2020 12:28
Show Gist options
  • Save fishtopher/eae1ab55b24827660e4a299eecce70d6 to your computer and use it in GitHub Desktop.
Save fishtopher/eae1ab55b24827660e4a299eecce70d6 to your computer and use it in GitHub Desktop.
Tween the Oculus Avatar hands to any custom pose you want
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using Oculus.Avatar;
// Smoothly tween between the "real" hand pose that comes out of the
// Oculus Avatar SDK and a custom grip pose.
//
// ## Why?
// The SDK doesn't natively do this so we have to do it manually here.
// We can't just naively lerp from the current hand pose to our target because
// ovr doesn't update bones unless the C layer says they changed - so there's
// no consistent FROM state, and thus no real control over how far through
// the lerp you want to be
// (i.e. you can't say "lerp 50% please" because the next frame the from
// state will have moved to 50% so the request for 50% is now really for 75%
// and so you'll always tend towards the TO state no matter how much you
// actually want to lerp.)
//
// ## Overview
// Roughtly we:
// - Make a copy of the hand skeleton
// - When the OvrAvatar updates, update our copy to match.
// - Use our copy as the FROM version in a lerp TO our custom pose
// - Set the rendered skeleton to whatever our lerped versions are
//
// ## How
// To make a custom hand pose, follow Oculus' instructions:
// https://developer.oculus.com/documentation/avatarsdk/latest/concepts/avatars-sdk-unity/#avatars-sdk-unity-custom-grip-poses
//
// ## TODO / Known Issues
// - Not tested with OvrAvatarSkinnedMeshRenderPBSComponent
// - Multiple target poses or some sort of blend tree would be nice.
// - Limit tweens to specific fingers to allow it to play nice with other hand
// poses (e.g. index finger trigger should only effect index finger).
//
// ## Contact
// Any questions or bug reports - email me!
// Chris McLaughlin - chris@vitei.com
public class OVRAvatarHandPoseOverrider : MonoBehaviour {
[Range(0,1)]
public float m_tweenAmount;
public OvrAvatar m_avatar;
public enum Handedness { Right, Left }
public Handedness m_hand;
public Transform m_gripPose;
OvrAvatarHand m_ovrHand;
OvrAvatarRenderComponent m_ovrHandRenderer;
Transform[] m_bonesTruePose;
IntPtr m_ovrRenderPart = IntPtr.Zero;
[System.Serializable]
public class BoneSet {
public Transform m_rendered;
public Transform m_base;
public Transform m_target;
}
List<BoneSet> m_boneSets;
#if UNITY_EDITOR
Transform m_lastGripPose;
private void OnValidate() {
// if the game is runnnign and the user changes the pose in editor,
// then we want to make sure to update everything so that's what
// they see!
if(Application.isPlaying && m_lastGripPose != m_gripPose) {
SetupBoneSets();
}
m_lastGripPose = m_gripPose;
}
#endif
protected virtual void Start() {
// Get all the relevant components that we're gonna need later on
m_avatar = GetComponent<OvrAvatar>();
m_ovrHand = (m_hand == Handedness.Left) ? m_avatar.HandLeft : m_avatar.HandRight;
}
protected virtual void Update() {
// we need a renderpart before we can do anything else. This doesn't
// exist until the SDK tries to render the hand, so we have to keep
// testing here in update until we can get it.
if (m_ovrRenderPart == IntPtr.Zero){
GetNativeRenderPartPtr();
return;
}
// we need the bonesets setup otherwise there's nothing to actually do
// the work on. Again we need the rendered data before we can do
// this setup.
if (m_boneSets == null) {
SetupBoneSets();
return;
}
// Use the renderpart to look into the avatar sdk adn find out what bones have changed.
// Update our TRUE bones to whatever the sdk says they should be
// We can't use the meshrenderer's bones as the FROM in our lerp
// because the SDK doesn't update them unless _it_ thinks that they
// changed.
ulong dirtyJoints = CAPI.ovrAvatarSkinnedMeshRender_GetDirtyJoints(m_ovrRenderPart);
for (UInt32 i = 0; i < m_bonesTruePose.Length; i++) {
UInt64 dirtyMask = (ulong)1 << (int)i;
// We need to make sure that we fully update the initial position of
// Skinned mesh renderers, then, thereafter, we can only update dirty joints
if ((dirtyMask & dirtyJoints) != 0) {
//This joint is dirty and needs to be updated
Transform targetBone = m_ovrHandRenderer.bones[i];
if (m_bonesTruePose != null && m_bonesTruePose[i] != null) {
m_bonesTruePose[i].localPosition = targetBone.localPosition;
m_bonesTruePose[i].localRotation = targetBone.localRotation;
m_bonesTruePose[i].localScale = targetBone.localScale;
}
}
}
// Loop through all our bones based on how much the trigger is pulled and put them into the right positions!
for (int i = 0; i < m_boneSets.Count; i++) {
// Not lerping POSITION or SCALE because hand bones don't need to, but if you want to do that for some reason, then add it here!
// You could use a SLERP here, but I didn't see any difference in action, so went with the cheaper LERP instead.
m_boneSets[i].m_rendered.localRotation = Quaternion.Lerp(m_boneSets[i].m_base.localRotation, m_boneSets[i].m_target.localRotation, m_tweenAmount);
}
}
//-------------------------------------------------------
// Dive into the avatar sdk and pull out a reference to what I guess is
// the thing that renders a hand. we can then use it to look up how
// dirty it is later on.
void GetNativeRenderPartPtr() {
if (m_avatar.sdkAvatar != IntPtr.Zero) { // null check, otherwise the next line will hard crash the game.
UInt32 componentCount = CAPI.ovrAvatarComponent_Count(m_avatar.sdkAvatar);
for (UInt32 i = 0; i < componentCount; i++) {
IntPtr ptr = CAPI.ovrAvatarComponent_Get_Native(m_avatar.sdkAvatar, i);
ovrAvatarComponent component = (ovrAvatarComponent)System.Runtime.InteropServices.Marshal.PtrToStructure(ptr, typeof(ovrAvatarComponent));
if (component.name == (m_hand == Handedness.Left ? "hand_left" : "hand_right")) {
m_ovrRenderPart = OvrAvatar.GetRenderPart(component, 0); //magic 0
}
}
}
}
// Make a copy of all the bones in the OvrAvatarSkinnedMeshRenderComponent, these will represent the position as the avatar sdk thinks is should be
void CreateTrueBoneArray() {
Transform[] bones = m_ovrHandRenderer.bones;
m_bonesTruePose = new Transform[bones.Length];
for (int i = 0; i < bones.Length; i++) {
m_bonesTruePose[i] = new GameObject(bones[i].name + " (true pose)").transform;
}
List<Transform> bonesAsList = new List<Transform>(bones);
for (int i = 0; i < bones.Length; i++) {
int parentIdx = bonesAsList.IndexOf(bones[i].parent);
if (parentIdx >= 0) {
m_bonesTruePose[i].parent = m_bonesTruePose[parentIdx];
}
else {
m_bonesTruePose[i].parent = bones[i].parent;
}
m_bonesTruePose[i].transform.localPosition = bones[i].transform.localPosition;
m_bonesTruePose[i].transform.localRotation = bones[i].transform.localRotation;
m_bonesTruePose[i].transform.localScale = bones[i].transform.localScale;
}
}
// Make sets of corresponding bones that we'll lerp between.
void SetupBoneSets() {
if(m_ovrHand == null) {
return;
}
// Get the skinnedmeshrenderer because it has all the bone data we need
m_ovrHandRenderer = m_ovrHand.GetComponentInChildren<OvrAvatarRenderComponent>();
if (m_ovrHandRenderer == null) {
return;
}
// Make our copy of the bones to use as the TRUTH
CreateTrueBoneArray();
// Make our bonesets that allow us to lerp between TRUTH and m_gripPose;
m_boneSets = new List<BoneSet>();
for (int i = 0; i < m_ovrHandRenderer.bones.Length; i++) {
BoneSet bs = new BoneSet();
bs.m_rendered = m_ovrHandRenderer.bones[i];
bs.m_base = m_bonesTruePose[i];
bs.m_target = m_gripPose.FindChildRecursive(m_ovrHandRenderer.bones[i].name);
// don't add any that have incomplete data as it will cause errors later on ehwn we're interpolating
if (bs.m_rendered != null && bs.m_base != null && bs.m_target != null) {
m_boneSets.Add(bs);
}
}
}
}
using UnityEngine;
// Stripped this file down to just the fn required for the pose overrider
public static class TransformExtensions {
public static Transform FindChildRecursive(this Transform target, string name) {
if (target.name == name)
return target;
for (int i = 0; i < target.childCount; ++i) {
Transform result = FindChildRecursive(target.GetChild(i), name);
if (result != null)
return result;
}
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment