Search code examples
c#unity-game-enginevectorphysicsgame-physics

Calculating rotational speed of a GameObject, given a force at position C# Unity


I found this script that returns rotational speed (taking into account where force is applied and distance from center of mass).

public Vector3 ForceToTorque(Vector3 force, Vector3 position, ForceMode forceMode = ForceMode.Force)
{
    Vector3 t = Vector3.Cross(position - body.worldCenterOfMass, force);
    ToDeltaTorque(ref t, forceMode);

    return t;
}

private void ToDeltaTorque(ref Vector3 torque, ForceMode forceMode)
{
    bool continuous = forceMode == ForceMode.VelocityChange || forceMode == ForceMode.Acceleration;
    bool useMass = forceMode == ForceMode.Force || forceMode == ForceMode.Impulse;

    if (continuous) torque *= Time.fixedDeltaTime;
    if (useMass) ApplyInertiaTensor(ref torque);
}

private void ApplyInertiaTensor(ref Vector3 v)
{
    v = body.rotation * Div(Quaternion.Inverse(body.rotation) * v, body.inertiaTensor);
}

private static Vector3 Div(Vector3 v, Vector3 v2)
{
    return new Vector3(v.x / v2.x, v.y / v2.y, v.z / v2.z);
}

With 2100 newtons I'm getting 0.6 radians (36 degrees) of rotation.

var test = rotationScript.ForceToTorque(shipFront.right * 2100, shipFront.position, ForceMode.Force);
Debug.Log(test + " " + test * Mathf.Rad2Deg);
// Above gives (0, 0.6, 0) and (0, 36.1, 0)

But using AddForceAtPosition to rotate the ship with the same force I don't get the same result

if (currTurn > 0) {
    body.AddForceAtPosition(shipFront.right * 2100, shipFront.position, ForceMode.Force);
    body.AddForceAtPosition(-shipBack.right * 2100, shipBack.position, ForceMode.Force);
} else if (currTurn < 0) {
    body.AddForceAtPosition(-shipFront.right * 2100, shipFront.position, ForceMode.Force);
    body.AddForceAtPosition(shipBack.right * 2100, shipBack.position, ForceMode.Force);
}

It's not giving me 36 degrees per second - I tested by counting how long it took to do a full 360 spin, supposedly it should've been done in 10s but it took 10s to rotate only ~90º.

There's a lot I don't understand in the first script, like most of the physics part, but I don't see it taking into consideration my ships mass (body.worldCenterOfMass?), could that be it?

I need this so I can rotate my ship more precisely.


Solution

  • The major mistake was a confusion between acceleration and velocity. Applying a torque leads to angular acceleration (radians per second per second), which is given by your test (36.1 deg / s^2 around the Y-axis). This is not the angular velocity, but the rate-of-change, so you should not expect the same result.

    (Also the force passed to ForceToTorque is only half of the required force.)


    Quick physics notes - torque equation:

    enter image description here

    I is the moment-of-inertia tensor, a 3x3 matrix given by the integral above, over all mass elements of the body. It is obviously symmetric in its indices i and j, so it is diagonalizable (any decent linear algebra book):

    enter image description here

    D is the M-of-I tensor in the body's principal axes basis, and R is the rotation matrix from the principal to the current basis. The diagonal elements of D are the values of the vector body.inertiaTensor, which means that Unity always aligns the object's principal axes with the world axes, and that we always have I = D.

    Therefore to obtain the angular acceleration arising from a torque:

    enter image description here

    Where the last line is performed by Div.


    A better way to ensure accurate rotation is to apply an angular impulse, which directly changes the angular velocity. Q and the corresponding linear impulse P required both satisfy:

    enter image description here

    This directly changes the angular velocity of the body. (*) is a condition that the input parameters must satisfy. You can still use AddForceAtPosition with ForceMode.Impulse. Code:

    Vector3 AngularvelocityToImpulse(Vector3 vel, Vector3 position)
    {
       Vector3 R = position - body.worldCenterOfMass;
       Vector3 Q = MultiplyByInertiaTensor(vel);
    
       // condition (*)
       if (Math.Abs(Vector3.Dot(Q, R)) > 1e-5) 
          return new Vector3();
    
       // one solution
       // multiply by 0.5 because you need to apply this to both sides
       // fixes the factor-of-2 issue from before
       return 0.5 * Vector3.Cross(Q, R) / R.sqrMagnitude;
    } 
    
    Vector3 MultiplyByInertiaTensor(Vector3 v)
    {
       return body.rotation * Mul(Quaternion.Inverse(body.rotation) * v, body.inertiaTensor);
    }
    
    Vector3 Mul(Vector3 v, Vector3 a)
    {
       return new Vector3(v.x * a.x, v.y * a.y, v.z * a.z);
    }
    

    To apply:

    var test = AngularvelocityToImpulse(...);
    body.AddForceAtPosition(test, shipFront.position, ForceMode.Impulse);
    body.AddForceAtPosition(-test, shipBack.position, ForceMode.Impulse);