Quaternion.LookDirection with parent rotated

I’ve been stuck on this problem for a while, cant get the Turret to rotate correctly when its parent is rotated. I can think of one solution, but its kinda hacky which I really want to avoid if possible. I’m looking for any other cleaner solution.

Hierarchy
------ EmptyGameObject (the GameObject that is rotated)
--------------Turret (has this script attached)

Here in Update I’am rotating a GameObject towards mouse cursor on Y axis.

    void Update()
    {
        var direction = (GetMouseWorld3D(20) - transform.position).normalized;
        var newRotation = Quaternion.LookRotation(direction);

        var newRotationInEuler = newRotation.eulerAngles;
        newRotationInEuler.x = 0;
        newRotationInEuler.z = 0;
 
        transform.localRotation = Quaternion.Euler(newRotationInEuler);
    }
    public static Vector3 GetMouseWorld3D(float distance)
    {
        Ray ray = _camera.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out RaycastHit raycastHit, distance, _lm))
            return raycastHit.point;
        return ray.GetPoint(20);
    }

“the cyan sphere is the mouse cursor”

Ignore the GUI label rotation value is incorrect: in the first image, parent of the Turret is not rotated so it works as expected. But that’s not the case is all other images

Rotated by 90

Rotated by 40

localRotation will ignore any parent by definition, yet you rely on transform.position meaning that you’re operating in the world space, but then you modify the object space transformation.

You must be mindful of the spaces when you’re working with linear algebra in this. We use localRotation because it is de facto the storage of the actual quaternion being applied locally. It is faster and more precise.

But when you’re operating in the world space – and when you have a turret aiming at some free target out there, that’s a clear case of world space operation – we rely on rotations and positions instead of their local counterparts.

The difference is that rotation and positions have to take the hierarchy into account to be truly representative of the world space. Getting/setting them is slower and less precise, especially with scale (which is why it’s called lossyScale), the reason being that the matrices are composited super-fast, but then you need to decompose the final matrix, to get the separate results, such as a quaternion, from it.

This is not super slow in the general case like yours, and you definitely want to work like this, but here I’m trying to explain why we use localRotation and localPosition historically. And Unity actually uses these to KEEP the original information without having to pack/unpack matrices all the time. However as soon as you start building up a hierarchy, the actual result you see on the screen is all converted into world space matrices, and the original information is KEPT but not really rendered (unless you change the values, then it has to propagate upward to compute the world space again). If you attempt to change the object-space information to values that are derived from the world-space it will probably be buggy and non-sensical, unless you had a super-simple identity hierarchy to begin with (i.e. parent’s rotation is 0,0,0).

With all that in mind, you can however, convert the result from the world space to object space on the fly, if you really need to, by multiplying Transform.worldToLocalMatrix (it gets pretty advanced from there, and I don’t recommend this if you’re not familiar with matrices).

(Edit: Ok, you’re using eulerAngles to avoid having rotations on Y and Z. For some reason I read Quaternion.Euler, please disregard the next paragraph for this case, but it’s a useful advice anyway. There are btw ways to avoid eulerAngles just as well, you don’t need that.)

Btw you don’t really need Euler angles for what you’re trying to achieve. Euler angles are not really useful unless you’re the one driving them – i.e. by actually setting the angles to some “designed” rotation. I have never seen a live algorithm employing a naturally occurring reorientation (i.e. turret aiming) that needs Euler angles. Euler angles transformation is super-slow and unreliable and should be used only as an in-code human-translation tool for when you really want to make a quaternion but don’t want to mess with the weird values and you want your code readable. You call it once however and you’re done with it, that’s how one uses it.

Let’s now think about this from the beginning.

I’m guessing you’re having two objects that behave like this: one is the shaft that is fixed to some ground point, and rotates on the vertical (Y) axis (YAW), while its child object is allowed to go up/down (PITCH). Is this correct?

I will show you the solution once we confirm the actual setup.

