Files
dungeons/Assets/ThirdParty/UltimateXR/Editor/Manipulation/HandPoses/UxrFingerSpinner.cs

348 lines
14 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrFingerSpinner.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using UltimateXR.Core;
using UltimateXR.Core.Math;
using UltimateXR.Extensions.Unity.Render;
using UnityEngine;
namespace UltimateXR.Editor.Manipulation.HandPoses
{
/// <summary>
/// Finger spinner widget that allows to handle the rotation of finger nodes. It is placed inside a hand image together
/// with all other finger spinners.
/// </summary>
[Serializable]
public class UxrFingerSpinner
{
#region Inspector Properties/Serialized Fields
[SerializeField] private UxrFingerAngleType _angleType;
[SerializeField] private Texture2D _texMouse;
[SerializeField] private Color32 _color;
[SerializeField] private Transform _target;
[SerializeField] private Transform _parent;
[SerializeField] private UxrUniversalLocalAxes _targetLocalAxes;
[SerializeField] private UxrUniversalLocalAxes _parentLocalAxes;
[SerializeField] private float _minAngle;
[SerializeField] private float _maxAngle;
[SerializeField] private UxrHandSide _handSide;
[SerializeField] private bool _isThumbProximal;
[SerializeField] private Vector2 _quadMax;
[SerializeField] private Vector2 _quadMin;
[SerializeField] private float _offset;
#endregion
#region Public Types & Data
/// <summary>
/// Gets the type of angle controlled by the spinner.
/// </summary>
public UxrFingerAngleType Angle => _angleType;
/// <summary>
/// Gets the finger bone transform controlled by the spinner.
/// </summary>
public Transform Target => _target;
/// <summary>
/// Gets which hand the spinner belongs to.
/// </summary>
public UxrHandSide HandSide => _handSide;
/// <summary>
/// Gets the <see cref="Rect" /> of the UI control in local hand image coordinates.
/// </summary>
public Rect MouseRect => new Rect(_quadMin.x, _quadMin.y, _quadMax.x - _quadMin.x, _quadMax.y - _quadMin.y);
/// <summary>
/// Gets or sets the angle value in degrees.
/// </summary>
public float Value
{
get => GetValueFromObject();
set
{
float oldValue = GetValueFromObject();
float newValue = Mathf.Clamp(value, _minAngle, _maxAngle);
if (_angleType == UxrFingerAngleType.Spread)
{
_target.Rotate(_targetLocalAxes.LocalUp, newValue - oldValue, Space.Self);
}
else if (_angleType == UxrFingerAngleType.Curl)
{
_target.Rotate(_targetLocalAxes.LocalRight, newValue - oldValue, Space.Self);
}
}
}
/// <summary>
/// Gets or sets the offset applied to the spinner. It is used to draw the widget instead of using <see cref="Value" />
/// which sometimes propagates slightly from one spinner to another since transforms are related.
/// </summary>
public float Offset
{
get => _offset;
set
{
float forgiveness = 0.1f;
if (Value > _minAngle + forgiveness && Value < _maxAngle - forgiveness)
{
_offset = value;
}
}
}
#endregion
#region Constructors & Finalizer
/// <summary>
/// Constructor.
/// </summary>
/// <param name="angleType">The type of angle this control will rotate (spread or curl)</param>
/// <param name="texMouse">The image that has the clickable parts encoded in colors</param>
/// <param name="color">The color this control has inside the <paramref name="texMouse" /> texture</param>
/// <param name="target">The transform that will be rotated</param>
/// <param name="parent">The parent transform</param>
/// <param name="targetLocalAxes">The universal right/up/forward axes of the target transform</param>
/// <param name="parentLocalAxes">The universal right/up/forward axes of the parent transform</param>
/// <param name="minAngle">The minimum allowed value</param>
/// <param name="maxAngle">The maximum allowed angle</param>
/// <param name="handSide">Which hand</param>
/// <param name="isThumbProximal">Is target a proximal bone from the thumb?</param>
public UxrFingerSpinner(UxrFingerAngleType angleType,
Texture2D texMouse,
Color32 color,
Transform target,
Transform parent,
UxrUniversalLocalAxes targetLocalAxes,
UxrUniversalLocalAxes parentLocalAxes,
float minAngle,
float maxAngle,
UxrHandSide handSide,
bool isThumbProximal = false)
{
_angleType = angleType;
_texMouse = texMouse;
_color = color;
_target = target;
_parent = parent;
_targetLocalAxes = targetLocalAxes;
_parentLocalAxes = parentLocalAxes;
_minAngle = minAngle;
_maxAngle = maxAngle;
_handSide = handSide;
_isThumbProximal = isThumbProximal;
if (handSide == UxrHandSide.Left && s_boundsLeftHand == null)
{
s_boundsLeftHand = GetMouseTextureBounds(texMouse);
}
if (handSide == UxrHandSide.Right && s_boundsRightHand == null)
{
s_boundsRightHand = GetMouseTextureBounds(texMouse);
}
_quadMin = GetBoundsMin(handSide == UxrHandSide.Left ? s_boundsLeftHand : s_boundsRightHand, color);
_quadMax = GetBoundsMax(handSide == UxrHandSide.Left ? s_boundsLeftHand : s_boundsRightHand, color);
}
#endregion
#region Public Methods
/// <summary>
/// Checks whether the mouse position lies within the control bounds.
/// </summary>
/// <param name="mousePos">Mouse position in local hand image coordinates</param>
/// <returns>Whether the mouse position lies within the control bounds</returns>
public bool ContainsMousePos(Vector2 mousePos)
{
return mousePos.x >= _quadMin.x && mousePos.x <= _quadMax.x && mousePos.y >= _quadMin.y && mousePos.y <= _quadMax.y;
/*
* Alternative: Get from mouse texture.
*
if (mousePos.x >= 0 && mousePos.x < _texMouse.width && mousePos.y >= 0 && mousePos.y < _texMouse.height)
{
Color32 texColor = _texMouse.GetPixels32(0)[((_texMouse.height - Mathf.RoundToInt(mousePos.y)) * _texMouse.width) + Mathf.RoundToInt(mousePos.x)];
return texColor.r == _color.r && texColor.g == _color.g && texColor.b == _color.b && texColor.a == _color.a;
}
return false;
*/
}
/// <summary>
/// Computes the rotation angle from the object's current transform.
/// </summary>
/// <returns>Rotation value in degrees</returns>
public float GetValueFromObject()
{
return GetValueFromObject(_angleType);
}
#endregion
#region Private Methods
/// <summary>
/// Computes a dictionary where for a given color a minimum and maximum rect value is stored, representing the corners
/// where the color appears in the texture.
/// </summary>
/// <param name="texture">Texture</param>
/// <returns>Dictionary</returns>
private static Dictionary<int, Bounds> GetMouseTextureBounds(Texture2D texture)
{
Dictionary<int, Bounds> colorBounds = new Dictionary<int, Bounds>();
Color32[] pixels = texture.GetPixels32(0);
for (int x = 0; x < texture.width; ++x)
{
for (int y = 0; y < texture.height; ++y)
{
int color = pixels[(texture.height - y - 1) * texture.width + x].ToInt();
if (!colorBounds.ContainsKey(color))
{
colorBounds.Add(color, new Bounds(new Vector3(x, y, 0.0f), Vector3.zero));
}
Bounds bounds = colorBounds[color];
Vector3 min = bounds.min;
Vector3 max = bounds.max;
if (x < min.x)
{
min.x = x;
}
if (x > max.x)
{
max.x = x;
}
if (y < min.y)
{
min.y = y;
}
if (y > max.y)
{
max.y = y;
}
bounds.min = min;
bounds.max = max;
colorBounds[color] = bounds;
}
}
return colorBounds;
}
/// <summary>
/// Gets the minimum value of a rect for a given color.
/// </summary>
/// <param name="colorBounds">Bounds dictionary</param>
/// <param name="color">Color to get the max bounds value of</param>
/// <returns>Maximum rect x and y for the given color</returns>
private static Vector2 GetBoundsMin(Dictionary<int, Bounds> colorBounds, Color32 color)
{
if (colorBounds.TryGetValue(color.ToInt(), out Bounds bounds))
{
return new Vector2(bounds.min.x, bounds.min.y);
}
return Vector2.zero;
}
/// <summary>
/// Gets the maximum value of a rect for a given color.
/// </summary>
/// <param name="colorBounds">Bounds dictionary</param>
/// <param name="color">Color to get the max bounds value of</param>
/// <returns>Maximum rect x and y for the given color</returns>
private static Vector2 GetBoundsMax(Dictionary<int, Bounds> colorBounds, Color32 color)
{
if (colorBounds.TryGetValue(color.ToInt(), out Bounds bounds))
{
return new Vector2(bounds.max.x, bounds.max.y);
}
return Vector2.zero;
}
/// <summary>
/// Computes a rotation angle from the object's current transform.
/// </summary>
/// <param name="angleType">Angle to compute (spread or curl)</param>
/// <returns>Angle in degrees</returns>
private float GetValueFromObject(UxrFingerAngleType angleType)
{
// Compute roll value by rotating "right" vector from the current forward direction to the initial forward direction. Roll value will be
// the angle between initial right and current right.
// The parent reference directions are different for the proximal thumb bone which is a special case.
Vector3 parentRight = _isThumbProximal
? _handSide == UxrHandSide.Left ? _parent.TransformDirection(-_parentLocalAxes.LocalForward) : _parent.TransformDirection(_parentLocalAxes.LocalForward)
: _parent.TransformDirection(_parentLocalAxes.LocalRight);
Vector3 parentUp = _parent.TransformDirection(_parentLocalAxes.LocalUp);
Vector3 parentForward = _isThumbProximal
? _handSide == UxrHandSide.Left ? _parent.TransformDirection(_parentLocalAxes.LocalRight) : _parent.TransformDirection(-_parentLocalAxes.LocalRight)
: _parent.TransformDirection(_parentLocalAxes.LocalForward);
Vector3 targetRight = _target.TransformDirection(_targetLocalAxes.LocalRight);
Vector3 targetUp = _target.TransformDirection(_targetLocalAxes.LocalUp);
Vector3 targetForward = _target.TransformDirection(_targetLocalAxes.LocalForward);
Quaternion toForward = Quaternion.FromToRotation(targetForward, parentForward);
Vector3 rightOnlyRoll = toForward * targetRight;
float roll = Vector3.SignedAngle(parentRight, rightOnlyRoll, parentForward);
// Compute rotation to remove roll component
Quaternion removeRoll = Quaternion.AngleAxis(-roll, targetForward);
if (angleType == UxrFingerAngleType.Spread)
{
// Compute correct spread angle
Vector3 spreadAxis = removeRoll * targetUp;
Vector3 planeRightNormal = parentRight;
Vector3 spreadVectorOnPlane = Vector3.Cross(planeRightNormal, Vector3.ProjectOnPlane(spreadAxis, planeRightNormal));
return Vector3.SignedAngle(spreadVectorOnPlane, targetForward, spreadAxis);
}
if (angleType == UxrFingerAngleType.Curl)
{
// Compute correct curl angle
Vector3 curlAxis = removeRoll * targetRight;
Vector3 planeUpNormal = parentUp;
Vector3 curlVectorOnPlane = Vector3.Cross(Vector3.ProjectOnPlane(curlAxis, planeUpNormal), planeUpNormal);
return Vector3.SignedAngle(curlVectorOnPlane, targetForward, curlAxis);
}
return 0.0f;
}
#endregion
#region Private Types & Data
private static Dictionary<int, Bounds> s_boundsLeftHand;
private static Dictionary<int, Bounds> s_boundsRightHand;
#endregion
}
}