0
votes

I’m working on a mechanism to control the position and rotation of an object around an anchorPoint relative to a controlPoint in 3D space. The object can have any initial rotation, necessitating the use of a rotational offset in order to maintain a relative alignment when the controlPoint is moved. Neither the control nor anchor points exist within the unity scene hierarchy as they’re stored as Vector3’s. The object however, utilizes unity’s Transform and may have one or more parent objects each with their own rotation.

The expected behavior is much like that of a local rotation gizmo where you click on the ring of the gizmo to set the rotation of the object.

I've tried without success to implement this using FromToRotation, LookRotation as rotations from identity as well as incremental rotations from the previous rotation state.

The below was the closest i got, however if the object or it's parent have a Z rotation upon calculating the offset, the object then rotates as expected but also includes an undesired roll around the look axis.

public Vector3 anchorPoint = Vector3.zero;
public Vector3 controlPoint = Vector3.left;

Quaternion rotationOffset = Quaternion.identity;

//Called when the object is "attached" to the anchor to store the difference between it's rotation and the directional vector from the control to the anchor
public void StoreOffset() {
    //The unit vector pointing from the controlPoint to it's anchorPoint
    Vector3 controlFwd = (anchorPoint - controlPoint).normalized;

    //Generate a rotation using the direction vector while respecting the objects local up.
    Quaternion lookRot = Quaternion.LookRotation(controlFwd, transform.up);

    //Subtract the object's rotation from the lookRotation to determine the rotational offset between the two.
            //Invert the offset rotation to save on calculations during update
            rotationOffset = Quaternion.Inverse(Quaternion.Inverse(transform.rotation) * lookRot);
}

//Called any time the control point is moved to update the objects rotation
public void UpdateRotation() {
    //The unit vector pointing from the controlPoint to it's anchorPoint
    Vector3 controlFwd = (anchorPoint - controlPoint).normalized;

    //Generate a rotation using the above direction vector while respecting the objects local up.
    Quaternion lookRot = Quaternion.LookRotation(controlFwd, transform.up);

    //Assign the new look rotation to the object then apply the offset rotation.
    //ToLocalRotation is a Quaternion extension that converts rotations from world to local space
            transform.localRotation = (lookRot * rotationOffset).ToLocalRotation(transform);
}

Example Animations

The first .gif shows the correct functionality so long as the parent object has no rotation.

The second .gif shows errant rotation if the parent has a Z rotation of 10°

Expected Rotation Errant Rotation

1
It's still a little unclear what controlPoint is meant to do. Maybe walking through some examples could help. What is supposed to happen if control point is set to be some distance in front of the object, then StoreOffset is called, then the control point is set to be directly above the object, UpdateRotation is called, then some time later, the control point is set so that it is on the opposite side of the object (at this point the back of the object) then UpdateRotation is called. How is the object supposed to be oriented after UpdateRotation is called each of those times?Ruzihm
Also it's not quite stated but it seems that both controlPoint and anchorPoint are in global space. Is that correct?Ruzihm
@Ruzihm controlPoint and anchorPoint are in global space. Using your example, assuming the anchor is the origin, when StoreOffset is called, the object's forward is facing the controlPoint so the offset rotation would be the inverse of the LookRotation from the controlPoint to the anchorPoint (respecting the objects local up). Due to that specific offset, the object would always rotate to face the controlPoint.Reahreic
Right, but what directions would the objects local up be? The opposite direction from the control point's original position after the first UpdateRotation, then the same direction from the control point's original position? or would the local up not change on the 2nd call? We can say the angle is actually only changing 89 degrees then 179 degrees and just speak approximatelyRuzihm
Assuming a starting object rotation of local forward is aligned with global forward, The first update (+89° around local x axis) would cause up to move from global up towards global back. The second rotation (+179°) would depend on whether the shift from top to bottom favored the the local right, or the local up axis. (If it helps, controlPoint movement occurs incrementally by user drag and not by value assignment.)Reahreic

1 Answers

1
votes

So before storeoffset happens, you have a current rotation R. You want to calculate an offset X such that from the local axes at rotation R which would produce the look rotation where local forward = control->anchor and local up ≈ the up from R.

That is to say,

R * X = lookrotation(anchor - control, transform.up)

So, in order to find X, multiply both sides of the equation by inverse(R) on the left side:

inverse(R) * R * X = inverse(R) * lookrotation(anchor-control, transform.up)
                 X = inverse(R) * lookrotation(anchor-control, transform.up)

So, the end of StoreOffset should rather be:

rotationOffset = Quaternion.Inverse(transform.rotation) * lookRot;

Then, in UpdateRotation you have X and the lookRot with an up from a ~close-enough~ transform, so solve for R by multiplying both sides by inverse(X) on the right:

R * X              = lookrotation(anchor-control, transform.up)
R * X * inverse(X) = lookrotation(anchor-control, transform.up) * inverse(X)
                 R = lookrotation(anchor-control, transform.up) * inverse(X)

So, the end of UpdateRotation should rather be:

transform.rotation = lookRot * Quaternion.inverse(rotationOffset);

Or, since it's likely that UpdateRotation will be called much more often than StoreOffset, do the inversion before the assignment:

rotationOffsetInverse = Quaternion.inverse(Quaternion.Inverse(transform.rotation) 
                                           * lookRot);
// ...
transform.rotation = lookRot * rotationOffsetInverse;

So, altogether:

public Vector3 anchorPoint = Vector3.zero;
public Vector3 controlPoint = Vector3.left;

Quaternion rotationOffsetInverse = Quaternion.identity;

Quaternion GetControlLookRot()
{
    // The unit vector pointing from the controlPoint to it's anchorPoint
    Vector3 controlFwd = (anchorPoint - controlPoint).normalized;

    // Generate a rotation using the direction vector while respecting the 
    // objects local up.
    return Quaternion.LookRotation(controlFwd, transform.up);
}

// Called when the object is "attached" to the anchor to store the difference between 
// its rotation and the directional vector from the control to the anchor
public void StoreOffset()
{
    rotationOffsetInverse = Quaternion.inverse(Quaternion.Inverse(transform.rotation) 
                                        * GetControlLookRot());
}

// Called any time the control point is moved to update the objects rotation
public void UpdateRotation() 
{
    // Assign the new look rotation to the object then apply the offset rotation.
    transform.rotation = GetControlLookRot() * rotationOffsetInverse;
}