Technically the parent should be called a turret. Turrets are actually the gun parents in such mechanisms.

The gun itself either has some limited rotation in a cone, or like with a tank, it can only pitch up or down.

You’r right, there are two objects for the Turret
-----Base (rotates on Y)
-----------Gun (rotates on X)

in the actual game this Turret is attached as a child-GameObject to a Car-GameObject. In the game the Car will have wide range of motion being able to rotate in any direction. That’s why I wanted to handle the Turret’s rotation locally(or so I thought) since parent(the car) will have its own rotation.

This is exactly why you want to work in world space.

If you think about it, why do you care about the arrangements and compound transformations, when you can absolutely orient some gun toward some random point in space, and it will work at all times. The only issue you do have is that the rotations are constrained, and so certain configurations are not doable by the turret. For starters the gun is probably unable to pitch below the horizon (maybe only slightly), so to act like a true turret you actually model legal orientations as a half-dome.

Imagine sticking this half-dome to the roof of your car, and rotate this car however you want, that’s basically your orientation space. The gun can aim at a target only if there is a clear uninterrupted ray between the half-dome’s center and the target.

Now let’s unpack the half-dome itself, because once we’ve decided the target is aimable, we need to come up with the mechanical rotations inside the turret, and yet we have two independent rotations which need to combine into just one thing. This is btw an incredibly hard problem, BUT because your two axes of rotations are axis-aligned, it becomes trivial. At this point we can also completely ignore the fact that the car itself rotates, and allow this to be an afterthought.

So, practically, we want to decompose a simple, very directional orientation into two separate but inter-dependent orientations. For example (in yaw+pitch notation) if you need to aim at 60°+30° that just means rotate base by Y60° then rotate gun by X30°. However, what is needed is a way to turn the compound rotation into two base rotations.

I wrote some code that does this, it’ll be in the next post.

You can use eulers if you are a local rotation.

Let me know if eulers fail I’ll see if I can dig out some code where eulers work for you.

Sorry about the delay, I have severe technical problems with the forum and I’m posting this in the hope it’ll somehow refresh the saved draft which bugged for some reason and I can’t neither post nor preview due to some error.

I’ll slowly build up the code, so let’s start with the basics.
First we make the environment that allows us to work with this in an isolated manner.

using UnityEngine;

public class CarTurretRotation : MonoBehaviour {

  [SerializeField] GameObject gun;
  [SerializeField] GameObject target;

}

These are just some basic slots to drag’n’drop empty game objects. Make both ‘turret’ and ‘target’ in the scene (not parented to anything), then add a child object to ‘turret’ and name it ‘gun’. Make sure that all three transforms are reset. From now on, we’ll need only world position from ‘target’, so it doesn’t really matter if it’s rotated, however we won’t change ‘turret’ or ‘gun’.

In short, the hierarchy looks like this:
turret (top-level object; script goes here)

  • gun (empty child object)
    target (empty top-level object)

Next, let’s make sure this script works immediately in the editor. And we’ll add a couple more stuff.

using UnityEngine;

[ExecuteInEditMode]
public class CarTurretRotation : MonoBehaviour {

  [SerializeField] GameObject gun;
  [SerializeField] GameObject target;

  void Update() {
    if(gun is null) return; // safety bail

    // let's start by storing local rotations
    var baseRot = transform.localRotation;
    var gunRot = gun.transform.localRotation;
  }

  void OnDrawGizmos { // if I type () after OnDrawGizmos, the forum stops working o.O
    // I can't post or preview the message, it took me an hour to discover this
    // anyway we'll fix it later, hopefully once I post this, that broken cache will go away
  }

}

To be continued

1 Like

Test eulers should be controllable on the one axis during local.
The alternative is a complex quaternion subtract quaternion I believe

We’ll use OnDrawGizmos to draw some gizmo lines, without having to rely on meshes.
So let’s do this

