still working on VR interactions, I want to be able to rotate objects but I'm facing an issue.
For instance, I want to open/close the upper part of a laptop using my hands in VR. What I'm doing to achieve this, is that I placed the forward like that :
I'm creating a plane using position, forward, up. Then get the closest point on plane corresponding to my VR controller, then use transform.LookAt.
This is working fine, but I want to be able to clamp the rotation, so I cannot rotate too much (see the end of the video).
I've been trying everything, using eulersAngle and Quaternion, but I'm unable to do it.
I made some helpers (the text to show the localEulerAngles, and a transform to LookAt so I don't have to use the VR headset as it's getting pretty tedious)
Here is a video showing what's going on : https://www.youtube.com/watch?v=UfN97OpYElk
And here's my code :
using UnityEngine;
public class JVRLookAtRotation : MonoBehaviour, IJVRControllerInteract
{
[SerializeField] private Transform toRotate;
[SerializeField] private Vector3 minRotation;
[SerializeField] private Vector3 maxRotation;
[Header("Rotation contraints")]
[SerializeField] private bool lockX;
[SerializeField] private bool lockY;
[SerializeField] private bool lockZ;
private JVRController _jvrController;
private bool _isGrabbed;
private Vector3 _targetPosition;
private Vector3 _tmp;
public Transform followTransform;
private void LateUpdate()
{
/*
if (!_isGrabbed) return;
if (_jvrController.Grip + _jvrController.Trigger < Rules.GrabbingThreshold)
{
_isGrabbed = false;
_jvrController.StopGrabbing();
_jvrController = null;
return;
}
*/
Vector3 up = toRotate.up;
Vector3 forward = toRotate.forward;
Vector3 pos0 = toRotate.position;
Vector3 pos1 = pos0 + up;
Vector3 pos2 = pos0 + forward;
Plane p = new Plane(pos0, pos1, pos2);
// Using followTransform just to no have to use VR, otherwise it's the controller pos
_targetPosition = p.ClosestPointOnPlane(followTransform.position);
toRotate.LookAt(_targetPosition, up);
/*
_tmp = toRotate.localEulerAngles;
_tmp.x = Mathf.Clamp(WrapAngle(_tmp.x), minRotation.x, maxRotation.x);
_tmp.y = WrapAngle(_tmp.y);
_tmp.z = WrapAngle(_tmp.z);
toRotate.localRotation = Quaternion.Euler(_tmp);
*/
}
public void JVRControllerInteract(JVRController jvrController)
{
if (_isGrabbed) return;
if (!(jvrController.Grip + jvrController.Trigger > Rules.GrabbingThreshold)) return;
_jvrController = jvrController;
_jvrController.SetGrabbedObject(this);
_isGrabbed = true;
}
private static float WrapAngle(float angle)
{
angle%=360;
if(angle >180)
return angle - 360;
return angle;
}
private static float UnwrapAngle(float angle)
{
if(angle >=0)
return angle;
angle = -angle%360;
return 360-angle;
}
}
Suppose the monitor's parent transform is the body/keyboard of the laptop. Local axes of the parent shown below:
To describe the range of motion you can define a "center of rotation" vector (e.g., grey vector labeled C) that is local to the parent and an angle (e.g., 110 degrees, between each purple vector and the grey vector). For instance:
[SerializeField] private Vector3 LocalRotationRangeCenter = new Vector3(0f, 0.94f, 0.342f);
[SerializeField] private float RotationRangeExtent = 110f;
Then, you can take the forward vector it "wants" to go, and find the signed angle between the world direction of RotationRangeCenter
and that point, then clamp it to ±RotationRangeExtent
:
Vector3 worldRotationRangeCenter = toRotate.parent.TransformDirection(RotationRangeCenter);
Vector3 targetForward = _targetPosition - toRotate.position;
float targetAngle = Vector3.SignedAngle(worldRotationRangeCenter, targetForward,
toRotate.right);
float clampedAngle = Mathf.Clamp(targetAngle, -RotationRangeExtent, RotationRangeExtent);
Then, find the direction that corresponds to that angle. Finally, rotate the monitor so that its forward aligns with the clamped forward and its right doesn't change. You can use a cross product to find what the monitor's up would be, then use Quaternion.LookRotation
to find the corresponding rotation:
Vector3 clampedForward = Quaternion.AngleAxis(clampedAngle, toRotate.right)
* worldRotationRangeCenter;
toRotate.rotation = Quaternion.LookRotation(clampedForward,
Vector3.Cross(clampedForward, toRotate.right));
If someone tries to drag the monitor too far beyond the "boundaries" it will teleport from one limit to the other. If that's not desired behavior, you might consider interpolating from SignedAngle(worldRotationRangecenter, targetForward, toRotate.right)
to clampedAngle
, for a movement between the limits:
private float angleChangeLimit = 90f; // max angular speed
// ...
Vector3 worldRotationRangeCenter = toRotate.parent.TransformDirection(RotationRangeCenter);
Vector3 targetForward = _targetPosition - toRotate.position;
float targetAngle = Vector3.SignedAngle(worldRotationRangeCenter, targetForward,
toRotate.right);
float clampedAngle = Mathf.Clamp(targetAngle, -RotationRangeExtent, RotationRangeExtent);
float currentAngle = Vector3.SignedAngle(worldRotationRangeCenter, toRotate.forward,
toRotate.right);
clampedAngle = Mathf.MoveTowards(currentAngle, clampedAngle,
angleChangeLimit * Time.deltaTime);
Vector3 clampedForward = Quaternion.AngleAxis(clampedAngle, toRotate.right)
* worldRotationRangeCenter;
toRotate.rotation = Quaternion.LookRotation(clampedForward,
Vector3.Cross(clampedForward, toRotate.right));