VR4RoboticArm2/VR4RoboticArm/Library/PackageCache/com.meta.xr.sdk.core/Scripts/OVRGLTFAnimatinonNode.cs
IonutMocanu d7aba243a2 Main
2025-09-08 11:04:02 +03:00

498 lines
19 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System.Collections.Generic;
using System;
using System.Linq;
using UnityEngine;
using OVRSimpleJSON;
public enum OVRGLTFInputNode
{
None,
Button_A_X,
Button_B_Y,
Button_Oculus_Menu,
Trigger_Grip,
Trigger_Front,
ThumbStick
};
/// <summary>
/// Helper class specifically used for animating buttons, triggers, and thumbsticks on GLTF (GL Transmission Format) controller models loaded from the Meta Quest Runtime.
/// Controller input is passed in from <see cref="OVRRuntimeController"> which is then used to update the poses of each coresponding button, trigger, or thumbstick on the controller models.
/// <remarks>
/// These animation functions are used specifically for the [Controller Models](https://developer.oculus.com/documentation/unity/unity-runtime-controller/) loaded from the Meta Quest Runtime. We do not recommended using these functions any other GLTF models.
/// </remarks>
/// </summary>
public class OVRGLTFAnimatinonNode
{
private OVRGLTFInputNode m_intputNodeType;
private JSONNode m_jsonData;
private GameObject m_gameObj;
private InputNodeState m_inputNodeState = new InputNodeState();
private OVRGLTFAnimationNodeMorphTargetHandler m_morphTargetHandler;
private List<Vector3> m_translations = new List<Vector3>();
private List<Quaternion> m_rotations = new List<Quaternion>();
private List<Vector3> m_scales = new List<Vector3>();
private List<float> m_weights = new List<float>();
private int m_additiveWeightIndex = -1;
private static Dictionary<OVRGLTFInputNode, int> InputNodeKeyFrames = new Dictionary<OVRGLTFInputNode, int>
{
{ OVRGLTFInputNode.Button_A_X, 5 },
{ OVRGLTFInputNode.Button_B_Y, 8 },
{ OVRGLTFInputNode.Button_Oculus_Menu, 24 },
{ OVRGLTFInputNode.Trigger_Grip, 21 },
{ OVRGLTFInputNode.Trigger_Front, 16 },
{ OVRGLTFInputNode.ThumbStick, 0 }
};
private static List<int> ThumbStickKeyFrames = new List<int> { 29, 39, 34, 40, 31, 36, 32, 37 };
private static Vector2[] CardDirections = new[]
{
new Vector2(0.0f, 0.0f), // none
new Vector2(0.0f, 1.0f), // N
new Vector2(1.0f, 1.0f), // NE
new Vector2(1.0f, 0.0f), // E
new Vector2(1.0f, -1.0f), // SE
new Vector2(0.0f, -1.0f), // S
new Vector2(-1.0f, -1.0f), // SW
new Vector2(-1.0f, 0.0f), // W
new Vector2(-1.0f, 1.0f) // NW
};
private enum ThumbstickDirection
{
None,
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest,
};
private enum OVRGLTFTransformType
{
None,
Translation,
Rotation,
Scale,
Weights
};
private enum OVRInterpolationType
{
None,
LINEAR,
STEP,
CUBICSPLINE
};
private struct InputNodeState
{
public bool down;
public float t;
public Vector2 vecT;
}
/// <summary>
/// Creates a new OVRGLTFAnimationNode object which is used to animate a controller button, trigger, or joystick.
/// </summary>
/// <param name="inputNodeType">Input node type used to specify if this node is a button, trigger, or joystick.</param>
/// <param name="gameObj">Game object associated with the node being animated.</param>
/// <param name="morphTargetHandler">The morpph target data that is required for animating this node.</param>
public OVRGLTFAnimatinonNode(OVRGLTFInputNode inputNodeType,
GameObject gameObj, OVRGLTFAnimationNodeMorphTargetHandler morphTargetHandler)
{
m_intputNodeType = inputNodeType;
m_gameObj = gameObj;
m_morphTargetHandler = morphTargetHandler;
m_translations.Add(CloneVector3(m_gameObj.transform.localPosition));
m_rotations.Add(CloneQuaternion(m_gameObj.transform.localRotation));
m_scales.Add(CloneVector3(m_gameObj.transform.localScale));
}
/// <summary>
/// Adds an animation channel to this animation node. Check the GLTF [animation channel](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_animation_channels) section in the GLTF 2.0 specification page for more information.
/// </summary>
/// <param name="channel">The JSON node containing data for the animation channel.</param>
/// <param name="samplers">The JSON node containing GLTF samplers.</param>
/// <param name="dataAccessor">The OVRGLTFAccessor object for retriving animation data.</param>
public void AddChannel(JSONNode channel, JSONNode samplers, OVRGLTFAccessor dataAccessor)
{
int samplerId = channel["sampler"].AsInt;
var target = channel["target"];
var extras = channel["extras"];
int nodeId = target["node"].AsInt;
OVRGLTFTransformType transformType = GetTransformType(target["path"].Value);
ProcessAnimationSampler(samplers[samplerId], nodeId, transformType, extras, dataAccessor);
return;
}
/// <summary>
/// Updates the pose of a controller button object based on button down state.
/// </summary>
/// <param name="down">If the button is being pressed.</param>
public void UpdatePose(bool down)
{
if (m_inputNodeState.down == down)
return;
m_inputNodeState.down = down;
if (m_translations.Count > 1)
m_gameObj.transform.localPosition = (down ? m_translations[1] : m_translations[0]);
if (m_rotations.Count > 1)
m_gameObj.transform.localRotation = (down ? m_rotations[1] : m_rotations[0]);
if (m_scales.Count > 1)
SetScale((down) ? m_scales[1] : m_scales[0]);
}
/// <summary>
/// Updates the pose of a object based on a float value.
/// </summary>
/// <param name="t">The floating point value ranging from 0.0f to 1.0f.</param>
/// <param name="applyDeadZone">If a dead zone should be applied to the animation. The object will not move until the dead zone threshold is exceeded.</param>
public void UpdatePose(float t, bool applyDeadZone = true)
{
if (applyDeadZone)
{
const float deadZone = 0.05f;
if (Math.Abs(m_inputNodeState.t - t) < deadZone)
return;
}
m_inputNodeState.t = t;
if (m_translations.Count > 1)
m_gameObj.transform.localPosition = Vector3.Lerp(m_translations[0], m_translations[1], t);
if (m_rotations.Count > 1)
m_gameObj.transform.localRotation = Quaternion.Lerp(m_rotations[0], m_rotations[1], t);
if (m_scales.Count > 1)
SetScale(Vector3.Lerp(m_scales[0], m_scales[1], t));
if (m_morphTargetHandler != null && m_weights.Count > 0)
{
// TODO: t assumes an animation channel input of [0,1].
// Changes will be necessary if a model has animations with more keyframes for different timescales
var stride = m_morphTargetHandler.Weights.Length;
if (m_additiveWeightIndex == -1)
{
for (int i = 0; i < stride; i++)
{
m_morphTargetHandler.Weights[i] = Mathf.Lerp(m_weights[i], m_weights[i + stride], t);
}
}
else
{
m_morphTargetHandler.Weights[m_additiveWeightIndex] += Mathf.Lerp(m_weights[m_additiveWeightIndex],
m_weights[m_additiveWeightIndex + stride], t);
}
// mark the geo as dirty
m_morphTargetHandler.MarkModified();
}
}
/// <summary>
/// Updates the pose of a controller joystick object based on the joystick's x and y position.
/// </summary>
/// <param name="joystick">The floating point value of the joystick's x and y position.</param>
public void UpdatePose(Vector2 joystick)
{
const float deadZone = 0.05f;
if (Math.Abs((m_inputNodeState.vecT - joystick).magnitude) < deadZone)
return;
m_inputNodeState.vecT.x = joystick.x;
m_inputNodeState.vecT.y = joystick.y;
if (m_rotations.Count != (int)ThumbstickDirection.NorthWest + 1)
{
Debug.LogError("Wrong joystick animation data.");
return;
}
Tuple<ThumbstickDirection, ThumbstickDirection> dir = GetCardinalThumbsticks(joystick);
Vector2 weights = GetCardinalWeights(joystick, dir);
Quaternion a = CloneQuaternion(m_rotations[0]);
for (int i = 0; i < 2; i++)
{
float t = weights[i];
if (t != 0)
{
int poseIndex = (i == 0 ? (int)dir.Item1 : (int)dir.Item2) - (int)ThumbstickDirection.North;
Quaternion b = m_rotations[poseIndex + 1];
a = Quaternion.Slerp(a, b, t);
}
}
m_gameObj.transform.localRotation = a;
if (m_translations.Count > 1 || m_scales.Count > 1)
Debug.LogWarning("Unsupported pose.");
}
// We will blend the 2 closest animations, this picks which 2.
private Tuple<ThumbstickDirection, ThumbstickDirection> GetCardinalThumbsticks(Vector2 joystick)
{
const float deadZone = 0.005f;
if (joystick.magnitude < deadZone)
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.None,
ThumbstickDirection.None);
}
// East half
if (joystick.x >= 0.0f)
{
// Northeast quadrant
if (joystick.y >= 0.0f)
{
// North-Northeast
if (joystick.y > joystick.x)
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.North,
ThumbstickDirection.NorthEast);
}
// East-Northeast
else
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.NorthEast,
ThumbstickDirection.East);
}
}
// Southeast quadrant
else
{
// East-Southeast
if (joystick.x > -joystick.y)
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.East,
ThumbstickDirection.SouthEast);
}
// South-southeast
else
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.SouthEast,
ThumbstickDirection.South);
}
}
}
// West half
else
{
// Southwest quadrant
if (joystick.y < 0.0f)
{
// South-Southwest
if (joystick.x > joystick.y)
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.South,
ThumbstickDirection.SouthWest);
}
// West-Southwest
else
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.SouthWest,
ThumbstickDirection.West);
}
}
// Northwest quadrant
else
{
// West-Northwest
if (-joystick.x > joystick.y)
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.West,
ThumbstickDirection.NorthWest);
}
// North-Northwest
else
{
return new Tuple<ThumbstickDirection, ThumbstickDirection>(ThumbstickDirection.NorthWest,
ThumbstickDirection.North);
}
}
}
}
// This figures out how much of each of the 2 animations to blend, based on where in between the 2
// cardinal directions the user is actually pushing the thumbstick, and how far they are pushing
// the thumbstick("animations" themselves are a fixed pose for a "maximum" push.
private Vector2 GetCardinalWeights(Vector2 joystick, Tuple<ThumbstickDirection, ThumbstickDirection> cardinals)
{
// follows ThumbstickDirection, can use ThumbstickDirection to directly index into this
if (cardinals.Item1 == ThumbstickDirection.None || cardinals.Item2 == ThumbstickDirection.None)
{
return new Vector2(0.0f, 0.0f);
}
// Compute the barycentric coordinates of the joystick position in the triangle formed by the 2
// cardinal directions
Vector2 triangleEdge1 = CardDirections[(int)(cardinals.Item1)];
Vector2 triangleEdge2 = CardDirections[(int)(cardinals.Item2)];
float dot11 = Vector2.Dot(triangleEdge1, triangleEdge1);
float dot12 = Vector2.Dot(triangleEdge1, triangleEdge2);
float dot1j = Vector2.Dot(triangleEdge1, joystick);
float dot22 = Vector2.Dot(triangleEdge2, triangleEdge2);
float dot2j = Vector2.Dot(triangleEdge2, joystick);
float invDenom = 1.0f / (dot11 * dot22 - dot12 * dot12);
float weight1 = (dot22 * dot1j - dot12 * dot2j) * invDenom;
float weight2 = (dot11 * dot2j - dot12 * dot1j) * invDenom;
return new Vector2(weight1, weight2);
}
private void ProcessAnimationSampler(JSONNode samplerNode, int nodeId, OVRGLTFTransformType transformType,
JSONNode extras, OVRGLTFAccessor _dataAccessor)
{
int outputId = samplerNode["output"].AsInt;
OVRInterpolationType interpolationId = ToOVRInterpolationType(samplerNode["interpolation"].Value);
if (interpolationId == OVRInterpolationType.None)
{
Debug.LogError("Unsupported interpolation type: " + samplerNode["interpolation"].Value);
return;
}
int inputId = samplerNode["input"].AsInt;
_dataAccessor.Seek(inputId);
float[] inputFloats = _dataAccessor.ReadFloat();
// implementation assumes inputFloats = [0, 1]
if (inputFloats.Length > 2 && m_intputNodeType == OVRGLTFInputNode.None)
{
Debug.LogWarning("Unsupported keyframe count");
}
// Changes will be necessary if a model has animations with more keyframes for different timescales
_dataAccessor.Seek(outputId);
switch (transformType)
{
case OVRGLTFTransformType.Translation:
CopyData(ref m_translations, _dataAccessor.ReadVector3(OVRGLTFLoader.GLTFToUnitySpace));
break;
case OVRGLTFTransformType.Rotation:
CopyData(ref m_rotations, _dataAccessor.ReadQuaterion(OVRGLTFLoader.GLTFToUnitySpace_Rotation));
break;
case OVRGLTFTransformType.Scale:
CopyData(ref m_scales, _dataAccessor.ReadVector3(Vector3.one));
break;
case OVRGLTFTransformType.Weights:
CopyData(ref m_weights, _dataAccessor.ReadFloat());
if (extras != null && extras["additiveWeightIndex"] != null)
{
m_additiveWeightIndex = extras["additiveWeightIndex"].AsInt;
}
if (m_morphTargetHandler != null)
{
m_morphTargetHandler.Weights = new float[m_weights.Count / inputFloats.Length];
}
break;
default:
Debug.LogError("Unsupported transform type: " + transformType.ToString());
break;
}
}
private OVRGLTFTransformType GetTransformType(string transform)
{
switch (transform)
{
case "translation":
return OVRGLTFTransformType.Translation;
case "rotation":
return OVRGLTFTransformType.Rotation;
case "scale":
return OVRGLTFTransformType.Scale;
case "weights":
return OVRGLTFTransformType.Weights;
case "none":
return OVRGLTFTransformType.None;
default:
Debug.LogError("Unsupported transform type: " + transform);
return OVRGLTFTransformType.None;
}
}
private OVRInterpolationType ToOVRInterpolationType(string interpolationType)
{
switch (interpolationType)
{
case "LINEAR":
return OVRInterpolationType.LINEAR;
case "STEP":
Debug.LogError("Unsupported interpolationType type." + interpolationType);
return OVRInterpolationType.STEP;
case "CUBICSPLINE":
Debug.LogError("Unsupported interpolationType type." + interpolationType);
return OVRInterpolationType.CUBICSPLINE;
default:
Debug.LogError("Unsupported interpolationType type." + interpolationType);
return OVRInterpolationType.None;
}
}
private void CopyData<T>(ref List<T> dest, T[] src)
{
if (m_intputNodeType == OVRGLTFInputNode.None)
{
dest = src.ToList();
}
else if (m_intputNodeType == OVRGLTFInputNode.ThumbStick)
{
foreach (int idx in ThumbStickKeyFrames)
{
if (idx < src.Length)
dest.Add(src[idx]);
}
}
else
{
int idx = InputNodeKeyFrames[m_intputNodeType];
if (idx < src.Length)
dest.Add(src[idx]);
}
}
private Vector3 CloneVector3(Vector3 v)
{
return new Vector3(v.x, v.y, v.z);
}
private Quaternion CloneQuaternion(Quaternion q)
{
return new Quaternion(q.x, q.y, q.z, q.w);
}
private void SetScale(Vector3 scale)
{
m_gameObj.transform.localScale = scale;
// disable any zero-scale gameobjects to reduce drawcalls
m_gameObj.SetActive(m_gameObj.transform.localScale != Vector3.zero);
}
}