void OnDrawGizmos { // yet again I can't type the parentheses () here :(
  if(gun is null) return; // safety bail
  drawBase(Color.red); // this will draw a line that represents the base
  drawGun(Color.yellow); // this will draw a line that represents the gun

  // obviously the gun is supposed to be offset from the base's pivot
  // we'll consider the gun's local pivot position to be this offset (to make things easy)

  // finally, we want to draw a line from the gun's pivot to target
  // this represent the aiming world direction
  if(target is null) return; // another safety bail because we need a target object for this
  drawAimLine(Color.cyan);
}

Next, we should compute a couple of interesting points. Namely the world position of the gun’s pivot and the world position of the gun’s nozzle. This will make drawing lines easier.

Gun’s pivot we already have, it’s just

Vector3 getGunPivotPos() => gun.transform.position;

Gun’s nozzle however, is a little more complicated, and we need the length of the gun for this.
So let’s add this at the top.

[SerializeField] [Min(0f)] float gunLength;

Now we can think about the nozzle. The position of the nozzle is affected by the general offset of the gun, as well as the world rotation of the gun. One way to find it, is to multiply gunLength with the forward vector (0, 0, 1), and this gives us a segment that’s just as long. Then we rotate this vector according to the gun’s world rotation. Finally we move the origin of this segment to match the gun’s pivot.

Thus

Vector3 getGunNozzlePos() => getGunPivotPos() + gun.transform.rotation * (gunLength * Vector3.forward);

Now we can write the three gizmo drawing functions.

void drawBase(Color color) {
  var p1 = transform.position; // this script lives on 'turret', its origin is point1
  var p2 = getGunPivotPos(); // gun's pivot is point2
  drawLine(p1, p2, color);
}

void drawGun(Color color) {
  var p1 = getGunPivotPos();
  var p2 = getGunNozzlePos();
  drawLine(p1, p2, color);
}

void drawAimLine(Color color) {
  var p1 = getGunPivotPos();
  var p2 = target.transform.position;
  drawLine(p1, p2, color);
}

void drawLine(Vector3 a, Vector3 b, Color color) {
  Gizmos.color = color;
  Gizmos.DrawLine(a, b);
}

To be continued.

1 Like

a) Place the script on ‘turret’,
b) Connect (drag’n’drop) ‘gun’ and ‘target’ objects to eponymous fields in the inspector,
c) You should be able to see a blue line in the scene, going from ‘turret’ to ‘target’,
d) If you modify ‘Gun Length’ you should be able to see a yellow line,
e) Select ‘gun’ and move it slightly upwards with the move tool (Y), the yellow line should move and you should be able to see a red line,
f) You are now free to move the target object live in the editor, blue line should update on its own,
g) Additionally you may change the object icon for ‘target’ so that it’s easier to locate in the scene.

1 Like

Now let’s add a toggle for the blue line.

[SerializeField] bool showAimLine;

void OnDrawGizmos { // and again pls add () in your code
  // ... keep everything else already here
  if(showAimLine) drawAimLine(Color.cyan);
}

From now on, everything else is in update.
Here’s what we can do

void Update() {
  var baseRot = transform.localRotation;
  var gunRot = gun.transform.localRotation;

  // we do some computation here
  
  transform.localRotation = baseRot;
  gun.transform.localRotation = gunRot;
}

With the Update loop prepared, we can now think of how exactly we can find out the rotations involved.
Here’s one way. We can begin by getting a direction from gun’s pivot to target. Let’s call this tdir for ‘target direction’. To find a direction subtract two points B - A and what you get is a vector that runs from A to B. Now normalize this vector to get a vector of length 1.

var tdir = (target.transform.position - getGunPivotPos()).normalized;

For now we treat the setup as if the turret is laid on the flat XZ ground, so no car rotations are involved, yet. This means that we can use the XZ plane to project this directional vector to it.

