/*
* 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;
using System.Collections.Generic;
using UnityEngine;
using Oculus.Interaction.Body.Input;
namespace Oculus.Interaction.Body.PoseDetection
{
///
/// Compares a user-provided set of joints between two Body Poses.
/// You can select which joints to monitor and what the maximum angle delta between each joint should be.
/// If all joints are within this maximum range, the IActiveState becomes Active.
///
public class BodyPoseComparerActiveState :
MonoBehaviour, IActiveState, ITimeConsumer
{
public struct BodyPoseComparerFeatureState
{
public readonly float Delta;
public readonly float MaxDelta;
public BodyPoseComparerFeatureState(float delta, float maxDelta)
{
Delta = delta;
MaxDelta = maxDelta;
}
}
[Serializable]
public class JointComparerConfig
{
[Tooltip("The joint to compare from each Body Pose")]
public BodyJointId Joint = BodyJointId.Body_Head;
[Min(0)]
[Tooltip("The maximum angle that two joint rotations " +
"can be from each other to be considered equal.")]
public float MaxDelta = 30f;
[Tooltip("The width of the threshold when transitioning " +
"states. " + nameof(Width) + " / 2 is added to " +
nameof(MaxDelta) + " when leaving Active state, and " +
"subtracted when entering.")]
[Min(0)]
public float Width = 4f;
}
///
/// The first body pose to compare.
///
[Tooltip("The first body pose to compare.")]
[SerializeField, Interface(typeof(IBodyPose))]
private UnityEngine.Object _poseA;
private IBodyPose PoseA;
///
/// The second body pose to compare.
///
[Tooltip("The second body pose to compare.")]
[SerializeField, Interface(typeof(IBodyPose))]
private UnityEngine.Object _poseB;
private IBodyPose PoseB;
///
/// A list of JointComparerConfigs which contains the parameters to test.
///
[SerializeField]
private List _configs =
new List()
{
new JointComparerConfig()
};
///
/// A new state must be maintaned for at least this many seconds before the Active property changes.
/// Prevents unwanted momentary activations/deactivations of the IActiveState.
///
[Tooltip("A new state must be maintaned for at least this " +
"many seconds before the Active property changes.")]
[SerializeField]
private float _minTimeInState = 0.05f;
public float MinTimeInState
{
get => _minTimeInState;
set => _minTimeInState = value;
}
private Func _timeProvider = () => Time.time;
public void SetTimeProvider(Func timeProvider)
{
_timeProvider = timeProvider;
}
public IReadOnlyDictionary FeatureStates =>
_featureStates;
private Dictionary _featureStates =
new Dictionary();
private bool _isActive;
private bool _internalActive;
private float _lastStateChangeTime;
protected virtual void Awake()
{
PoseA = _poseA as IBodyPose;
PoseB = _poseB as IBodyPose;
}
protected virtual void Start()
{
this.AssertField(PoseA, nameof(PoseA));
this.AssertField(PoseB, nameof(PoseB));
}
public bool Active
{
get
{
if (!isActiveAndEnabled)
{
return false;
}
bool wasActive = _internalActive;
_internalActive = true;
foreach (var config in _configs)
{
float maxDelta = wasActive ?
config.MaxDelta + config.Width / 2f :
config.MaxDelta - config.Width / 2f;
bool withinDelta = GetJointDelta(config.Joint, out float delta) &&
Mathf.Abs(delta) <= maxDelta;
_featureStates[config] = new BodyPoseComparerFeatureState(delta, maxDelta);
_internalActive &= withinDelta;
}
float time = _timeProvider();
if (wasActive != _internalActive)
{
_lastStateChangeTime = time;
}
if (time - _lastStateChangeTime >= _minTimeInState)
{
_isActive = _internalActive;
}
return _isActive;
}
}
private bool GetJointDelta(BodyJointId joint, out float delta)
{
if (!PoseA.GetJointPoseLocal(joint, out Pose localA) ||
!PoseB.GetJointPoseLocal(joint, out Pose localB))
{
delta = 0;
return false;
}
delta = Quaternion.Angle(localA.rotation, localB.rotation);
return true;
}
#region Inject
public void InjectAllBodyPoseComparerActiveState(
IBodyPose poseA, IBodyPose poseB,
IEnumerable configs)
{
InjectPoseA(poseA);
InjectPoseB(poseB);
InjectJoints(configs);
}
public void InjectPoseA(IBodyPose poseA)
{
_poseA = poseA as UnityEngine.Object;
PoseA = poseA;
}
public void InjectPoseB(IBodyPose poseB)
{
_poseB = poseB as UnityEngine.Object;
PoseB = poseB;
}
public void InjectJoints(IEnumerable configs)
{
_configs = new List(configs);
}
[Obsolete("Use SetTimeProvider()")]
public void InjectOptionalTimeProvider(Func timeProvider)
{
_timeProvider = timeProvider;
}
#endregion
}
}