/*
* 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 Oculus.Interaction.Input;
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace Oculus.Interaction.Grab.GrabSurfaces
{
[Serializable]
public class CylinderSurfaceData : ICloneable
{
public object Clone()
{
CylinderSurfaceData clone = new CylinderSurfaceData();
clone.startPoint = this.startPoint;
clone.endPoint = this.endPoint;
clone.arcOffset = this.arcOffset;
clone.arcLength = this.arcLength;
return clone;
}
public CylinderSurfaceData Mirror()
{
CylinderSurfaceData mirror = Clone() as CylinderSurfaceData;
return mirror;
}
public Vector3 startPoint = new Vector3(0f, 0.1f, 0f);
public Vector3 endPoint = new Vector3(0f, -0.1f, 0f);
[Range(0f, 360f)]
public float arcOffset = 0f;
[Range(0f, 360f)]
[FormerlySerializedAs("angle")]
public float arcLength = 360f;
}
///
/// This type of surface defines a cylinder in which the grip pose is valid around an object.
/// An angle and offset can be used to constrain the cylinder and not use a full circle.
/// The radius is automatically specified as the distance from the axis of the cylinder to the original grip position.
///
[Serializable]
public class CylinderGrabSurface : MonoBehaviour, IGrabSurface
{
[SerializeField]
protected CylinderSurfaceData _data = new CylinderSurfaceData();
[SerializeField]
[Tooltip("Transform used as a reference to measure the local data of the grab surface")]
private Transform _relativeTo;
private Pose RelativePose => PoseUtils.DeltaScaled(_relativeTo, this.transform);
private const float Epsilon = 0.000001f;
///
/// The reference pose of the surface. It defines the radius of the cylinder
/// as the point from the relative transform to the reference pose to ensure
/// that the cylinder covers this pose.
///
/// The reference transform to apply the surface to
/// Pose in world space
public Pose GetReferencePose(Transform relativeTo)
{
return PoseUtils.GlobalPoseScaled(relativeTo, RelativePose);
}
///
/// Degrees from the starting radius from which the arc section starts
///
public float ArcOffset
{
get
{
return _data.arcOffset;
}
set
{
if (value != 0 && value % 360f == 0)
{
_data.arcOffset = 360f;
}
else
{
_data.arcOffset = Mathf.Repeat(value, 360f);
}
}
}
///
/// The maximum angle for the surface of the cylinder, starting from the ArcOffset.
/// To invert the direction of the angle, swap the caps order.
///
public float ArcLength
{
get
{
return _data.arcLength;
}
set
{
if (value != 0 && value % 360f == 0)
{
_data.arcLength = 360f;
}
else
{
_data.arcLength = Mathf.Repeat(value, 360f);
}
}
}
///
/// The direction of the main radius of the cylinder. This is the
/// radius from the center of the cylinder to the reference position.
///
private Vector3 LocalPerpendicularDir
{
get
{
return Vector3.ProjectOnPlane(RelativePose.position - _data.startPoint, LocalDirection).normalized;
}
}
///
/// The direction of the central axis of the cylinder in local space.
///
private Vector3 LocalDirection
{
get
{
Vector3 dir = (_data.endPoint - _data.startPoint);
if (dir.sqrMagnitude <= Epsilon)
{
return Vector3.up;
}
return dir.normalized;
}
}
///
/// Direction from the axis of the cylinder to the original grip position.
///
/// The reference transform to apply the surface to
/// Direction in world space
public Vector3 GetPerpendicularDir(Transform relativeTo)
{
return relativeTo.TransformDirection(LocalPerpendicularDir);
}
///
/// Direction from the axis of the cylinder to the minimum angle allowance.
///
/// The reference transform to apply the surface to
/// Direction in world space
public Vector3 GetStartArcDir(Transform relativeTo)
{
Vector3 localStartArcDir = Quaternion.AngleAxis(ArcOffset, LocalDirection) * LocalPerpendicularDir;
return relativeTo.TransformDirection(localStartArcDir);
}
///
/// Direction from the axis of the cylinder to the maximum angle allowance.
///
/// The reference transform to apply the surface to
/// Direction in world space
public Vector3 GetEndArcDir(Transform relativeTo)
{
Vector3 localEndArcDir = Quaternion.AngleAxis(ArcLength, LocalDirection) *
Quaternion.AngleAxis(ArcOffset, LocalDirection) * LocalPerpendicularDir;
return relativeTo.TransformDirection(localEndArcDir);
}
///
/// Base cap of the cylinder, in world coordinates.
///
/// The reference transform to apply the surface to
/// Position in world space
public Vector3 GetStartPoint(Transform relativeTo)
{
return relativeTo.TransformPoint(_data.startPoint);
}
public void SetStartPoint(Vector3 point, Transform relativeTo)
{
_data.startPoint = relativeTo.InverseTransformPoint(point);
}
///
/// End cap of the cylinder, in world coordinates.
///
/// The reference transform to apply the surface to
/// Position in world space
public Vector3 GetEndPoint(Transform relativeTo)
{
return relativeTo.TransformPoint(_data.endPoint);
}
public void SetEndPoint(Vector3 point, Transform relativeTo)
{
_data.endPoint = relativeTo.InverseTransformPoint(point);
}
///
/// The generated radius of the cylinder.
/// Represents the distance from the axis of the cylinder to the original grip position.
///
/// The reference transform to apply the surface to
/// Distance in world space
public float GetRadius(Transform relativeTo)
{
Vector3 start = GetStartPoint(relativeTo);
Pose referencePose = GetReferencePose(relativeTo);
Vector3 direction = GetDirection(relativeTo);
Vector3 projectedPoint = start + Vector3.Project(referencePose.position - start, direction);
return Vector3.Distance(projectedPoint, referencePose.position);
}
///
/// Direction of the cylinder, from the start cap to the end cap.
///
/// The reference transform to apply the surface to
/// Direction in world space
public Vector3 GetDirection(Transform relativeTo)
{
return relativeTo.TransformDirection(LocalDirection);
}
///
/// Length of the cylinder, from the start cap to the end cap.
///
/// The reference transform to apply the surface to
/// Distance in world space
private float GetHeight(Transform relativeTo)
{
Vector3 start = GetStartPoint(relativeTo);
Vector3 end = GetEndPoint(relativeTo);
return Vector3.Distance(start, end);
}
///
/// The rotation of the central axis of the cylinder.
///
/// The reference transform to apply the surface to
/// Rotation in world space
private Quaternion GetRotation(Transform relativeTo)
{
if (_data.startPoint == _data.endPoint)
{
return relativeTo.rotation;
}
return relativeTo.rotation * Quaternion.LookRotation(LocalPerpendicularDir, LocalDirection);
}
#region editor events
protected virtual void Reset()
{
_relativeTo = this.GetComponentInParent()?.RelativeTo;
}
#endregion
protected virtual void Start()
{
this.AssertField(_data, nameof(_data));
this.AssertField(_relativeTo, nameof(_relativeTo));
}
public Pose MirrorPose(in Pose pose, Transform relativeTo)
{
Vector3 mirrorPlane = Vector3.Cross(LocalPerpendicularDir, LocalDirection).normalized;
Quaternion reflectedRot = HandMirroring.Reflect(pose.rotation, mirrorPlane);
return new Pose(pose.position, reflectedRot);
}
private Vector3 PointAltitude(Vector3 point, Transform relativeTo)
{
Vector3 start = GetStartPoint(relativeTo);
Vector3 direction = GetDirection(relativeTo);
Vector3 projectedPoint = start + Vector3.Project(point - start, direction);
return projectedPoint;
}
public GrabPoseScore CalculateBestPoseAtSurface(in Pose targetPose, out Pose bestPose,
in PoseMeasureParameters scoringModifier, Transform relativeTo)
{
return CalculateBestPoseAtSurface(targetPose, Pose.identity, out bestPose,
scoringModifier, relativeTo);
}
public GrabPoseScore CalculateBestPoseAtSurface(in Pose targetPose, in Pose offset, out Pose bestPose,
in PoseMeasureParameters scoringModifier, Transform relativeTo)
{
return GrabPoseHelper.CalculateBestPoseAtSurface(targetPose, offset, out bestPose,
scoringModifier, relativeTo,
MinimalTranslationPoseAtSurface,
MinimalRotationPoseAtSurface);
}
public IGrabSurface CreateMirroredSurface(GameObject gameObject)
{
CylinderGrabSurface surface = gameObject.AddComponent();
surface._data = _data.Mirror();
return surface;
}
public IGrabSurface CreateDuplicatedSurface(GameObject gameObject)
{
CylinderGrabSurface surface = gameObject.AddComponent();
surface._data = _data.Clone() as CylinderSurfaceData;
return surface;
}
protected Vector3 NearestPointInSurface(Vector3 targetPosition, Transform relativeTo)
{
Vector3 start = GetStartPoint(relativeTo);
Vector3 dir = GetDirection(relativeTo);
Vector3 projectedVector = Vector3.Project(targetPosition - start, dir);
float height = GetHeight(relativeTo);
if (projectedVector.magnitude > height)
{
projectedVector = projectedVector.normalized * height;
}
if (Vector3.Dot(projectedVector, dir) < 0f)
{
projectedVector = Vector3.zero;
}
Vector3 projectedPoint = start + projectedVector;
Vector3 targetDirection = Vector3.ProjectOnPlane((targetPosition - projectedPoint), dir).normalized;
Vector3 startArcDir = GetStartArcDir(relativeTo);
float desiredAngle = Mathf.Repeat(Vector3.SignedAngle(startArcDir, targetDirection, dir), 360f);
if (desiredAngle > ArcLength)
{
if (Mathf.Abs(desiredAngle - ArcLength) >= Mathf.Abs(360f - desiredAngle))
{
targetDirection = startArcDir;
}
else
{
targetDirection = GetEndArcDir(relativeTo);
}
}
Vector3 surfacePoint = projectedPoint + targetDirection * GetRadius(relativeTo);
return surfacePoint;
}
public bool CalculateBestPoseAtSurface(Ray targetRay, out Pose bestPose,
Transform relativeTo)
{
Pose recordedPose = GetReferencePose(relativeTo);
Vector3 start = GetStartPoint(relativeTo);
Vector3 direction = GetDirection(relativeTo);
Vector3 lineToCylinder = start - targetRay.origin;
float perpendiculiarity = Vector3.Dot(targetRay.direction, direction);
float rayToLineDiff = Vector3.Dot(lineToCylinder, targetRay.direction);
float cylinderToLineDiff = Vector3.Dot(lineToCylinder, direction);
float determinant = 1f / (perpendiculiarity * perpendiculiarity - 1f);
float lineOffset = (perpendiculiarity * cylinderToLineDiff - rayToLineDiff) * determinant;
float cylinderOffset = (cylinderToLineDiff - perpendiculiarity * rayToLineDiff) * determinant;
float radius = GetRadius(relativeTo);
Vector3 pointInLine = targetRay.origin + targetRay.direction * lineOffset;
Vector3 pointInCylinder = start + direction * cylinderOffset;
float distanceToSurface = Mathf.Max(Vector3.Distance(pointInCylinder, pointInLine) - radius);
if (distanceToSurface < radius)
{
float adjustedDistance = Mathf.Sqrt(radius * radius - distanceToSurface * distanceToSurface);
pointInLine -= targetRay.direction * adjustedDistance;
}
Vector3 surfacePoint = NearestPointInSurface(pointInLine, relativeTo);
Pose desiredPose = new Pose(surfacePoint, recordedPose.rotation);
bestPose = MinimalTranslationPoseAtSurface(desiredPose, relativeTo);
return true;
}
protected Pose MinimalRotationPoseAtSurface(in Pose userPose, Transform relativeTo)
{
Pose referencePose = GetReferencePose(relativeTo);
Vector3 direction = GetDirection(relativeTo);
Quaternion rotation = GetRotation(relativeTo);
float radius = GetRadius(relativeTo);
Vector3 desiredPos = userPose.position;
Quaternion desiredRot = userPose.rotation;
Quaternion baseRot = referencePose.rotation;
Quaternion rotDif = (desiredRot) * Quaternion.Inverse(baseRot);
Vector3 desiredDirection = (rotDif * rotation) * Vector3.forward;
Vector3 projectedDirection = Vector3.ProjectOnPlane(desiredDirection, direction).normalized;
Vector3 altitudePoint = PointAltitude(desiredPos, relativeTo);
Vector3 surfacePoint = NearestPointInSurface(altitudePoint + projectedDirection * radius, relativeTo);
Quaternion surfaceRotation = CalculateRotationOffset(surfacePoint, relativeTo) * baseRot;
return new Pose(surfacePoint, surfaceRotation);
}
protected Pose MinimalTranslationPoseAtSurface(in Pose userPose, Transform relativeTo)
{
Pose referencePose = GetReferencePose(relativeTo);
Vector3 desiredPos = userPose.position;
Quaternion baseRot = referencePose.rotation;
Vector3 surfacePoint = NearestPointInSurface(desiredPos, relativeTo);
Quaternion surfaceRotation = CalculateRotationOffset(surfacePoint, relativeTo) * baseRot;
return new Pose(surfacePoint, surfaceRotation);
}
protected Quaternion CalculateRotationOffset(Vector3 surfacePoint, Transform relativeTo)
{
Vector3 start = GetStartPoint(relativeTo);
Vector3 direction = GetDirection(relativeTo);
Vector3 referenceDir = GetPerpendicularDir(relativeTo);
Vector3 recordedDirection = Vector3.ProjectOnPlane(referenceDir, direction);
Vector3 desiredDirection = Vector3.ProjectOnPlane(surfacePoint - start, direction);
return Quaternion.FromToRotation(recordedDirection, desiredDirection);
}
#region Inject
public void InjectAllCylinderSurface(CylinderSurfaceData data,
Transform relativeTo)
{
InjectData(data);
InjectRelativeTo(relativeTo);
}
public void InjectData(CylinderSurfaceData data)
{
_data = data;
}
public void InjectRelativeTo(Transform relativeTo)
{
_relativeTo = relativeTo;
}
#endregion
}
}