Search Unity

Resolved Quaternion.LookDirection with parent rotated

Discussion in 'Scripting' started by asdfasdf234, Feb 20, 2022.

  1. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,105
    Heya, this should be the last feature update.

    There was one major issue I had with this turret thing, and it took me a while to figure out what I wanted and how to get it. It's the motion itself. RotateTowards was a decent solution just to get things moving in the right direction, but I imagined the animation to be much more open to some sort of customization.

    Now this is a tricky problem. What kind of customization exactly? Well, I imagined a range of all kinds of turrets, from very tight sci-fi "fly-swatting" turrets that buzz all over the place, to heavy WW2-era steel beasts akin to German 88mm AA/AT guns.

    This implied that what I really wanted was some weird measure of "mass" but when you think about it, the mass itself is invisible, but lends itself to angular inertia, meaning that all we need to successfully depict a massive object is to fake accelerating/decelerating motion that is consistent with the feel of rotating object at a certain scale.

    Of course, I thought of slerping and nlerping right from the beginning, but lerping of any kind introduces a big problem. Namely, all interpolating functions depend on knowing the start and end point, for which they produce the interpolated values. Change one end point and you introduce a weird discontinuity. Moreover, scale the interval and you introduce the scaling of velocity, which is a measure from one interpolated step to another over some delta time. Of course, there are ways to recalculate the amount of necessary steps and make this less apparent, but we shouldn't forget the scenario in which the target is constantly and unpredictably moving. But beyond that, how to then introduce accelerating motion (easing) in a rotating framework where the target is free to move at all times? It's basically doomed.

    So I had to abandon the idea of taming this beast altogether, and after some research and experimentation I came up with something that's actually incremental in nature, instead of being tied to absolute end points, which is exactly how any mechanical turret would behave in reality.

    And, when it comes to mechanical motion, we're already in a very good state. Namely, the two axes of rotation are already decoupled, and the code already reacts live to a moving target. Finally, we can linearize this rotational system for free, because we already extract the actual degrees of rotation, which is perfect.

    So what I did was a half-artistic half-physicsy approach which took a lot of experimenting to get just right. Here's what I wanted to see from such a system:
    1. base and gun should emulate having a "motor", capable of introducing a simplified torque,
    2. whatever the input, the turret should be fairly precise and end up exactly (or nearly exactly) oriented toward the target,
    3. the turret should never abruptly stop or suddenly get up to speed, especially when the target is moving, including the transversal motion (over the sights, which implies a sudden change in the motion direction); this has a notorious but important effect of overtracking due to not being able to decelerate in time,
    4. top speed, acceleration, and deceleration should be customizable, allowing the user to define a specific apparent inertia and tracking speed.
    So let's start from the notion of "simplified torque".
    Well, torque is τ = r x F and F = ma, however given that the turret logic is entirely size-agnostic we can simply ignore both radius and mass, and all that's left is the linear acceleration. In our case however, we care only about angular acceleration, so it's even more simplified.

    Ok, so if we supply a constant angular acceleration, we apply that to some angular velocity. Then if we apply this velocity to the current orientation, that should be enough. Fingers crossed.

    Given that our rotations can easily be depicted on a 1D number line, we can imagine any one component (say turret's base) to move strictly linearly. I.e. if it was pointing at 15° and it should in fact point at 90°, it should accelerate toward right up to a certain point. It should then stop accelerating and start braking, until it finally reaches 90° where it should come to rest. Yes, this is easier said then done, but the real headache starts when you think about the opposite scenario: if it was pointing at 90° when it should in fact point at 15°, it's supposed to move toward left. This implies the angular velocity itself should be negative in this case.

    Therefore I've decided to work with "signed velocity" to make the whole thing ambivalent toward the way of motion.

    I envisioned the motion would have three independent parts: accelerating part, active braking part, and finally a rolling lead-in. The latter kind serves a very simple but important purpose of a catch-all case: it's a low-speed motion that rolls the current value toward target, but is also capable of zeroing-in and coming to a complete halt. This kind of motion isn't sufficient on its own, but will help us combat the imprecisions and/or any chaotic near-zero behavior.

    So let's try this
    Code (csharp):
    1. velocity += 0.05f * Time.deltaTime;
    This is some kind of small acceleration obviously. It will always increment velocity, and therefore it will always accelerate to the right. So let's make it work both ways instead.
    Code (csharp):
    1. velocity += signGoal * 0.05f * Time.deltaTime;
    We apply a sign to this acceleration. signGoal is either -1 or +1 and basically stores whether the shortest distance toward the goal is to the left or to the right. Notice that even though this sign can abruptly change, this won't change the final velocity at the same rate. Instead it will slowly accumulate, until it tips over, as it should.

    However, if we apply this to our turret, this won't behave as intended. It will slowly tend toward the goal, but because the velocity is accumulated over time, it will overtrack and end up reversing after some time. The motion will then proceed to oscillate back and forth incapable of actually stopping exactly where it is supposed to land.

    First, let's prohibit uncontrolled accumulation. This rolling behavior should really be weak, almost "magnetic" in the way it leads to the target and then snaps to it.
    Code (csharp):
    1. velocity += signGoal * 0.05f * Time.deltaTime;
    2. if(absDist < 0.5f) velocity *= 0.9f;
    3. if(Mathf.Abs(velocity) < 0.015f) velocity = 0f;
    So let's consider the distance to the target and when we're within half a degree of it, begin to diminish the velocity. Finally if the absolute velocity is less than some sufficiently small value, set it to zero. This should make the whole thing just stop when it's critically near the desired angle.

    On its own, this code isn't very impressive. The object will rotate very slowly, but on the upside, it will know when it's done. Now that we've reached zero, we can guard this code like this
    Code (csharp):
    1. if(velocity != 0f) {
    2.   velocity += sgnGoal * 0.05f * Time.deltaTime;
    3.   if(absDist < 0.5f) velocity *= 0.9f;
    4.   if(Mathf.Abs(velocity) < 0.015f) velocity = 0f;
    5. }
    but this will have a side-effect of prohibiting any movement, since we now start at a zero velocity and nothing will ever change this. Let's fix this by introducing proper acceleration.

    First of all, how long should acceleration last? Well that depends on how much time we need to properly decelerate. We want to begin decelerating as late as possible to maximize the average velocity, but at the same time, as early as possible to have just enough time to brake all the way to zero. So there's an inflection point where acceleration stops and deceleration takes over. Hopefully it's now clear that if these are the two main modes of operation, we use the third kind of motion only as a safety net, and to provide a closure.

    Acceleration works similarly to rolling, in that it's supposed to change velocity towards the target, however it should never accelerate beyond the top speed.
    Code (csharp):
    1. velocity += sgnGoal * acceleration * Time.deltaTime;
    2. if(Mathf.Abs(velocity) > topSpeed) velocity = sgnTravel * topSpeed;
    Here we're using the travelling sign, -1 or +1, to be able to tell which way we're currently moving.

    But now is the time to introduce the metric of a braking distance or else this will accelerate the turret's base non-stop. We should be able to compute this quite easily by knowing that -a = V / dt (where -a is some deceleration and dt is 'time to decelerate to zero'). So the time is then dt = V / -a.
    Code (csharp):
    1. var decTime = absVel / deceleration;
    If we do a little math it happens that stopping distance is T = 1/2*V*dt
    Code (csharp):
    1. var brkDist = 0.5f * absVel * decTime;
    We can now guard acceleration with a simple test. Acceleration should only ever occur when we're supposed to get somewhere else. But if we try to formalize this as "when distance is greater than zero" or "when distance is greater than 'braking distance'" we encounter a problem, because this might be intuitive but isn't really well-defined.

    Braking distance is heavily dependent on the current velocity. Distance will never be greater than 'braking distance' if velocity is zero. And likewise, if we test for "distance greater than zero" this will nearly always be true. Fortunately we can safeguard the two conditions by considering that the braking distance has to scale uniformly with the speed. We can thus interpret the tiny braking distance as "moving too slow" which helps us discern what's what.
    Code (csharp):
    1. if((brkDist <= 1f && absDist >= 0.5f) || (brkDist > 1f && absDist > brkDist)) {
    2.   velocity += sgnGoal * acceleration * Time.deltaTime;
    3.   if(Mathf.Abs(velocity) > topSpeed) velocity = sgnTravel * topSpeed;
    4. }
    And so "if moving too slow and distance to target is greater than half a degree -or- if not moving too slow and total distance is greater than the braking distance" will suffice. This is not a hard science btw, this is my own artistic approach to solving this. And it's good enough.

    Then, to solve deceleration, we do something similar, but try to stay true to what braking means. It shouldn't behave as an active torque, but more of an applied friction. This is a cosmetic choice to prevent rare but weird glitches where the act of braking switches the sign of velocity.

    Here we'll test for whether velocity is heading in the same way where the target is, and so the braking will only ever occur if this is true. Additionally, we check if total distance is less or equal than the braking distance, because that's logically the braking window. Given the context, we can also make sure to never drop below zero.
    Code (csharp):
    1. if(way > 0f && absDist <= brkDist) {
    2.   velocity -= sgnTravel * deceleration * Time.deltaTime;
    3.   if(Math.Abs(velocity) < 0.015f) velocity = 0f;
    4. }
    Notice
    -=
    which denotes that we're taking from the velocity, instead of adding to it. Also the sign we're using is sgnTravel instead of sgnGoal we used for rolling and acceleration.

    Finally we can implement this as a conditional cascade, and finish the whole function.
    Here velocity and sgnDistance are both signed values, however, topSpeed, acceleration, deceleration, and deltaTime are unsigned (absolute values).
    Code (csharp):
    1. float computeVelocity(float velocity, float sgnDistance, float topSpeed, float acceleration, float deceleration, float deltaTime) {
    2.   var absVel = Mathf.Abs(velocity); // when we care exclusively about the amount, not the way of travelling
    3.   var sgnTravel = snz(velocity); // here we extract the sign: notice that Mathf.Sign doesn't suffice, we treat 0 as positive
    4.  
    5.   var decTime = absVel / deceleration; // estimated time to decelerate to zero (a = v/dt; dt = v/a)
    6.   var brkDist = .5f * absVel * decTime; // stopping distance (T = 1/2v*dt)
    7.  
    8.   var absDist = Mathf.Abs(sgnDistance);
    9.   var sgnGoal = snz(sgnDistance);
    10.   var way = swfz(sgnDistance) * swfz(velocity); // I'll explain this later
    11.  
    12.   // var eta = Mathf.Max(0f, absDist - brkDist) / absVel + decTime; // total estimated time of arrival -- I might use this in the future
    13.  
    14.   if((brkDist <= 1f && absDist >= .5f) || (brkDist > 1f && absDist < brkDist)) { // accelerate
    15.     velocity += sgnGoal * acceleration * deltaTime;
    16.     if(Mathf.Abs(velocity) > topSpeed) velocity = sgnTravel * topSpeed;
    17.  
    18.   } else if(way > 0f && absDist <= brkDist) { // active braking
    19.     velocity -= sgnTravel * deceleration * deltaTime;
    20.     if(Mathf.Abs(velocity) < .015f) velocity = 0f;
    21.  
    22.   } else { // rolling
    23.     if(velocity != 0f) {
    24.       velocity += sgnGoal * .05f * deltaTime;
    25.       if(absDist < .5f) velocity *= .9f;
    26.       if(Mathf.Abs(velocity) < .015f) velocity = 0f;
    27.     }
    28.   }
    29.  
    30.   return velocity;
    31. }
    The only thing lacking is snz, which means "sign no zero"
    float snz(float v) => (v < 0f)? -1f : 1f;


    And swfz, which means "sign with FAT zero" a simple sign function that is extra-tolerant toward zero and widens it a bit.
    float swfz(float v) => (Mathf.Abs(v) < 1E-4f)? 0f : snz(v);
    Simple stuff right?

    So what's that
    way
    variable again? Well, it's a simple comparison between the way we actually travel and the way we should be travelling. Whenever the two are different, you get -1 as a result, when they are the same, +1, and if any of these equals 0, you get 0. We actively brake only when way > 0f.

    To be continued.
     
    Last edited: Mar 2, 2022
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,105
    Okay, so what else is missing for this to replace the previous RotateTowards solution?

    Well, to prepare for this, I've changed the code a lot in the meantime, so it's not as easy to explain every single step, but in a nutshell, this is supposed to work with raw yaw and pitch angles (which we kind of need to extract anyway, so there is no additional cost), then decides how to modulate the independent velocities (with the function explained above), and finally uses AngleAxis to bake it all back into quaternions (which is done instead of RotateTowards).

    topXVel
    ,
    XAcc
    ,
    XDec
    are all inspector fields, where X is either Base or Gun. But other than that, we're supposed to get the actual distances.

    This is similar to how limits work, and this is the whole code regarding velocities
    Code (csharp):
    1. var dt = Time.deltaTime;
    2.  
    3. var yd = DEG180 - angMod(_actualAngles.yaw - _aimingAngles.yaw + DEG180); // signed distance between yaw angles
    4. var pd = _aimingAngles.pitch - _actualAngles.pitch; // signed distance between pitch angles
    5.  
    6. _velocity.yaw = computeVelocity(_velocity.yaw, yd, topBaseVel, baseAcc, baseDec, dt);
    7. _velocity.pitch = computeVelocity(_velocity.pitch, pd, topGunVel, gunAcc, gunDec, dt);
    Finally
    Code (csharp):
    1. if(!isDoneReorienting() || (Mathf.Abs(_velocity.yaw) > 0f || Mathf.Abs(_velocity.pitch) > 0f)) {
    2.   xfBase.localRotation = Quaternion.AngleAxis(_actualAngles.yaw + _velocity.yaw * dt, Vector3.up);
    3.   xfGun.localRotation = Quaternion.AngleAxis(-(_actualAngles.pitch + _velocity.pitch * dt), Vector3.right);
    4.   ...
    5.  
    The differences in how pitch is treated are there because of the ways my conventions differ from Unity's.

    I don't think we've had
    _actualAngles
    before, that's something I've introduced in the meantime. They are also exposed through the public interface, so that you can ask the turret about its ongoing internal angles at any point in time.

    That's all for now. I'll likely work a bit more on several modes of operation (i.e. Instantaneous, Mechanical, Uniform, LatentPitch etc) in the future. I'll post the full code when I clean it up, add some comments etc.
     
    Last edited: Mar 2, 2022
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,105
    After a couple of months and much experimenting I've learned about a technique (or is it a discipline?) called 'second order dynamics' which actually solves all my issues with how to animate the turret. The math is very robust yet lightweight, and I've done a couple of things with it, including a camera motion for my current project -- and it retains the adjustable animation "flavor" through only a couple of parameters, while providing a reliable and frame-independent zero-edge-cases solution to smooth and canny motion.

    I want to refurbish this turret solution I came up with in this thread with a second order dynamics add-on at some point, hopefully soon, if time lets me -- and after that I will consider this solution an off-the-shelf complete one, so stay tuned if you're interested.
     
  4. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    I’m following
     
    orionsyndrome likes this.