Reasoning behind this
This is a way to constrain the rotation to just one major axis, in this case Y for turret base rotation. Because we’re going to compute a quaternion from this tdir direction, it is much easier and faster to flatten it down, then to remove this extra rotation from a quaternion.

The easiest way to do it
We just nullify y component of tdir.

var tdir = target.transform.position - getGunPivotPos(); // we'll do normalization later
var pdir = new Vector3(tdir.x, 0f, tdir.z); // projected direction

To be continued.

1 Like

Damn, it took me much less to make this then to post it :slight_smile: the forum is messed up big time.

Anyway, now that we have pdir, we can nail down rotation Y all while making sure that the turret base’s local up stays that way.

// we can use the world's up because locally it's true
var baseQuat = Quaternion.LookRotation(pdir.normalized, Vector3.up);

Now, for reasons that I haven’t managed to nail down exactly – it’s probably something I did (or didn’t do) in the setup – we have to rotate this result by -90 degrees on Y to make it correct. But who cares if it’s easy to solve.

Let’s add this right after serialized fields.

Quaternion _twist = Quaternion.Euler(0f, -90f, 0f);

Now we can add this to Update

baseRot = _twist * baseQuat;

These things in Update should be guarded against null target anyway, so here’s the full Update function so far

void Update() {
  // get local rotations
  var baseRot = transform.localRotation;
  var gunRot = gun.transform.localRotation;

  if(target is null) {
    // we'll use this later

  } else {
    // technically these are not directions, but deltas, but we'll normalize them on the spot
    var tdir = target.transform.position - getGunPivotPos();
    var pdir = new Vector3(tdir.x, 0f, tdir.z);

    var baseQuat = Quaternion.LookRotation(pdir.normalized, Vector3.up);
    baseRot = _twist * baseQuat;

    // here we'll add gun rotation at a later point, code is below

  }

  // store rotations back
  transform.localRotation = baseRot;
  gun.transform.localRotation = gunRot;
}

}

To be continued.

Two things remain to be done.

First, to rotate the gun. The gun is supposed to be oriented in such a way to always point directly toward the target with its nozzle. It should also always pitch, and should never roll. Now you can see what’s the idea behind the blue line: if everything works properly the yellow line should always line up exactly with the blue one.

Second, we need to account for the rotating car underneath this contraption. We’ll do this as the last thing.

When it comes to gun rotation, there is a simple trick how to get the secondary rotation, now that we have the horizontal one.

What we need is something that’s called a decomposition of quaternions. The final rotation (lets call it Rf) consists of two independent ones (R1 and R2). Just imagine if we could represent this with simple math, it would look like Rf = R1 + R2. In this case we already have Rf and R1, but we need to find R2. If we could only subtract the rotations, right? R2 = Rf - R1 would be the answer.

(I’m sorry for slicing this up so much, but I can’t post otherwise.)

Well, quaternions can be “subtracted” – or better put, a differential rotation can be computed simply by multiplying one with the inverse of another. And so, in our case, we practically “cancel” the turret base’s Y rotation from the compound one.

var gunQuat = Quaternion.Inverse(baseRot) * Quaternion.LookRotation(tdir.normalized, Vector3.up);
gunRot = gunQuat;

Decomposition of quaternions is not an easy problem, but this trick when you’re having just a single hinge makes it very easy. Fun fact: the only remaining rotation that would perfectly complete this rotation is the gun’s pitch.

There is one more thing we could add. A way to tell how much the gun is pitched, and to prohibit certain angles. For example, if we don’t want the gun to pitch below the horizon, or if there is a maximum angle above the horizon.

To be continued.

They kno it’s coming

1 Like

So your current angle relative to your parent angle is

transform.localEulerAngles.
You can just += new Vector(X,Y,Z)
No trouble at all.

but if you want to read the angle reliably you’ll have to make a string or int of it.

so to clamp your angle to 45* you write a vector3 of 45* on the axis you want, and then compare the strings or ints or rounded floats of your current vector and your clamp vector.

