/*
* 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 UnityEngine;
namespace Oculus.Interaction.Throw
{
///
/// A helper class that uses an underlying to select the best pair of linear
/// and angular velocities from a buffer of recent timed poses.
///
///
/// RANSACVelocity allows for the stable estimation of velocity under noise-prone circumstances. This is particuarly
/// important when throwing objects because of the unreliability of data around the moment of intended release: if the
/// perceived moment of release is slightly too late, for example, the behavior of the throwing implement (hand, controller,
/// etc.) should not impact the calculated velocity. RANSAC velocity estimation across several frames mitigates this problem.
///
public class RANSACVelocity
{
private bool _highConfidenceStreak = false;
private float _lastProcessedTime = 0;
private float _maxSyntheticSpeed = 5.0f;
///
/// Maximum speed (in m/s) allowed for synthetic poses. This is used in internal calculations and should generally not
/// need to be tuned.
///
///
/// Synthetic poses are generated and used by RANSACVelocity to fill tracking gaps (when
/// for a tracked hand is false, for example). Capping the allowed speed
/// mitigates the risk of generating nonsensical synthetic poses.
///
public float MaxSyntheticSpeed
{
get => _maxSyntheticSpeed;
set => _maxSyntheticSpeed = Mathf.Max(_minSyntheticSpeed, value);
}
private const float _minSyntheticSpeed = 0.0001f;
private RandomSampleConsensus _ransac;
private RingBuffer _poses;
[Obsolete("The minHighConfidenceSamples parameter will be ignored. Use the constructor without it")]
public RANSACVelocity(int samplesCount = 10, int samplesDeadZone = 2, int minHighConfidenceSamples = 2)
: this(samplesCount, samplesDeadZone)
{
}
///
/// Creates a new RANSACVelocity for estimating velocity.
///
/// The size of the rolling sample buffer from which to seek consensus.
/// The number of most recent samples to exclude from the consensus.
public RANSACVelocity(int samplesCount = 10, int samplesDeadZone = 2)
{
_poses = new RingBuffer(samplesCount);
_ransac = new RandomSampleConsensus(samplesCount, samplesDeadZone);
}
///
/// Initializes a RANSACVelocity calculator, clearing its state and preparing it to receive new data.
/// Can be called repeatedly to reset the state of an existing RANSACVelocity.
///
public void Initialize()
{
_poses.Clear();
_highConfidenceStreak = false;
}
///
/// Consumes a new frame of Pose data --- for example, from the of a grabbed
/// . The RANSACVelocity instance must be regularly supplied with such data so
/// that velocities can be estimated when needed.
///
/// The Pose observed.
/// The time at which was observed.
///
/// Whether or not was observed with high confidence. For example, if
/// is from an being held by an for which
/// is false, then might also be considered to
/// be known with low confidence.
///
public void Process(Pose pose, float time,
bool isHighConfidence = true)
{
if (_poses.Count > 0 && _poses.Peek().time == time)
{
return;
}
if (!isHighConfidence)
{
_highConfidenceStreak = false;
}
else
{
//first high-confidence frame
if (!_highConfidenceStreak &&
_poses.Count > 0)
{
TimedPose repeatedPose = _poses.Peek();
//remove the dirty data from the buffer
_poses.Clear();
//add a first synthetic pose as if it where from the previous frame
//cap the speed so it is within the allowed limit
float distance = Vector3.Distance(pose.position, repeatedPose.pose.position);
float deltaTime = time - _lastProcessedTime;
if (Mathf.Approximately(deltaTime, 0f)
|| distance / deltaTime > _maxSyntheticSpeed)
{
repeatedPose.time = time - (distance / _maxSyntheticSpeed);
}
else
{
repeatedPose.time = _lastProcessedTime;
}
_poses.Add(repeatedPose);
}
_highConfidenceStreak = true;
TimedPose timedPose = new TimedPose(time, pose);
_poses.Add(timedPose);
}
_lastProcessedTime = time;
}
///
/// Estimates the current translational and rotational velocities based on the available data.
/// If there is insufficient data to produce an estimate, returns 0 trivial velocities.
///
/// Output parameter for translational velocity.
/// Output parameter for rotational velocity.
public void GetVelocities(out Vector3 velocity, out Vector3 torque)
{
if (_poses.Count >= 2)
{
velocity = _ransac.FindOptimalModel(
CalculateVelocityFromSamples, ScoreDistance, _poses.Count);
torque = _ransac.FindOptimalModel(
CalculateTorqueFromSamples, ScoreAngularDistance, _poses.Count);
}
else
{
velocity = Vector3.zero;
torque = Vector3.zero;
}
}
private Vector3 CalculateVelocityFromSamples(int idx1, int idx2)
{
GetSortedTimePoses(idx1, idx2, out TimedPose older, out TimedPose younger);
float timeShift = younger.time - older.time;
Vector3 positionShift = PositionOffset(younger.pose, older.pose);
return positionShift / timeShift;
}
private Vector3 CalculateTorqueFromSamples(int idx1, int idx2)
{
GetSortedTimePoses(idx1, idx2, out TimedPose older, out TimedPose younger);
Vector3 torque = GetTorque(older, younger);
return torque;
}
protected virtual Vector3 PositionOffset(Pose youngerPose, Pose olderPose)
{
return youngerPose.position - olderPose.position;
}
private float ScoreDistance(Vector3 distance, Vector3[,] distances)
{
float score = 0f;
for (int i = 0; i < _poses.Count; ++i)
{
for (int j = i + 1; j < _poses.Count; ++j)
{
score += (distance - distances[i, j]).sqrMagnitude;
}
}
return score;
}
protected void GetSortedTimePoses(int idx1, int idx2,
out TimedPose older, out TimedPose younger)
{
int youngerIdx = idx1;
int olderIdx = idx2;
if (idx2 > idx1)
{
youngerIdx = idx2;
olderIdx = idx1;
}
older = _poses[olderIdx];
younger = _poses[youngerIdx];
}
private float ScoreAngularDistance(Vector3 angularDistance, Vector3[,] angularDistances)
{
float score = 0f;
Quaternion target = Quaternion.Euler(angularDistance);
for (int i = 0; i < _poses.Count; ++i)
{
for (int j = i + 1; j < _poses.Count; ++j)
{
Quaternion sample = Quaternion.Euler(angularDistances[i, j]);
score += Mathf.Abs(Quaternion.Dot(target, sample));
}
}
return score;
}
protected static Vector3 GetTorque(TimedPose older, TimedPose younger)
{
float timeShift = younger.time - older.time;
Quaternion olderRot = older.pose.rotation;
Quaternion youngerRot = younger.pose.rotation;
//Quaternions have two ways of expressing the same rotation.
//This code ensures that the result is the same rotation but expressed in the desired sign.
if (Quaternion.Dot(olderRot, youngerRot) < 0)
{
youngerRot.x = -youngerRot.x;
youngerRot.y = -youngerRot.y;
youngerRot.z = -youngerRot.z;
youngerRot.w = -youngerRot.w;
}
Quaternion deltaRotation = youngerRot * Quaternion.Inverse(olderRot);
deltaRotation.ToAngleAxis(out float angularSpeed, out Vector3 torqueAxis);
angularSpeed = (angularSpeed * Mathf.Deg2Rad) / timeShift;
return torqueAxis * angularSpeed;
}
protected struct TimedPose
{
public float time;
public Pose pose;
public TimedPose(float time, Pose pose)
{
this.time = time;
this.pose = pose;
}
}
}
}