Forum Discussion
Anonymous
3 years agoMeta Avatars: how to feed custom input?
Hi, I'm prototyping with the new Meta Avatars system and am using the SampleAvatarEntity script to instantiate fake avatars. I'd like to replay head pose, controller states, and/or hand states (p...
Anonymous
3 years agoYes I can but it's very specific to my project and you'll have to do some work on your own to port it to your case. Some things to keep in mind:
1) I emit text files. These are super slow to load and will lock up your application for many seconds. I recommend moving to a binary format and also streaming in the file as chunks.
2) I use Normcore avatars in my project as well (legacy left over stuff I didn't remove), so you will see me looking for objects called "Head", "Left Hand", and "Right Hand". These are just driven directly by the OVRCameraRig's camera and controller components.
RecordAvatarClickHandler.cs:
/*
* RecordAvatarClickHandler.cs
*/
using System.IO;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Normal.Realtime;
public class RecordAvatarClickHandler : MonoBehaviour
{
[SerializeField]
private KeyCode _editorSimulateRecordButtonClickedKey = KeyCode.R;
private List<AvatarRecordingSample> _samples = new List<AvatarRecordingSample>();
private bool _recording = false;
private float _recordingStartedAt = 0;
private Transform _anchor;
private Transform _head;
private Transform _leftHand; // these are obtained from local anchor but must correspond to Oculus hand anchor position else Meta Avatars playback might be wrong
private Transform _rightHand;
private OVRSkeleton _ovrLeftHand;
private OVRSkeleton _ovrRightHand;
private void Start()
{
// Get Oculus hands
foreach (var skeleton in FindObjectsOfType<OVRSkeleton>())
{
if (!skeleton.name.ToLower().Contains("hand"))
{
continue;
}
if (skeleton.name.ToLower().Contains("left"))
{
_ovrLeftHand = skeleton;
}
if (skeleton.name.ToLower().Contains("right"))
{
_ovrRightHand = skeleton;
}
}
}
public void OnButtonClicked()
{
_recording = !_recording;
#if UNITY_EDITOR
if (_recording)
{
StartRecording();
}
else
{
StopRecording();
}
#endif
}
private void StartRecording()
{
Debug.Log("Recording started");
_samples.Clear();
_recordingStartedAt = Time.time;
_anchor = null;
_head = null;
_leftHand = null;
_rightHand = null;
FindLocalAvatarAndAnchor();
}
private void StopRecording()
{
//Trim(); // trim first and last second
SaveToFile();
_samples.Clear();
Debug.Log("Recording stopped");
}
private void FindLocalAvatarAndAnchor()
{
Betterverse.RealtimeAvatarManager avatarManager = FindObjectOfType<Betterverse.RealtimeAvatarManager>();
if (!avatarManager)
{
Debug.LogError("RealtimeAvatarManager not found in scene");
return;
}
Betterverse.RealtimeAvatar ourAvatar = avatarManager.localAvatar;
if (!ourAvatar)
{
Debug.LogError("No local avatar");
return;
}
var anchor = FindObjectOfType<AvatarAnchor>();
if (!anchor)
{
Debug.LogError("No avatar anchor in scene");
return;
}
_anchor = anchor.transform;
// For now, we assume avatar components are: SyncedToRemoteTransforms/{Head,LeftHand,RightHand}
// These objects are driven by Normcore.
Transform remoteTransforms = ourAvatar.transform.FindChildRecursive("SyncedToRemoteTransforms");
if (!remoteTransforms)
{
Debug.LogError("Local avatar lacks expected SyncedToRemoteTransforms node");
return;
}
_head = remoteTransforms.transform.FindChildRecursive("Head");
_leftHand = remoteTransforms.transform.FindChildRecursive("Left Hand");
_rightHand = remoteTransforms.transform.FindChildRecursive("Right Hand");
}
private void SaveToFile()
{
string filepath = Path.Combine(Application.persistentDataPath, "AvatarPoseRecording.txt");
var lines = _samples.Select(sample => sample.Serialize());
try
{
File.WriteAllLines(filepath, lines);
Debug.LogFormat("Wrote recording to file: {0}", filepath);
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
private void LateUpdate()
{
#if UNITY_EDITOR
if (Input.GetKeyDown(_editorSimulateRecordButtonClickedKey))
{
OnButtonClicked();
}
if (_recording)
{
Matrix4x4 anchorPose = Matrix4x4.identity;
Matrix4x4 headPose = Matrix4x4.identity;
Matrix4x4 leftHandPose = Matrix4x4.identity;
Matrix4x4 rightHandPose = Matrix4x4.identity;
if (_anchor)
{
anchorPose = _anchor.localToWorldMatrix;
}
if (_head)
{
headPose = _head.localToWorldMatrix;
}
if (_leftHand)
{
leftHandPose = _leftHand.localToWorldMatrix;
}
if (_rightHand)
{
rightHandPose = _rightHand.localToWorldMatrix;
}
AvatarRecordingSample sample = new AvatarRecordingSample();
sample.time = Time.time - _recordingStartedAt;
sample.leftHandVisible = _leftHand && _leftHand.gameObject.activeSelf;
sample.rightHandVisible = _rightHand && _rightHand.gameObject.activeSelf;
sample.anchorPose = anchorPose;
sample.headPose = headPose;
sample.leftHandPose = leftHandPose;
sample.rightHandPose = rightHandPose;
sample.leftHandBones = GetHandBonePoses(_ovrLeftHand);
sample.rightHandBones = GetHandBonePoses(_ovrRightHand);
_samples.Add(sample);
}
#endif
}
private List<Matrix4x4> GetHandBonePoses(OVRSkeleton skeleton)
{
List<Matrix4x4> poses = new List<Matrix4x4>();
if (skeleton != null)
{
OVRSkeleton.BoneId[] boneOrder = new OVRSkeleton.BoneId[]
{
// Make sure this is consistent with avatar playback code
// (ReplayAvatarInputManager.cs)
OVRSkeleton.BoneId.Hand_WristRoot, // 0
OVRSkeleton.BoneId.Hand_Thumb0, // 1
OVRSkeleton.BoneId.Hand_Thumb1, // 2
OVRSkeleton.BoneId.Hand_Thumb2, // 3
OVRSkeleton.BoneId.Hand_Thumb3, // 4
OVRSkeleton.BoneId.Hand_Index1, // 5
OVRSkeleton.BoneId.Hand_Index2, // 6
OVRSkeleton.BoneId.Hand_Index3, // 7
OVRSkeleton.BoneId.Hand_Middle1, // 8
OVRSkeleton.BoneId.Hand_Middle2, // 9
OVRSkeleton.BoneId.Hand_Middle3, // 10
OVRSkeleton.BoneId.Hand_Ring1, // 11
OVRSkeleton.BoneId.Hand_Ring2, // 12
OVRSkeleton.BoneId.Hand_Ring3, // 13
OVRSkeleton.BoneId.Hand_Pinky0, // 14
OVRSkeleton.BoneId.Hand_Pinky1, // 15
OVRSkeleton.BoneId.Hand_Pinky2, // 16
OVRSkeleton.BoneId.Hand_Pinky3 // 17
};
// Create transform matrices for each bone. We cannot use localToWorld because each bone is
// relative to its parent in a hierarchy and we must preserve that. That is, Hand_Thumb1 is
// a child of Hand_Thumb0, which is a child of Hand_WristRoot, etc.
for (int i = 0; i < boneOrder.Length; i++)
{
Transform boneTransform = skeleton.Bones[(int)boneOrder[i]].Transform;
Matrix4x4 transformMatrix = Matrix4x4.TRS(boneTransform.localPosition, boneTransform.localRotation, boneTransform.localScale);
poses.Add(transformMatrix);
}
}
return poses;
}
}
AvatarRecordingSample.cs:
/*
* AvatarRecordingSample.cs
*/
using System.Collections.Generic;
using UnityEngine;
public struct AvatarRecordingSample
{
public float time;
public bool leftHandVisible;
public bool rightHandVisible;
public Matrix4x4 anchorPose;
public Matrix4x4 headPose;
public Matrix4x4 leftHandPose;
public Matrix4x4 rightHandPose;
public List<Matrix4x4> leftHandBones;
public List<Matrix4x4> rightHandBones;
private void SerializeMatrix(List<string> output, Matrix4x4 matrix)
{
for (int i = 0; i < 4; i++)
{
output.Add(matrix.GetRow(i).x.ToString());
output.Add(matrix.GetRow(i).y.ToString());
output.Add(matrix.GetRow(i).z.ToString());
output.Add(matrix.GetRow(i).w.ToString());
}
}
private static Matrix4x4 DeserializeMatrix(string[] components, int startIdx)
{
Matrix4x4 matrix = Matrix4x4.zero;
for (int i = 0; i < 4; i++)
{
float x = float.Parse(components[startIdx + i * 4 + 0]);
float y = float.Parse(components[startIdx + i * 4 + 1]);
float z = float.Parse(components[startIdx + i * 4 + 2]);
float w = float.Parse(components[startIdx + i * 4 + 3]);
matrix.SetRow(i, new Vector4(x, y, z, w));
}
return matrix;
}
public string Serialize()
{
List<string> components = new List<string>();
components.Add(time.ToString());
components.Add((leftHandVisible ? 1 : 0).ToString());
components.Add((rightHandVisible ? 1 : 0).ToString());
SerializeMatrix(components, anchorPose);
SerializeMatrix(components, headPose);
SerializeMatrix(components, leftHandPose);
SerializeMatrix(components, rightHandPose);
components.Add(leftHandBones.Count.ToString()); // number of left hand bones serialized
foreach (Matrix4x4 pose in leftHandBones)
{
SerializeMatrix(components, pose);
}
components.Add(rightHandBones.Count.ToString()); // number of left hand bones serialized
foreach (Matrix4x4 pose in rightHandBones)
{
SerializeMatrix(components, pose);
}
return string.Join(" ", components);
}
public AvatarRecordingSample(string serialized)
{
string[] components = serialized.Trim().Split(' ');
time = float.Parse(components[0]);
leftHandVisible = int.Parse(components[1]) != 0;
rightHandVisible = int.Parse(components[2]) != 0;
anchorPose = DeserializeMatrix(components, 3 + 0 * 16);
headPose = DeserializeMatrix(components, 3 + 1 * 16);
leftHandPose = DeserializeMatrix(components, 3 + 2 * 16);
rightHandPose = DeserializeMatrix(components, 3 + 3 * 16);
leftHandBones = new List<Matrix4x4>();
rightHandBones = new List<Matrix4x4>();
int idx = 3 + 4 * 16 + 0;
if (idx < components.Length)
{
int numLeftHandBones = int.Parse(components[idx++]);
for (int i = 0; i < numLeftHandBones; i++)
{
leftHandBones.Add(DeserializeMatrix(components, idx));
idx += 16;
}
}
if (idx < components.Length)
{
int numRightHandBones = int.Parse(components[idx++]);
for (int i = 0; i < numRightHandBones; i++)
{
rightHandBones.Add(DeserializeMatrix(components, idx));
idx += 16;
}
}
// If the number of bones is not 18, clear
if (leftHandBones.Count != 18)
{
leftHandBones.Clear();
}
if (rightHandBones.Count != 18)
{
rightHandBones.Clear();
}
}
}
Matrix4x4Extensions.cs:
using UnityEngine;
public static class Matrix4x4Extensions
{
public static Vector3 Translation(this Matrix4x4 matrix)
{
Vector4 position = matrix.GetColumn(3);
return new Vector3(position.x, position.y, position.z);
}
public static Vector3 Scale(this Matrix4x4 matrix)
{
Vector3 scale;
scale.x = new Vector4(matrix.m00, matrix.m10, matrix.m20, matrix.m30).magnitude;
scale.y = new Vector4(matrix.m01, matrix.m11, matrix.m21, matrix.m31).magnitude;
scale.z = new Vector4(matrix.m02, matrix.m12, matrix.m22, matrix.m32).magnitude;
return scale;
}
public static Quaternion Rotation(this Matrix4x4 matrix)
{
Vector3 forward;
forward.x = matrix.m02;
forward.y = matrix.m12;
forward.z = matrix.m22;
Vector3 up;
up.x = matrix.m01;
up.y = matrix.m11;
up.z = matrix.m21;
return Quaternion.LookRotation(forward, up);
}
public static Vector3 Forward(this Matrix4x4 matrix)
{
Vector4 column = matrix.GetColumn(2);
return new Vector3(column.x, column.y, column.z).normalized;
}
public static Vector3 Up(this Matrix4x4 matrix)
{
Vector4 column = matrix.GetColumn(1);
return new Vector3(column.x, column.y, column.z).normalized;
}
public static Vector3 Right(this Matrix4x4 matrix)
{
Vector4 column = matrix.GetColumn(0);
return new Vector3(column.x, column.y, column.z).normalized;
}
}
Good luck.
Quick Links
- Horizon Developer Support
- Quest User Forums
- Troubleshooting Forum for problems with a game or app
- Quest Support for problems with your device
Other Meta Support
Related Content
- 7 months ago
- 2 months ago
- 15 days ago
- 9 months ago