To check out the gun’s pitch and its angles, we need to use eulerAngles computed from gunQuat. Here the X rotation will show the pitch in degrees. But the numbers are all over the place even though they are in the 0…360 range.

When I made this I wanted to have a strict control over what numbers make sense in this scenario. I wanted the number to be +90° when the gun is pointing directly up, -90° when it’s pointing directly down, and 0° when it’s pointing at the horizon.

So here’s a short recipe of what I did to make this happen.

  1. I got the angle,
  2. incremented it by 180°,
  3. took a 360° modulo and made sure that the value is now in the 0…360° range and not something like -200 or +1400,
  4. subtracted this result from 180°.

This process was more of a trial and error, not really something important or special, but hey you want to do it in one go, and this will help with the parameters that you can set to invalidate the pitch.

But we need this modulo thing, right?

float mod(float v, float m) => (v %= m) < 0f? v + m : v;

Now you can add the limiting parameters (top code) and the invalid flag.

[SerializeField] [Range(-90f, 90f)] float pitchMin; // i.e. 5° below horizon (-5)
[SerializeField] [Range(-90f, 90f)] float pitchMax; // i.e. 75

bool _invalid; // if true then this aiming angle is beyond limits
Quaternion _twist = Quaternion.Euler(0f, -90f, 0f); // this was already here

Now let’s change drawGun call in OnDrawGizmos so that it shows the gun in a different color when the aim is invalid.

...
drawGun(!_invalid? Color.yellow : Color.magenta);
...

Finally, we evaluate whether the aim is fine in the Update

var pitch = 180f - mod((gunQuat.eulerAngles.x + 180f, 360f);
_invalid = pitch < pitchMin || pitch > pitchMax;

Lastly, we need to account for the tilting (or backflipping) car.

This probably sounds daunting considering all these quaternions, but here’s another trick. Because we do the whole calculation for the local space we are actually independent from any reference frame. This means that the only thing that’s coming from the external frame of reference and will change its relative position if the car rotates is the target.

Notice that, in the relative terms, from the perspective of the turret it doesn’t matter what rotates, the only thing that matters is the blue aim line. It is what we use to determine rotations.

Let’s change the hierarchy slightly to make accommodations for the car. Manually add ‘turret’ to a new object ‘car’, so that you get:
car (new empty object)

  • turret (script lives here)
    • gun
      target

Once we add the following line of code you’ll be free to rotate car however you want, and the thing should hopefully work as intended. We also want to invalidate the aim if there is no target, so here’s the full Update function after these changes

void Update() {
  var baseRot = transform.localRotation;
  var gunRot = gun.transform.localRotation;

  if(target is null) {
    _invalid = true;

  } else {
    var tdir = target.transform.position - getGunPivotPos();

    // this is the trick, we apply inverted car's rotation to the target instead
    tdir = Quaternion.Inverse(transform.parent.rotation) * tdir;

    var pdir = new Vector3(tdir.x, 0f, tdir.z);

    var baseQuat = Quaternion.LookRotation(pdir.normalized, Vector3.up);
    baseRot = _twist * baseQuat;

    var gunQuat = Quaternion.Inverse(baseRot) * Quaternion.LookRotation(tdir.normalized, Vector3.up);
    gunRot = gunQuat;

    var pitch = 180f - mod(gunQuat.eulerAngles.x + 180f, 360f);
    _invalid = pitch < pitchMin || pitch > pitchMax;

  }

  transform.localRotation = baseRot;
  gun.transform.localRotation = gunRot;

}

And that’s it.

I’ve tested this with some boxes for some time and it appeared to be working solidly. You can move and/or rotate both ‘car’ and ‘target’ and the parts should behave correctly. You can test for yourself by parenting elongated cubes to ‘turret’ and ‘gun’. Make sure to adjust the gun’s pivot (nest it inside another object if needed) so that it lands on local zero.