unity-game-enginephysx

Lossless movement in hinge joints - Unity


I've created a simple pendulum in Unity - GameObject with Rigidbody and Hinge Joint components. I've set both drag ang angular drag to 0. With starting position at 90 degrees I'd expect the pendulum to swing back and forth from 90 to -90 degrees. However, that's not the case - the amplitude decays very quickly, but for small angles the pendulum looks like it's never going to stop.

My question is: How should I configure hinge joints in order to achieve full control over physics and forces that resist motion? My goal is to have physics simulation as precise as it's possible, even at the cost of performance.

I've already tried to reduce time intervals for fixed step and increased solver iterations - none of these worked out.

Why do I need it? I'm planning to design a control system for multiple inverted pendulum on a cart. I have a mathematical model of my pendulum implemented in Matlab and I wanted to verify it using a simple model in Unity (because in that case I adjust all parameters, initial conditions etc. and the physics engine is calculating everything for me). If it turns out the physics engine that is backing Unity isn't reliable enough, what other software would you recommend me?


Solution

  • My understanding is that due to the way Unity's physics operates, there can be a loss of kinetic energy over time in this sort of pendulum motion if you only use a hinge joint. Basically, if you want an accurate pendulum simulation, you have to bypass the physics engine and implement it directly.

    There is a very good post on the gamedev stackexchange originally posted by MLM about how to implement a more accurate pendulum simulation in Unity, which I have pasted below.


    I thought this would be a relatively simple problem to solve but I spent a couple days trying to figure out how the heck to simulate pendulum movement. I didn't want to cheat and just change the x,y position based on sin(theta) and cos(theta) curves. Instead I wanted to deal with the two forces that are applied in real life, Gravity and Tension. The main piece I was missing was centripetal force.

    The Pendulum (mathematics) wikipedia page has a great animation(below, on left) explaining the pendulum motion. You can see my result(on right) strikingly similar to that diagram

    The "bob" is the swinging object and the "pivot" is the origin/root.

    Pendulum Motion: velocity and acceleration

    I also found this article and diagram(below) pretty helpful:


    Theta equals the angle between the rope and the direction of gravity.

    When the bob is on the left or right the tension equals:

    m*g*cos(theta)

    The reason the tension force is greater as the bob approaches equilibrium point(middle) is because of centripetal force:

    (m*v^2)/ropeLength

    So the overrall tension formula looks like as the bob swings is:

    m*g*cos(theta) + (m*v^2)/ropeLength

    There are two forces in the pendulum system:

    Just apply gravity to your object like you would for a normal object and then apply the tension. When you apply the forces, just multiply the force by the direction and deltaTime.

    Below is the Pendulum.cs script(also as a GitHub Gist). It works quite well but there is some rounding error drift if you leave it for a while (doesn't return to exactly same position).

    The script works in 3D but of course a pendulum only swings in a 2D plane. It also works with gravity in any direction. So for example, if you invert the gravity the pendulum works upside down. Edit->Project Settings->Physics->Gravity

    It is very important to have a consistent relatively small deltaTime when updating the pendulum so that you do not bounce around the curve. I am using the technique found in this article, FIX YOUR TIMESTEP! by Glenn Fiedler to accomplish this. Check the Update() function below to see how I implemented it.

    Also as a GitHub Gist

    using UnityEngine;
    using System.Collections;
    
    // Author: Eric Eastwood (ericeastwood.com)
    //
    // Description:
    //      Written for this gd.se question: http://gamedev.stackexchange.com/a/75748/16587
    //      Simulates/Emulates pendulum motion in code
    //      Works in any 3D direction and with any force/direciton of gravity
    //
    // Demonstration: https://i.sstatic.net/tcJJ1.gif
    //
    // Usage: https://i.sstatic.net/lgr9t.png
    public class Pendulum : MonoBehaviour {
    
        public GameObject Pivot;
        public GameObject Bob;
    
    
        public float mass = 1f;
    
        float ropeLength = 2f;
    
        Vector3 bobStartingPosition;
        bool bobStartingPositionSet = false;
    
        // You could define these in the `PendulumUpdate()` loop 
        // But we want them in the class scope so we can draw gizmos `OnDrawGizmos()`
        private Vector3 gravityDirection;
        private Vector3 tensionDirection;
    
        private Vector3 tangentDirection;
        private Vector3 pendulumSideDirection;
    
        private float tensionForce = 0f;
        private float gravityForce = 0f;
    
    
        // Keep track of the current velocity
        Vector3 currentVelocity = new Vector3();
    
        // We use these to smooth between values in certain framerate situations in the `Update()` loop
        Vector3 currentStatePosition;
        Vector3 previousStatePosition;
    
        // Use this for initialization
        void Start () {
            // Set the starting position for later use in the context menu reset methods
            this.bobStartingPosition = this.Bob.transform.position;
            this.bobStartingPositionSet = true;
    
            this.PendulumInit();
        }
    
    
        float t = 0f;
        float dt = 0.01f;
        float currentTime = 0f;
        float accumulator = 0f;
    
        void Update()
        {
            /* */
            // Fixed deltaTime rendering at any speed with smoothing
            // Technique: http://gafferongames.com/game-physics/fix-your-timestep/
            float frameTime = Time.time - currentTime;
            this.currentTime = Time.time;
    
            this.accumulator += frameTime;
    
            while (this.accumulator >= this.dt)
            {
                this.previousStatePosition = this.currentStatePosition;
                this.currentStatePosition = this.PendulumUpdate(this.currentStatePosition, this.dt);
                //integrate(state, this.t, this.dt);
                accumulator -= this.dt;
                this.t += this.dt;
            }
    
            float alpha = this.accumulator/this.dt;
    
            Vector3 newPosition = this.currentStatePosition*alpha + this.previousStatePosition*(1f-alpha);
    
            this.Bob.transform.position = newPosition; //this.currentStatePosition;
            /* */
    
            //this.Bob.transform.position = this.PendulumUpdate(this.Bob.transform.position, Time.deltaTime);
        }
    
    
        // Use this to reset forces and go back to the starting position
        [ContextMenu("Reset Pendulum Position")]
        void ResetPendulumPosition()
        {
            if(this.bobStartingPositionSet)
                this.MoveBob(this.bobStartingPosition);
            else
                this.PendulumInit();
        }
    
        // Use this to reset any built up forces
        [ContextMenu("Reset Pendulum Forces")]
        void ResetPendulumForces()
        {
            this.currentVelocity = Vector3.zero;
    
            // Set the transition state
            this.currentStatePosition = this.Bob.transform.position;
        }
    
        void PendulumInit()
        {
            // Get the initial rope length from how far away the bob is now
            this.ropeLength = Vector3.Distance(Pivot.transform.position, Bob.transform.position);
            this.ResetPendulumForces();
        }
    
        void MoveBob(Vector3 resetBobPosition)
        {
            // Put the bob back in the place we first saw it at in `Start()`
            this.Bob.transform.position = resetBobPosition;
    
            // Set the transition state
            this.currentStatePosition = resetBobPosition;
        }
    
    
        Vector3 PendulumUpdate(Vector3 currentStatePosition, float deltaTime)
        {
            // Add gravity free fall
            this.gravityForce = this.mass * Physics.gravity.magnitude;
            this.gravityDirection = Physics.gravity.normalized;
            this.currentVelocity += this.gravityDirection * this.gravityForce * deltaTime;
    
            Vector3 pivot_p = this.Pivot.transform.position;
            Vector3 bob_p = this.currentStatePosition;
    
    
            Vector3 auxiliaryMovementDelta = this.currentVelocity * deltaTime;
            float distanceAfterGravity = Vector3.Distance(pivot_p, bob_p + auxiliaryMovementDelta);
    
            // If at the end of the rope
            if(distanceAfterGravity > this.ropeLength || Mathf.Approximately(distanceAfterGravity, this.ropeLength))
            {
    
                this.tensionDirection = (pivot_p - bob_p).normalized;
    
                this.pendulumSideDirection = (Quaternion.Euler(0f, 90f, 0f) * this.tensionDirection);
                this.pendulumSideDirection.Scale(new Vector3(1f, 0f, 1f));
                this.pendulumSideDirection.Normalize();
    
                this.tangentDirection = (-1f * Vector3.Cross(this.tensionDirection, this.pendulumSideDirection)).normalized;
    
    
                float inclinationAngle = Vector3.Angle(bob_p-pivot_p, this.gravityDirection);
    
                this.tensionForce = this.mass * Physics.gravity.magnitude * Mathf.Cos(Mathf.Deg2Rad * inclinationAngle);
                float centripetalForce = ((this.mass * Mathf.Pow(this.currentVelocity.magnitude, 2))/this.ropeLength);
                this.tensionForce += centripetalForce;
    
                this.currentVelocity += this.tensionDirection * this.tensionForce * deltaTime;
            }
    
            // Get the movement delta
            Vector3 movementDelta = Vector3.zero;
            movementDelta += this.currentVelocity * deltaTime;
    
    
            //return currentStatePosition + movementDelta;
    
            float distance = Vector3.Distance(pivot_p, currentStatePosition + movementDelta);
            return this.GetPointOnLine(pivot_p, currentStatePosition + movementDelta, distance <= this.ropeLength ? distance : this.ropeLength);
        }
    
        Vector3 GetPointOnLine(Vector3 start, Vector3 end, float distanceFromStart)
        {
            return start + (distanceFromStart * Vector3.Normalize(end - start));
        }
    
        void OnDrawGizmos()
        {
            // purple
            Gizmos.color = new Color(.5f, 0f, .5f);
            Gizmos.DrawWireSphere(this.Pivot.transform.position, this.ropeLength);
    
            Gizmos.DrawWireCube(this.bobStartingPosition, new Vector3(.5f, .5f, .5f));
    
    
            // Blue: Auxilary
            Gizmos.color = new Color(.3f, .3f, 1f); // blue
            Vector3 auxVel = .3f * this.currentVelocity;
            Gizmos.DrawRay(this.Bob.transform.position, auxVel);
            Gizmos.DrawSphere(this.Bob.transform.position + auxVel, .2f);
    
            // Yellow: Gravity
            Gizmos.color = new Color(1f, 1f, .2f);
            Vector3 gravity = .3f * this.gravityForce*this.gravityDirection;
            Gizmos.DrawRay(this.Bob.transform.position, gravity);
            Gizmos.DrawSphere(this.Bob.transform.position + gravity, .2f);
    
            // Orange: Tension
            Gizmos.color = new Color(1f, .5f, .2f); // Orange
            Vector3 tension = .3f * this.tensionForce*this.tensionDirection;
            Gizmos.DrawRay(this.Bob.transform.position, tension);
            Gizmos.DrawSphere(this.Bob.transform.position + tension, .2f);
    
            // Red: Resultant
            Gizmos.color = new Color(1f, .3f, .3f); // red
            Vector3 resultant = gravity + tension;
            Gizmos.DrawRay(this.Bob.transform.position, resultant);
            Gizmos.DrawSphere(this.Bob.transform.position + resultant, .2f);
    
    
            /* * /
            // Green: Pendulum side direction
            Gizmos.color = new Color(.3f, 1f, .3f);
            Gizmos.DrawRay(this.Bob.transform.position, 3f*this.pendulumSideDirection);
            Gizmos.DrawSphere(this.Bob.transform.position + 3f*this.pendulumSideDirection, .2f);
            /* */
    
            /* * /
            // Cyan: tangent direction
            Gizmos.color = new Color(.2f, 1f, 1f); // cyan
            Gizmos.DrawRay(this.Bob.transform.position, 3f*this.tangentDirection);
            Gizmos.DrawSphere(this.Bob.transform.position + 3f*this.tangentDirection, .2f);
            /* */
        }
    }
    

    More glamour shots: