Skip to content

Instantly share code, notes, and snippets.

@gszauer
Last active July 10, 2023 11:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gszauer/27e0bfc3dcb3003d608e502559edae82 to your computer and use it in GitHub Desktop.
Save gszauer/27e0bfc3dcb3003d608e502559edae82 to your computer and use it in GitHub Desktop.
Rotation Gizmo
function GetWorldMatrix_CombineExperimental(transform) {
var position = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
transform.position[0], transform.position[1], transform.position[2], 1
];
var x = Mul_QV(transform.rotation, [1, 0, 0]);
var y = Mul_QV(transform.rotation, [0, 1, 0]);
var z = Mul_QV(transform.rotation, [0, 0, 1]);
var rotation = [
x[0], x[1], x[2], 0,
y[0], y[1], y[2], 0,
z[0], z[1], z[2], 0,
0, 0, 0, 1
]
var scale = [
transform.scale[0], 0, 0, 0,
0, transform.scale[1], 0, 0,
0, 0, transform.scale[2], 0,
0, 0, 0, 1
]
var SR = Mul_MM(scale, rotation);
var TSR = Mul_MM(SR, position)
var local = TSR;
var world = local;
transform.worldRotation = Copy_Q(transform.rotation);
if (t
using UnityEngine;
public class Rotator : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0,0,0);
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private bool isRunning = false;
void Awake() {
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
void Update() {
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
if (!isRunning) {
if (Input.GetKeyDown(KeyCode.R) || Input.GetKey(KeyCode.R)) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startRotation = transform.rotation;
isRunning = true;
if (debugStartPoint != null) {
debugStartPoint.SetActive(true);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(true);
}
}
}
}
else {
if (Input.GetKeyUp(KeyCode.R) || !Input.GetKey(KeyCode.R)) {
isRunning = false;
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
else {
Vector3 currentHitPosition = Vector3.zero;
if (!RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out currentHitPosition)) {
// TODO: Make plane to raycast against!
}
/* Start & current are both points on a sphere
A quaternion is constructed like so:
q.w == cos(angle / 2)
q.<xyz> == sin(angle / 2)
The sine & cosine of the angles relate to the
dot & cross products of the start & current vectors.
dot(start, current) == cos(theta)
cross(start, current).<xyz> == sin(theta) * <xyz>
The problem is that the dot product will return in cos(theta),
but we want cos(theta / 2). Same with the cross product.
One way to solve this is to create a half-way vector between
start & current. Let's call it half. Use this to construct
the quaternion that rotates from start to current.
The resulting quaternion must be normalized.
If u == -v, a shortest path is not possible. See this for details:
https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another
*/
Vector3 halfWayVector = startHitPosition + (currentHitPosition - startHitPosition) * 0.5f;
Vector3 perpendicularAxis = Vector3.Cross(startHitPosition, halfWayVector);
Quaternion fromToRotation;
fromToRotation.w = Vector3.Dot(startHitPosition, halfWayVector);
fromToRotation.x = perpendicularAxis.x;
fromToRotation.y = perpendicularAxis.y;
fromToRotation.z = perpendicularAxis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection.normalized);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection.normalized * t;
return true;
}
}
using UnityEngine;
public class RotatorV10 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private Vector3 startPointOnPlane = Vector3.zero;
private Vector3 startPointOnCameraPlane = Vector3.zero;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugConstraintPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
// transform.rotation = Quaternion.identity;
}
void Update() {
// Potentially set the constraint axis
Vector3 constraint = Constraint;
// Don't do anything if there is no constraint!
if (constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane that has the normal facing the camera and always bisects the arcball sphere
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, arcballPosition);
// Find where the world space ray intersects the plane that was created
Vector3 pointOnPlane = Vector3.zero;
RaycastPlane(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnPlane);
Vector3 pointOnCameraPlane = pointOnPlane;
// Using some trig, project the point on the plane onto the hemisphere of the arcball.
// A^2 + B^2 = C^2
// C^2 = Radius^2
// A^2 = distance of point on plane from arcball ^2
// Clamp A^2 to C^2. That way the mouse can never trully leave the sphere
float cSq = arcballRadius * arcballRadius;
float aSq = (arcballPosition - pointOnPlane).sqrMagnitude;
if (aSq > cSq) aSq = cSq;
float bSq = cSq - aSq;
// SampledPoint holds where on the hemisphere the point on plane is. Remember, it's clamped to the edge of the sphere!
Vector3 mapDirection = cameraPlaneNormal;
Vector3 sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * Mathf.Sqrt(aSq) + (mapDirection * Mathf.Sqrt(bSq));
// Construct a plane out of the constrain axis
Vector3 constraintPlaneNormal = constraint.normalized;
float constraintPlaneDistance = Vector3.Dot(constraintPlaneNormal, arcballPosition);
Ray closestPointRay = new Ray();
closestPointRay.origin = sampledPoint;
if (Mathf.Abs (Vector3.Dot (constraintPlaneNormal, cameraPlaneNormal)) < 0.9f) { // NON-EDGE CASE
closestPointRay.direction = Vector3.Cross (Vector3.Cross (cameraPlaneNormal, constraintPlaneNormal), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection (constraintPlaneNormal, constraintPlaneDistance, closestPointRay.origin, closestPointRay.direction, out pointOnPlane);
RaycastPlaneIgnoringRayDirection (constraintPlaneNormal, constraintPlaneDistance, pointOnCameraPlane, closestPointRay.direction, out pointOnCameraPlane);
}
sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * arcballRadius;
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startHitPosition = sampledPoint;
startRotation = transform.rotation;
startPointOnPlane = pointOnPlane; // Will be used for distance calculation
startPointOnCameraPlane = pointOnCameraPlane;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentPointOnCameraPlane = pointOnCameraPlane;
Vector3 currentHitPosition = sampledPoint;
Vector3 startVector = startHitPosition - arcballPosition;
Vector3 currentVector = currentHitPosition - arcballPosition;
Vector3 axis = Vector3.Cross(startVector, currentVector).normalized;
float distance = (startPointOnCameraPlane - currentPointOnCameraPlane).magnitude;
float turns = distance / (4f * arcballRadius); // Every 4R of distance = 1 full rotation! Unit is currently in number of rotations.
float angle = turns * 6.28319f; // convert rotations to radians. One full rotation = 6.28 radians.
//Debug.Log("Angle: " + angle);
float hCos = Mathf.Cos(angle * 0.5f);
float hSin = Mathf.Sin(angle * 0.5f);
// Create the actual quaternion from the above values. This isn't a from-to rotation per-say. We still use the same vectors to figure out the
// angle of orientation, but the actual angle comes from distance of mouse movement.
Quaternion fromToRotation = Quaternion.identity;
fromToRotation.w = hCos;
fromToRotation.x = hSin * axis.x;
fromToRotation.y = hSin * axis.y;
fromToRotation.z = hSin * axis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
if (Mathf.Abs (Vector3.Dot (constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) { // EDGE CASE
fromToRotation = HalfQuaternionMethod (startVector, currentVector);
fromToRotation = NormalizeQuaternion(fromToRotation);
}
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;//startPointOnCameraPlane;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;//currentPointOnCameraPlane;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startPointOnPlane;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startPointOnPlane;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
debugPlane.transform.position = transform.position;
}
if (debugConstraintPlane != null) {
debugConstraintPlane.SetActive(value && debugShowPlane);
debugConstraintPlane.transform.position = transform.position;
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
// Quaternion.fromToRotation
protected Quaternion HalfQuaternionMethod(Vector3 v0, Vector3 v1) {
Quaternion result = Quaternion.identity;
Vector3 axis = Vector3.zero;
float k_cos_theta = Vector3.Dot(v0, v1);
float k = Mathf.Sqrt(v0.sqrMagnitude * v1.sqrMagnitude);
if (k_cos_theta / k == -1) {
result.w = 0.0f;
axis = OrthogonalAxis(v0).normalized;
}
else {
result.w = k_cos_theta + k;
axis = Vector3.Cross(v0, v1);
}
result.x = axis.x;
result.y = axis.y;
result.z = axis.z;
return NormalizeQuaternion(result);
}
protected Vector3 OrthogonalAxis(Vector3 v) {
float x = Mathf.Abs(v.x);
float y = Mathf.Abs(v.y);
float z = Mathf.Abs(v.z);
Vector3 other = Vector3.zero;
if (x < y) {
if (x < z) {
other = Vector3.right; // x axis
}
else {
other = Vector3.forward; // z axis
}
}
else {
if (y < z) {
other = Vector3.up; // y axis
}
else {
other = Vector3.forward; // z axis
}
}
return Vector3.Cross(v, other);
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
if (debugConstraintPlane != null && IsRotationActive) {
debugConstraintPlane.transform.localRotation = Quaternion.LookRotation(Camera.main.transform.forward * -1.0f, Vector3.up);
}
return _constraint;
}
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastPlaneIgnoringRayDirection(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
//if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
//}
//return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV2 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0,0,0);
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private bool isRunning = false;
void Awake() {
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
void Update() {
Vector3 mousePos = Input.mousePosition;
Vector3 radiusWorldPosition = arcballPosition + Camera.main.transform.right * arcballRadius;
Vector3 arcScreenPosition = Camera.main.WorldToScreenPoint(arcballPosition);
arcScreenPosition.z = 0.0f;
Vector3 radiusScreenPosition = Camera.main.WorldToScreenPoint(radiusWorldPosition);
radiusScreenPosition.z = 0.0f;
float screenRadius = (radiusScreenPosition - arcScreenPosition).magnitude;
Vector3 pointOnSphere = Vector3.zero;
pointOnSphere.x = (mousePos.x - arcScreenPosition.x) / screenRadius;
pointOnSphere.y = (mousePos.y - arcScreenPosition.y) / screenRadius;
float r = pointOnSphere.x * pointOnSphere.x + pointOnSphere.y * pointOnSphere.y;
if (r > 1.0f) {
float s = 1.0f / Mathf.Sqrt(r);
pointOnSphere.x = pointOnSphere.x * s;
pointOnSphere.y = pointOnSphere.y * s;
pointOnSphere.z = 0.0f;
}
else {
pointOnSphere.z = Mathf.Sqrt(1.0f - r);
}
pointOnSphere.z *= -1.0f;
if (!isRunning) {
if (Input.GetKeyDown(KeyCode.R) || Input.GetKey(KeyCode.R)) {
startHitPosition = pointOnSphere;
startRotation = transform.rotation;
isRunning = true;
if (debugStartPoint != null) {
debugStartPoint.SetActive(true);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(true);
}
}
}
else {
if (Input.GetKeyUp(KeyCode.R) || !Input.GetKey(KeyCode.R)) {
isRunning = false;
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
else {
Vector3 currentHitPosition = pointOnSphere;
Vector3 halfWayVector = startHitPosition + (currentHitPosition - startHitPosition) * 0.5f;
Vector3 perpendicularAxis = Vector3.Cross(startHitPosition, halfWayVector);
Quaternion fromToRotation;
fromToRotation.w = Vector3.Dot(startHitPosition, halfWayVector);
fromToRotation.x = perpendicularAxis.x;
fromToRotation.y = perpendicularAxis.y;
fromToRotation.z = perpendicularAxis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
}
using UnityEngine;
public class RotatorV3 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private bool isRunning = false;
void Awake() {
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
}
void Update() {
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
if (!isRunning) {
if (Input.GetKeyDown(KeyCode.R) || Input.GetKey(KeyCode.R)) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startRotation = transform.rotation;
isRunning = true;
if (debugStartPoint != null) {
debugStartPoint.SetActive(true);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(true);
}
}
}
}
else {
if (Input.GetKeyUp(KeyCode.R) || !Input.GetKey(KeyCode.R)) {
isRunning = false;
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
else {
Vector3 currentHitPosition = Vector3.zero;
if (!RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out currentHitPosition)) {
// Plane bisects sphere in half
Vector3 planeNormal = Camera.main.transform.forward * -1.0f; // This should probably be sphere forward?
float planeDistance = Vector3.Dot(planeNormal, arcballPosition);
if (!RaycastPlane(planeNormal, planeDistance, screenRay.origin, screenRay.direction, out currentHitPosition)) {
Debug.LogError("Mouse did not intersect either sphere or plane");
}
}
// Don't test an absolute position. Everything should be relative to the center of the sphere
Vector3 startVector = startHitPosition - arcballPosition;
Vector3 currentVector = currentHitPosition - arcballPosition;
Quaternion fromToRotation = HalfQuaternionMethod(startVector, currentVector);
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Vector3 ClosestPointOnRay(Vector3 point, Vector3 rayOrigin, Vector3 rayDirection) {
rayDirection = rayDirection.normalized;
float t = Vector3.Dot(point - rayOrigin, rayDirection);
t = Mathf.Max(t, 0.0f);
return rayOrigin + rayDirection * t;
}
// Quaternion.fromToRotation
protected Quaternion NaiveMethod(Vector3 v0, Vector3 v1) {
Vector3 halfWayVector = v0 + (v1 - v0) * 0.5f;
Vector3 perpendicularAxis = Vector3.Cross(v0, halfWayVector);
Quaternion result = Quaternion.identity;
result.w = Vector3.Dot(v0, halfWayVector);
result.x = perpendicularAxis.x;
result.y = perpendicularAxis.y;
result.z = perpendicularAxis.z;
return NormalizeQuaternion(result);
}
// Quaternion.fromToRotation
protected Quaternion HalfVectorMethod(Vector3 v0, Vector3 v1) {
Quaternion result = Quaternion.identity;
Vector3 axis = Vector3.zero;
v0 = v0.normalized;
v1 = v1.normalized;
if (v0 == v1 * -1.0f) {
result.w = 0.0f;
axis = OrthogonalAxis(v0).normalized;
}
else {
Vector3 half = (v0 + v1).normalized;
result.w = Vector3.Dot(v0, half);
axis = Vector3.Cross(v0, half);
}
result.x = axis.x;
result.y = axis.y;
result.z = axis.z;
return NormalizeQuaternion(result);
}
// Quaternion.fromToRotation
protected Quaternion HalfQuaternionMethod(Vector3 v0, Vector3 v1) {
Quaternion result = Quaternion.identity;
Vector3 axis = Vector3.zero;
float k_cos_theta = Vector3.Dot(v0, v1);
float k = Mathf.Sqrt(v0.sqrMagnitude * v1.sqrMagnitude);
if (k_cos_theta / k == -1) {
result.w = 0.0f;
axis = OrthogonalAxis(v0).normalized;
}
else {
result.w = k_cos_theta + k;
axis = Vector3.Cross(v0, v1);
}
result.x = axis.x;
result.y = axis.y;
result.z = axis.z;
return NormalizeQuaternion(result);
}
protected Vector3 OrthogonalAxis(Vector3 v) {
float x = Mathf.Abs(v.x);
float y = Mathf.Abs(v.y);
float z = Mathf.Abs(v.z);
Vector3 other = Vector3.zero;
if (x < y) {
if (x < z) {
other = Vector3.right; // x axis
}
else {
other = Vector3.forward; // z axis
}
}
else {
if (y < z) {
other = Vector3.up; // y axis
}
else {
other = Vector3.forward; // z axis
}
}
return Vector3.Cross(v, other);
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV4 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private Vector3 startPointOnPlane = Vector3.zero;
private bool isRunning = false;
void Awake() {
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
}
void Update() {
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane that has the normal facing the camera and always bisects the arcball sphere
Vector3 planeNormal = Camera.main.transform.forward * -1.0f;
float planeDistance = Vector3.Dot(planeNormal, arcballPosition);
// Find where the world space ray intersects the plane that was created
Vector3 pointOnPlane = Vector3.zero;
RaycastPlane(planeNormal, planeDistance, screenRay.origin, screenRay.direction, out pointOnPlane);
// Using some trig, project the point on the plane onto the hemisphere of the arcball.
// A^2 + B^2 = C^2
// C^2 = Radius^2
// A^2 = distance of point on plane from arcball ^2
// Clamp A^2 to C^2. That way the mouse can never trully leave the sphere
float cSq = arcballRadius * arcballRadius;
float aSq = (arcballPosition - pointOnPlane).sqrMagnitude;
if (aSq > cSq) aSq = cSq;
float bSq = cSq - aSq;
// SampledPoint holds where on the hemisphere the point on plane is. Remember, it's clamped to the edge of the sphere!
Vector3 sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * Mathf.Sqrt(aSq) + (planeNormal * Mathf.Sqrt(bSq));
if (!isRunning) {
if (Input.GetKeyDown(KeyCode.R) || Input.GetKey(KeyCode.R)) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
// startHitPosition is cached by the raycast function
startRotation = transform.rotation;
startPointOnPlane = pointOnPlane; // Will be used for distance calculation
isRunning = true;
if (debugStartPoint != null) {
debugStartPoint.SetActive(true);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(true);
}
}
}
}
else {
if (Input.GetKeyUp(KeyCode.R) || !Input.GetKey(KeyCode.R)) {
isRunning = false;
if (debugStartPoint != null) {
debugStartPoint.SetActive(false);
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(false);
}
}
else {
Vector3 currentPointOnPlane = pointOnPlane;
Vector3 currentHitPosition = sampledPoint;
// Find how far on the plane the user has dragged the mouse.
// Every 1 radian of distance = 1/4 of a turn. So 4 radians of distance = 1 turn
// This of course will need to be converted to radians. 1 full turn = 6.28 radians
float totalDistance = (startPointOnPlane - currentPointOnPlane).magnitude;
float numTurns = totalDistance / (4f * arcballRadius); // Every 4R of distance = 1 full rotation! Unit is currently in number of rotations.
float angle = numTurns * 6.28319f; // convert rotations to radians. One full rotation = 6.28 radians.
// We now know the angle of rotation. Remember the formula for quaternion constructs angle x 2, so we need to half this.
float hCos = Mathf.Cos(angle * 0.5f);
float hSin = Mathf.Sin(angle * 0.5f);
// Construct the axis of rotation. In order to work in world space, all vectors should be relative
// to arcball position! (I'm not sure if the axis of rotation needs to be normalized or not).
Vector3 startVector = startHitPosition - arcballPosition;
Vector3 currentVector = currentHitPosition - arcballPosition;
Vector3 axis = Vector3.Cross(startVector, currentVector).normalized;
// Create the actual quaternion from the above values. This isn't a from-to rotation per-say. We still use the same vectors to figure out the
// angle of orientation, but the actual angle comes from distance of mouse movement.
Quaternion fromToRotation = Quaternion.identity;
fromToRotation.w = hCos;
fromToRotation.x = hSin * axis.x;
fromToRotation.y = hSin * axis.y;
fromToRotation.z = hSin * axis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV5 : MonoBehaviour {
public Vector3 transformCenter = new Vector3(0, 0, 0);
public float transformRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
transformCenter = debugArcBall.transform.position;
transformRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
}
void Update() {
// Potentially set the constraint axis
Vector3 constraint = Constraint;
// Don't do anything if there is no constraint!
if (constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane out of the constrain axis
Vector3 constraintPlaneNormal = constraint.normalized;
float constraintPlaneDifference = Vector3.Dot(constraintPlaneNormal, transformCenter);
// Construct the camera plane (a plane that faces the camera)
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, transformCenter);
// Find the point on the plane
Vector3 pointOnPlane = Vector3.zero;
Vector3 sampledPoint = Vector3.zero;
// These are needed to detect the edge cases in which the method proposed falls appart
float planeCameraDifference = Vector3.Dot(constraintPlaneNormal, Camera.main.transform.forward * -1.0f);
float planeMouseDifference = Vector3.Dot(constraintPlaneNormal, screenRay.direction.normalized);
// Edge case 1: The plane and mouse ray are perpendicular
// Edge case 2: The plane is perpendicular to the camera
if (Mathf.Abs(planeCameraDifference) < 0.01f || Mathf.Abs(planeMouseDifference) < 0.01f) {
// Find the closest point on the camera plane
if (!RaycastPlaneIgnoringRayDirection(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnPlane)) {
Debug.LogError("This isn't supposed to happen! Should be an assert, but Unity.");
}
// Find the closest point on the constraint plane to that point on the camera plane
// This gives us a point on the axis, just not "bubbled" out
float distance = Vector3.Dot(constraintPlaneNormal, pointOnPlane) - constraintPlaneDifference;
pointOnPlane = pointOnPlane - constraintPlaneNormal * distance;
// Make sure sampled point is always at the edge of the sphere
// Need to do a hemi-map for this
float cSq = transformRadius * transformRadius;
float aSq = (transformCenter - pointOnPlane).sqrMagnitude;
if (aSq > cSq) aSq = cSq;
float bSq = cSq - aSq;
sampledPoint = transformCenter + (pointOnPlane - transformCenter).normalized * Mathf.Sqrt(aSq) + (Camera.main.transform.forward * -1.0f * Mathf.Sqrt(bSq));
}
// Normal case
else {
// Find the closest point on the constraint plane
if (!RaycastPlaneIgnoringRayDirection(constraintPlaneNormal, constraintPlaneDifference, screenRay.origin, screenRay.direction, out pointOnPlane)) {
Debug.LogError("This isn't supposed to happen! Should be an assert, but Unity.");
}
// Make sure sampled point is always at the edge of the sphere
sampledPoint = transformCenter + (pointOnPlane - transformCenter).normalized * transformRadius;
}
// For debug purposes, let's see where the mouse should be!
Debug.DrawLine(transformCenter, pointOnPlane, Color.cyan);
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(transformCenter, transformRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startHitPosition = sampledPoint;
startRotation = constraintGlobal ? transform.rotation : transform.rotation;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentHitPosition = sampledPoint;
// Construct the axis of rotation. In order to work in world space, all vectors should be relative
// to arcball position! (I'm not sure if the axis of rotation needs to be normalized or not).
Vector3 startVector = startHitPosition - transformCenter;
Vector3 currentVector = currentHitPosition - transformCenter;
Quaternion fromToRotation = Quaternion.FromToRotation(startVector, currentVector);
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startHitPosition;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startHitPosition;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
return _constraint;
}
}
protected bool RaycastPlaneIgnoringRayDirection(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
//if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
//}
//return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV6 : MonoBehaviour {
public Vector3 arcballPosition = Vector3.zero;
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
//private Vector3 referenceUp;
private Vector3 referenceRight;
//private Vector3 referenceForward;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
//transform.rotation = Quaternion.identity;
}
void Update() {
// Don't do anything if there is no constraint!
if (Constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct the camera plane (a plane that faces the camera)
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, arcballPosition);
// Find the point on the plane
Vector3 pointOnCameraPlane = Vector3.zero;
if (!RaycastPlane(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnCameraPlane)) {
Debug.LogError("This isn't supposed to happen! Should be an assert, but Unity.");
}
Vector3 constraintAxisNormal = Constraint;
if (Mathf.Abs(Vector3.Dot(cameraPlaneNormal, constraintAxisNormal)) > 0.6f) {
constraintAxisNormal = referenceRight;
// TODO: Set left or right scalar!
}
float constraintAxisDistance = Vector3.Dot(constraintAxisNormal, arcballPosition);
Vector3 pointOnConstraintAxis = ClosestPoint(constraintAxisNormal, constraintAxisDistance, pointOnCameraPlane);
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startHitPosition = pointOnConstraintAxis;
startRotation = constraintGlobal ? transform.rotation : transform.rotation;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentHitPosition = pointOnConstraintAxis;
Vector3 testAxis = Vector3.Cross(cameraPlaneNormal, constraintAxisNormal).normalized;
float distance = (startHitPosition - currentHitPosition).magnitude;
float rotations = distance / (4.0f * arcballRadius);
float angle = rotations * 6.28319f;
float scale = -1.0f;
if (Vector3.Dot((currentHitPosition - startHitPosition).normalized, testAxis) < 0.0f) {
scale *= -1.0f;
}
//scale = -1.0f * Vector3.Dot((currentHitPosition - startHitPosition).normalized, testAxis);
angle *= scale;
Debug.Log("Scale: " + scale + ", Angle: " + angle);
transform.rotation = Quaternion.AngleAxis(Mathf.Rad2Deg * angle, Constraint) * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startHitPosition;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startHitPosition;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
if (Input.GetKeyDown(KeyCode.X) || Input.GetKeyDown(KeyCode.Y) || Input.GetKeyDown(KeyCode.Z)) {
if (constraintGlobal) {
//referenceUp = Camera.main.transform.up;
referenceRight = Camera.main.transform.right;
//referenceForward = Camera.main.transform.forward;
}
else {
//referenceUp = transform.up;
referenceRight = transform.right;
//referenceForward = transform.forward;
}
}
//referenceUp = Camera.main.transform.up;
referenceRight = Camera.main.transform.right;
//referenceForward = Camera.main.transform.forward;
return _constraint;
}
}
Vector3 ClosestPoint(Vector3 planeNormal, float planeDistance, Vector3 point) {
planeNormal = planeNormal.normalized;
float distance = Vector3.Dot(planeNormal, point) - planeDistance;
return point - planeNormal * distance;
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV7 : MonoBehaviour {
private Quaternion startRotation = Quaternion.identity;
private Vector3 startPointOnPlane = Vector3.zero;
Vector3 startPointOnCameraPlane = Vector3.zero;
public Vector3 arcballCenter = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugConstraintPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
arcballCenter = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
transform.rotation = Quaternion.identity;
}
void Update() {
// Potentially set the constraint axis
Vector3 constraint = Constraint;
// Don't do anything if there is no constraint!
if (constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane that has the normal facing the camera and always bisects the arcball sphere
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, arcballCenter);
// Construct a plane out of the constrain axis
Vector3 constraintPlaneNormal = constraint.normalized;
float constraintPlaneDistance = Vector3.Dot(constraintPlaneNormal, arcballCenter);
// Find where the world space ray intersects the plane that was created
Vector3 pointOnPlane = Vector3.zero;
RaycastPlane(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnPlane);
Vector3 pointOnCameraPlane = pointOnPlane;
// Find the closest point on the constraint plane to that point on the camera plane
// This gives us a point on the axis, just not "bubbled" out
Ray closestPointRay = new Ray();
closestPointRay.origin = pointOnPlane;
if (Mathf.Abs(Vector3.Dot(constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) { // EDGE CASE
closestPointRay.direction = Vector3.Cross(Vector3.Cross(cameraPlaneNormal, Camera.main.transform.right), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection(Camera.main.transform.right, Vector3.Dot(Camera.main.transform.right, arcballCenter), closestPointRay.origin, closestPointRay.direction, out pointOnPlane);
}
else { // NORMAL CASE
closestPointRay.direction = Vector3.Cross(Vector3.Cross(cameraPlaneNormal, constraintPlaneNormal), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection(constraintPlaneNormal, constraintPlaneDistance, closestPointRay.origin, closestPointRay.direction, out pointOnPlane);
}
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(arcballCenter, arcballRadius, screenRay.origin, screenRay.direction, out startPointOnPlane)) {
startRotation = transform.rotation;
startPointOnPlane = pointOnPlane;
startPointOnCameraPlane = pointOnCameraPlane;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentPointOnPlane = pointOnPlane;
// Find how far on the plane the user has dragged the mouse.
// Every 1 radian of distance = 1/4 of a turn. So 4 radians of distance = 1 turn
// This of course will need to be converted to radians. 1 full turn = 6.28 radians
float totalDistance = (startPointOnPlane - currentPointOnPlane).magnitude;
float numTurns = totalDistance / (4f * arcballRadius); // Every 4R of distance = 1 full rotation! Unit is currently in number of rotations.
float angle = numTurns * 6.28319f; // convert rotations to radians. One full rotation = 6.28 radians.
// We now know the angle of rotation. Remember the formula for quaternion constructs angle x 2, so we need to half this.
float hCos = Mathf.Cos(angle * 0.5f);
float hSin = Mathf.Sin(angle * 0.5f);
// Construct the axis of rotation.
Vector3 axis = Constraint;
// Normal case, flip axis when doubling over self
if (Mathf.Abs(Vector3.Dot(constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) {
// This code is duplicated from below. Not sure how to fix it proper :(
Vector3 testPoint = (currentPointOnPlane - startPointOnPlane).normalized;
float dot = Vector3.Dot(testPoint, Vector3.Cross(Camera.main.transform.right, cameraPlaneNormal));
Debug.Log("dot: " + dot);
if (dot < 0.0f) {
axis *= -1.0f;
}
dot = Vector3.Dot((startPointOnCameraPlane - arcballCenter).normalized, Camera.main.transform.right);
if (dot < 0f) {
Debug.Log("Edge flip");
axis *= -1.0f;
}
}
else {
Vector3 testPoint = (currentPointOnPlane - startPointOnPlane).normalized;
float dot = Vector3.Dot(testPoint, Vector3.Cross(constraintPlaneNormal, cameraPlaneNormal));
Debug.Log("dot: " + dot);
if (dot < 0.0f) {
axis *= -1.0f;
}
}
// Create the actual quaternion from the above values. This isn't a from-to rotation per-say. We still use the same vectors to figure out the
// angle of orientation, but the actual angle comes from distance of mouse movement.
Quaternion fromToRotation = Quaternion.identity;
fromToRotation.w = hCos;
fromToRotation.x = hSin * axis.x;
fromToRotation.y = hSin * axis.y;
fromToRotation.z = hSin * axis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startPointOnPlane;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentPointOnPlane;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startPointOnPlane;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startPointOnPlane;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
debugPlane.transform.position = transform.position;
}
if (debugConstraintPlane != null) {
debugConstraintPlane.SetActive(value && debugShowPlane);
debugConstraintPlane.transform.position = transform.position;
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
if (debugConstraintPlane != null && IsRotationActive) {
debugConstraintPlane.transform.localRotation = Quaternion.LookRotation(Camera.main.transform.forward * -1.0f, Vector3.up);
}
return _constraint;
}
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastPlaneIgnoringRayDirection(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
//if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
//}
//return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV8 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private Vector3 startPointOnPlane = Vector3.zero;
protected Vector3 startPointOnCameraPlane = Vector3.zero;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugConstraintPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
// transform.rotation = Quaternion.identity;
}
void Update() {
// Potentially set the constraint axis
Vector3 constraint = Constraint;
// Don't do anything if there is no constraint!
if (constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane that has the normal facing the camera and always bisects the arcball sphere
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, arcballPosition);
// Find where the world space ray intersects the plane that was created
Vector3 pointOnPlane = Vector3.zero;
RaycastPlane(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnPlane);
Vector3 pointOnCameraPlane = pointOnPlane;
// Construct a plane out of the constrain axis
Vector3 constraintPlaneNormal = constraint.normalized;
float constraintPlaneDistance = Vector3.Dot(constraintPlaneNormal, arcballPosition);
Ray closestPointRay = new Ray();
closestPointRay.origin = pointOnPlane;
if (Mathf.Abs(Vector3.Dot(constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) { // EDGE CASE
closestPointRay.direction = Vector3.Cross(Vector3.Cross(cameraPlaneNormal, Camera.main.transform.right), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection(Camera.main.transform.right, Vector3.Dot(Camera.main.transform.right, arcballPosition), closestPointRay.origin, closestPointRay.direction, out pointOnPlane);
}
else { // NORMAL CASE
closestPointRay.direction = Vector3.Cross(Vector3.Cross(cameraPlaneNormal, constraintPlaneNormal), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection(constraintPlaneNormal, constraintPlaneDistance, closestPointRay.origin, closestPointRay.direction, out pointOnPlane);
}
// Using some trig, project the point on the plane onto the hemisphere of the arcball.
// A^2 + B^2 = C^2
// C^2 = Radius^2
// A^2 = distance of point on plane from arcball ^2
// Clamp A^2 to C^2. That way the mouse can never trully leave the sphere
float cSq = arcballRadius * arcballRadius;
float aSq = (arcballPosition - pointOnPlane).sqrMagnitude;
if (aSq > cSq) aSq = cSq;
float bSq = cSq - aSq;
// SampledPoint holds where on the hemisphere the point on plane is. Remember, it's clamped to the edge of the sphere!
Vector3 mapDirection;
if (Mathf.Abs(Vector3.Dot(constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) { // EDGE CASE
mapDirection = Vector3.Cross(constraintPlaneNormal, Vector3.Cross(cameraPlaneNormal, Camera.main.transform.right));
}
else { // NORMAL CASE
mapDirection = Vector3.Cross(constraintPlaneNormal, Vector3.Cross(cameraPlaneNormal, constraintPlaneNormal));
}
Vector3 sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * Mathf.Sqrt(aSq) + (mapDirection * Mathf.Sqrt(bSq));
sampledPoint = arcballPosition + (sampledPoint - arcballPosition).normalized * arcballRadius;
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startHitPosition = sampledPoint;
startRotation = transform.rotation;
startPointOnPlane = pointOnPlane; // Will be used for distance calculation
startPointOnCameraPlane = pointOnCameraPlane;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentPointOnPlane = pointOnPlane;
Vector3 currentHitPosition = sampledPoint;
// Find how far on the plane the user has dragged the mouse.
// Every 1 radian of distance = 1/4 of a turn. So 4 radians of distance = 1 turn
// This of course will need to be converted to radians. 1 full turn = 6.28 radians
float totalDistance = (startPointOnPlane - currentPointOnPlane).magnitude;
float numTurns = totalDistance / (4f * arcballRadius); // Every 4R of distance = 1 full rotation! Unit is currently in number of rotations.
float angle = numTurns * 6.28319f; // convert rotations to radians. One full rotation = 6.28 radians.
// We now know the angle of rotation. Remember the formula for quaternion constructs angle x 2, so we need to half this.
float hCos = Mathf.Cos(angle * 0.5f);
float hSin = Mathf.Sin(angle * 0.5f);
// Construct the axis of rotation. In order to work in world space, all vectors should be relative
// to arcball position! (I'm not sure if the axis of rotation needs to be normalized or not).
Vector3 startVector = startHitPosition - arcballPosition;
Vector3 currentVector = currentHitPosition - arcballPosition;
Vector3 axis = Vector3.Cross(startVector, currentVector).normalized;
if (Mathf.Abs(Vector3.Dot(constraintPlaneNormal, cameraPlaneNormal)) > 0.9f) {
float dot = Vector3.Dot((startPointOnCameraPlane - arcballPosition).normalized, Camera.main.transform.right);
if (dot < 0f) {
Debug.Log("Edge flip");
axis *= -1.0f;
}
}
// Create the actual quaternion from the above values. This isn't a from-to rotation per-say. We still use the same vectors to figure out the
// angle of orientation, but the actual angle comes from distance of mouse movement.
Quaternion fromToRotation = Quaternion.identity;
fromToRotation.w = hCos;
fromToRotation.x = hSin * axis.x;
fromToRotation.y = hSin * axis.y;
fromToRotation.z = hSin * axis.z;
fromToRotation = NormalizeQuaternion(fromToRotation);
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startPointOnPlane;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startPointOnPlane;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startPointOnPlane;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
debugPlane.transform.position = transform.position;
}
if (debugConstraintPlane != null) {
debugConstraintPlane.SetActive(value && debugShowPlane);
debugConstraintPlane.transform.position = transform.position;
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
if (debugConstraintPlane != null && IsRotationActive) {
debugConstraintPlane.transform.localRotation = Quaternion.LookRotation(Camera.main.transform.forward * -1.0f, Vector3.up);
}
return _constraint;
}
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastPlaneIgnoringRayDirection(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
//if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
//}
//return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
using UnityEngine;
public class RotatorV9 : MonoBehaviour {
public Vector3 arcballPosition = new Vector3(0, 0, 0);
public float arcballRadius = 1.0f;
private Quaternion startRotation = Quaternion.identity;
private Vector3 startHitPosition = Vector3.zero;
private Vector3 startPointOnPlane = Vector3.zero;
protected Vector3 startPointOnCameraPlane = Vector3.zero;
public bool constraintGlobal = true;
public bool debugShowPlane = false;
private bool isRunning = false;
public GameObject debugStartPoint = null;
public GameObject debugCurrentPoint = null;
public GameObject debugArcBall = null;
public GameObject debugPlane = null;
public GameObject debugConstraintPlane = null;
public GameObject debugAxisLocal = null;
public GameObject debugAxisGlobal = null;
public GameObject debugAxisLocal_X = null;
public GameObject debugAxisGlobal_X = null;
public GameObject debugAxisLocal_Y = null;
public GameObject debugAxisGlobal_Y = null;
public GameObject debugAxisLocal_Z = null;
public GameObject debugAxisGlobal_Z = null;
void Awake() {
DebugVisible = false;
if (debugArcBall != null) {
arcballPosition = debugArcBall.transform.position;
arcballRadius = debugArcBall.transform.lossyScale.x * 0.5f;
}
// transform.rotation = Quaternion.identity;
}
void Update() {
// Potentially set the constraint axis
Vector3 constraint = Constraint;
// Don't do anything if there is no constraint!
if (constraint == Vector3.zero) {
isRunning = false;
DebugVisible = false;
return;
}
// Get the mouse position & create a world space ray from it
Vector3 mousePos = Input.mousePosition;
Ray screenRay = Camera.main.ScreenPointToRay(mousePos);
// Construct a plane that has the normal facing the camera and always bisects the arcball sphere
Vector3 cameraPlaneNormal = Camera.main.transform.forward * -1.0f;
float cameraPlaneDistance = Vector3.Dot(cameraPlaneNormal, arcballPosition);
// Find where the world space ray intersects the plane that was created
Vector3 pointOnPlane = Vector3.zero;
RaycastPlane(cameraPlaneNormal, cameraPlaneDistance, screenRay.origin, screenRay.direction, out pointOnPlane);
Vector3 pointOnCameraPlane = pointOnPlane;
// Using some trig, project the point on the plane onto the hemisphere of the arcball.
// A^2 + B^2 = C^2
// C^2 = Radius^2
// A^2 = distance of point on plane from arcball ^2
// Clamp A^2 to C^2. That way the mouse can never trully leave the sphere
float cSq = arcballRadius * arcballRadius;
float aSq = (arcballPosition - pointOnPlane).sqrMagnitude;
if (aSq > cSq) aSq = cSq;
float bSq = cSq - aSq;
// SampledPoint holds where on the hemisphere the point on plane is. Remember, it's clamped to the edge of the sphere!
Vector3 mapDirection = cameraPlaneNormal;
Vector3 sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * Mathf.Sqrt(aSq) + (mapDirection * Mathf.Sqrt(bSq));
// Construct a plane out of the constrain axis
Vector3 constraintPlaneNormal = constraint.normalized;
float constraintPlaneDistance = Vector3.Dot(constraintPlaneNormal, arcballPosition);
Ray closestPointRay = new Ray();
closestPointRay.origin = sampledPoint;
if (Mathf.Abs (Vector3.Dot (constraintPlaneNormal, cameraPlaneNormal)) < 0.9f) { // NON-EDGE CASE
closestPointRay.direction = Vector3.Cross (Vector3.Cross (cameraPlaneNormal, constraintPlaneNormal), cameraPlaneNormal);
RaycastPlaneIgnoringRayDirection (constraintPlaneNormal, constraintPlaneDistance, closestPointRay.origin, closestPointRay.direction, out sampledPoint);
sampledPoint = arcballPosition + (sampledPoint - arcballPosition).normalized * arcballRadius;
} else { // EDGE CASE
sampledPoint = arcballPosition + (pointOnPlane - arcballPosition).normalized * arcballRadius;
}
if (!isRunning) {
if (IsRotationActive) {
if (RaycastSphere(arcballPosition, arcballRadius, screenRay.origin, screenRay.direction, out startHitPosition)) {
startHitPosition = sampledPoint;
startRotation = transform.rotation;
startPointOnPlane = pointOnCameraPlane; // Will be used for distance calculation
startPointOnCameraPlane = pointOnCameraPlane;
isRunning = true;
DebugVisible = true;
}
}
}
else {
if (!IsRotationActive) {
DebugVisible = false;
}
else {
Vector3 currentPointOnPlane = pointOnCameraPlane;
Vector3 currentHitPosition = sampledPoint;
Vector3 startVector = startHitPosition - arcballPosition;
Vector3 currentVector = currentHitPosition - arcballPosition;
// Create the actual quaternion from the above values. This isn't a from-to rotation per-say. We still use the same vectors to figure out the
Quaternion fromToRotation = HalfQuaternionMethod (startVector, currentVector);
fromToRotation = NormalizeQuaternion(fromToRotation);
// Apply the rotation.
transform.rotation = fromToRotation * startRotation;
if (debugStartPoint != null) {
debugStartPoint.transform.position = startHitPosition;
}
if (debugCurrentPoint != null) {
debugCurrentPoint.transform.position = currentHitPosition;
}
}
}
}
protected Quaternion NormalizeQuaternion(Quaternion quat) {
float len = quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w;
if (len != 0.0f) {
len = Mathf.Sqrt(len);
quat.x /= len;
quat.y /= len;
quat.z /= len;
quat.w /= len;
}
return quat;
}
bool DebugVisible {
set {
if (debugStartPoint != null) {
debugStartPoint.SetActive(value);
if (value) {
debugStartPoint.transform.position = startPointOnPlane;
}
}
if (debugCurrentPoint != null) {
debugCurrentPoint.SetActive(value);
if (value) {
debugCurrentPoint.transform.position = startPointOnPlane;
}
}
if (debugPlane != null) {
debugPlane.SetActive(value && debugShowPlane);
debugPlane.transform.position = transform.position;
}
if (debugConstraintPlane != null) {
debugConstraintPlane.SetActive(value && debugShowPlane);
debugConstraintPlane.transform.position = transform.position;
}
if (debugAxisLocal != null) {
debugAxisLocal.SetActive(value && !constraintGlobal);
}
if (debugAxisGlobal != null) {
debugAxisGlobal.SetActive(value && constraintGlobal);
}
if (debugAxisLocal_X != null) {
debugAxisLocal_X.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisLocal_Y != null) {
debugAxisLocal_Y.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisLocal_Z != null) {
debugAxisLocal_Z.SetActive(value && !constraintGlobal && Input.GetKey(KeyCode.Z));
}
if (debugAxisGlobal_X != null) {
debugAxisGlobal_X.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.X));
}
if (debugAxisGlobal_Y != null) {
debugAxisGlobal_Y.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Y));
}
if (debugAxisGlobal_Z != null) {
debugAxisGlobal_Z.SetActive(value && constraintGlobal && Input.GetKey(KeyCode.Z));
}
}
}
// Quaternion.fromToRotation
protected Quaternion HalfQuaternionMethod(Vector3 v0, Vector3 v1) {
Quaternion result = Quaternion.identity;
Vector3 axis = Vector3.zero;
float k_cos_theta = Vector3.Dot(v0, v1);
float k = Mathf.Sqrt(v0.sqrMagnitude * v1.sqrMagnitude);
if (k_cos_theta / k == -1) {
result.w = 0.0f;
axis = OrthogonalAxis(v0).normalized;
}
else {
result.w = k_cos_theta + k;
axis = Vector3.Cross(v0, v1);
}
result.x = axis.x;
result.y = axis.y;
result.z = axis.z;
return NormalizeQuaternion(result);
}
protected Vector3 OrthogonalAxis(Vector3 v) {
float x = Mathf.Abs(v.x);
float y = Mathf.Abs(v.y);
float z = Mathf.Abs(v.z);
Vector3 other = Vector3.zero;
if (x < y) {
if (x < z) {
other = Vector3.right; // x axis
}
else {
other = Vector3.forward; // z axis
}
}
else {
if (y < z) {
other = Vector3.up; // y axis
}
else {
other = Vector3.forward; // z axis
}
}
return Vector3.Cross(v, other);
}
bool IsRotationActive {
get {
if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.Y)) {
return true;
}
return false;
}
}
protected Vector3 _constraint = Vector3.zero;
public Vector3 Constraint {
get {
if (Input.GetKeyDown(KeyCode.X)) {
_constraint = constraintGlobal ? Camera.main.transform.up : transform.up;
}
if (Input.GetKeyDown(KeyCode.Y)) {
_constraint = constraintGlobal ? Camera.main.transform.right : transform.right;
}
if (Input.GetKeyDown(KeyCode.Z)) {
_constraint = constraintGlobal ? Camera.main.transform.forward : transform.forward;
}
if (Input.GetKeyUp(KeyCode.X) || Input.GetKeyUp(KeyCode.Y) || Input.GetKeyUp(KeyCode.Z)) {
_constraint = Vector3.zero;
}
if (debugPlane != null && IsRotationActive) {
debugPlane.transform.localRotation = Quaternion.LookRotation(_constraint, Vector3.up);
}
if (debugConstraintPlane != null && IsRotationActive) {
debugConstraintPlane.transform.localRotation = Quaternion.LookRotation(Camera.main.transform.forward * -1.0f, Vector3.up);
}
return _constraint;
}
}
protected bool RaycastPlane(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
}
return false;
}
protected bool RaycastPlaneIgnoringRayDirection(Vector3 planeNormal, float planeDistance, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
planeNormal = planeNormal.normalized;
float nd = Vector3.Dot(rayDirection, planeNormal);
float pn = Vector3.Dot(rayOrigin, planeNormal);
// Plane normal should not matter for this one!!!
if (nd >= 0.0f) { // Wrong direction
//return false;
}
float t = (planeDistance - pn) / nd;
//if (t >= 0.0f) {
hitPosition = rayOrigin + rayDirection * t;
return true;
//}
//return false;
}
protected bool RaycastSphere(Vector3 spherePosition, float sphereRadius, Vector3 rayOrigin, Vector3 rayDirection, out Vector3 hitPosition) {
hitPosition = Vector3.zero;
rayDirection = rayDirection.normalized;
Vector3 e = spherePosition - rayOrigin;
float rSq = sphereRadius * sphereRadius;
float eSq = e.sqrMagnitude;
float a = Vector3.Dot(e, rayDirection);
float bSq = eSq - (a * a);
float f = Mathf.Sqrt(Mathf.Abs((rSq) - bSq));
float t = a - f;
if (rSq - (eSq - a * a) < 0.0f) { // No Collision
return false;
}
else if (eSq < rSq) { // Ray starts inside the sphere
t = a + f;// Just reverse direction
}
hitPosition = rayOrigin + rayDirection * t;
return true;
}
}
// This is a unity style (free) rotation gizmo. There is no easy way to constrain rotation on any axis (afaik)
// Rot-dir could be projected and re-normalized, but that looks like a major pain to do
// It's not a pixel perfect rotation sceme, i don't really enjoy it.
// Rotation is based on an acumulated delta of the mouse. Think of it more like a tredmill with an axis rather than an arc-ball
using UnityEngine;
public class Rotator : MonoBehaviour {
private Vector3 sartMousePos;
private Vector3 lastMousePos;
private bool isRunning = false;
void Update() {
Vector3 mousePos = Input.mousePosition;
if (!isRunning) {
if (Input.GetKeyDown(KeyCode.R)) {
isRunning = true;
sartMousePos = mousePos;
}
}
else {
Vector3 delta = mousePos - lastMousePos;
Vector3 rotDir = Camera.main.transform.TransformDirection(new Vector3(delta.y, -delta.x, 0));
transform.rotation = Quaternion.AngleAxis(delta.magnitude, rotDir.normalized) * transform.rotation;
if (Input.GetKeyUp(KeyCode.R)) {
isRunning = false;
}
}
lastMousePos = mousePos;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment