Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Physics prediction slightly off compared to physics simulation.

Discussion in 'Physics' started by _geo__, Jun 20, 2023.

  1. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    Hi all,

    I am currently writing some physics prediction code (ballistic trajectory). I have time (flightDuration) and a distance (a simple float). I want to calculate the start velocity for a projectile.

    I have the formula and it kinda works. Yet, the point of arrival is always a bit short and I don't quite get why.

    Used formula [1]:
    upload_2023-6-20_10-10-10.png
    Code:

    var velocity = distanceVector / time - new Vector3(0, g, 0) * 0.5f * time;


    I have created a single code example which you can add to an empty scene and run. It's really bare bones, yet it shows the problem. My expectation was that the white projectiles hit the red target exactly.

    However, they are always a bit short. Like this (the white one should be at the position of the red target once it intersects the ground):

    upload_2023-6-20_10-20-34.png

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class PhysicsPredictionTest : MonoBehaviour
    6. {
    7.     private Material targetMaterial;
    8.     private Material groundMaterial;
    9.     private Material projectileMaterial;
    10.    
    11.     IEnumerator Start()
    12.     {
    13.         var shader = Shader.Find("Standard");
    14.        
    15.         targetMaterial = new Material(shader);
    16.         targetMaterial.color = Color.red;
    17.        
    18.         groundMaterial = new Material(shader);
    19.         groundMaterial.color = new Color(0f, 0.7f, 0f);
    20.        
    21.         projectileMaterial = new Material(shader);
    22.         projectileMaterial.color = Color.white;
    23.  
    24.         var ground = GameObject.CreatePrimitive(PrimitiveType.Quad);
    25.         ground.name = "Ground";
    26.         ground.GetComponent<MeshRenderer>().sharedMaterial = groundMaterial;
    27.         ground.GetComponent<MeshCollider>().enabled = false;
    28.         ground.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
    29.         ground.transform.localScale = Vector3.one * 1000f;
    30.  
    31.         var target = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    32.         target.name = "Target";
    33.         target.GetComponent<MeshRenderer>().sharedMaterial = targetMaterial;
    34.         target.GetComponent<SphereCollider>().enabled = false;
    35.         target.transform.localScale = Vector3.one * 0.05f;
    36.        
    37.         // Config (we derive the starting speed of the projectiles from these).
    38.         float distance = 20f;
    39.         float flightDuration = 3f;
    40.  
    41.         // Spawn target
    42.         target.transform.localPosition = new Vector3(0f, 0f, distance);
    43.        
    44.         // Spawn projectiles
    45.         for (int i = 0; i < 100; i++)
    46.         {
    47.             yield return new WaitForSeconds(0.1f);
    48.             spawnProjectile(distance, flightDuration);
    49.         }
    50.     }
    51.  
    52.     void spawnProjectile(float distance, float time)
    53.     {
    54.         var startVelocity = calcStartVelocity(distance, time);
    55.        
    56.         var projectile = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    57.         projectile.name = "projectile";
    58.         projectile.GetComponent<MeshRenderer>().sharedMaterial = projectileMaterial;
    59.         projectile.transform.localScale = Vector3.one * 0.05f;
    60.        
    61.         var collider = projectile.GetComponent<SphereCollider>();
    62.         collider.radius = 0.05f;
    63.        
    64.         var rb = projectile.AddComponent<Rigidbody>();
    65.         rb.AddForce(startVelocity, ForceMode.VelocityChange);
    66.     }
    67.    
    68.     Vector3 calcStartVelocity(float distance, float time)
    69.     {
    70.         var startPos = Vector3.zero;
    71.         var targetPos = new Vector3(0f, 0f, distance);
    72.         var distanceVector = targetPos - startPos;
    73.        
    74.         // With this it is always a bit short
    75.         float g = Physics.gravity.y;
    76.        
    77.         // With this it is an exact match, even across vast distances (500+). But this does not compute with my brain.
    78.         // float g = -9.87f;
    79.        
    80.         // Derived from equation for motion under constant acceleration.
    81.         var velocity = distanceVector / time - new Vector3(0, g, 0) * 0.5f * time;
    82.         return velocity;
    83.     }
    84. }
    85.  

    I expected my prediction to diverge from the simulation result on big distances due to floating point inaccuracy but right now it's always off.

    I feel like I am missing something very obvious (Unity 2021.2.0f1, 2022 LTS and 2023, no changes to physics settings)
    upload_2023-6-20_10-23-11.png

    Thank you.

    Update: Just realized I posted it in the Scripting forum. If a mod is nearby maybe this should be moved to the physics forum. Thank you.
     

    Attached Files:

    Last edited: Jun 20, 2023
  2. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    I have a hunch now. If I put the starting position at f(t), where t = 0.5 * Time.fixedDeltaTime then I get reliable results.
    Code (CSharp):
    1. Vector3 calcStartPosition(Vector3 startVelocity)
    2.     {
    3.         // Start at the position at half a physics update.
    4.         float dT = Time.fixedDeltaTime * 0.5f;
    5.      
    6.         return new Vector3(
    7.             0f,
    8.             dT * startVelocity.y + Physics.gravity.y * dT * dT * 0.5f,
    9.             dT * startVelocity.z
    10.         );
    11.     }

    That got me thinking, maybe it's a timing issue. After all, I was spawning a physics object in a coroutine and not FixedUpdate. Sadly, moving it to FixedUpdate did not change the outcome (it still requires the start-position-fix).

    This is the current working version (spawning in FixedUpdate AND modifying the start position). If have tested it with distances up to 500 (working fine). Even tried different fixed delta times in the settings (up to 0.33). It also works fine.

    Yet, I do not understand WHY it is working (or if it even should).

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Serialization;
    6.  
    7. public class PhysicsPredictionTest : MonoBehaviour
    8. {
    9.     private Material targetMaterial;
    10.     private Material groundMaterial;
    11.     private Material projectileMaterial;
    12.  
    13.     public float Distance = 20f;
    14.     public float FlightDuration = 3f;
    15.  
    16.     IEnumerator Start()
    17.     {
    18.         var shader = Shader.Find("Standard");
    19.    
    20.         targetMaterial = new Material(shader);
    21.         targetMaterial.color = Color.red;
    22.    
    23.         groundMaterial = new Material(shader);
    24.         groundMaterial.color = new Color(0f, 0.7f, 0f);
    25.    
    26.         projectileMaterial = new Material(shader);
    27.         projectileMaterial.color = Color.white;
    28.  
    29.         var ground = GameObject.CreatePrimitive(PrimitiveType.Quad);
    30.         ground.name = "Ground";
    31.         ground.GetComponent<MeshRenderer>().sharedMaterial = groundMaterial;
    32.         ground.GetComponent<MeshCollider>().enabled = false;
    33.         ground.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
    34.         ground.transform.localScale = Vector3.one * 1000f;
    35.  
    36.         var target = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    37.         target.name = "Target";
    38.         target.GetComponent<MeshRenderer>().sharedMaterial = targetMaterial;
    39.         target.GetComponent<SphereCollider>().enabled = false;
    40.         target.transform.localScale = Vector3.one * 0.04f;
    41.    
    42.         // Spawn target
    43.         target.transform.localPosition = new Vector3(0f, 0f, Distance);
    44.    
    45.         // Spawn projectiles
    46.         for (int i = 0; i < 300; i++)
    47.         {
    48.             yield return new WaitForSeconds(0.1f);
    49.             _spawnInNextUpdate = true;
    50.         }
    51.     }
    52.  
    53.  
    54.     bool _spawnInNextUpdate = false;
    55.  
    56.     void FixedUpdate()
    57.     {
    58.         if (_spawnInNextUpdate)
    59.         {
    60.             _spawnInNextUpdate = false;
    61.             spawnProjectile(Distance, FlightDuration);
    62.         }
    63.     }
    64.  
    65.     Vector3 calcStartPosition(Vector3 startVelocity)
    66.     {
    67.         // Start at the position at half a physics update.
    68.         float dT = Time.fixedDeltaTime * 0.5f;
    69.    
    70.         return new Vector3(
    71.             0f,
    72.             dT * startVelocity.y + Physics.gravity.y * dT * dT * 0.5f,
    73.             dT * startVelocity.z
    74.         );
    75.     }
    76.  
    77.     void spawnProjectile(float distance, float time)
    78.     {
    79.         var startVelocity = calcStartVelocity(distance, time);
    80.    
    81.         var projectile = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    82.         projectile.name = "projectile";
    83.         projectile.GetComponent<MeshRenderer>().sharedMaterial = projectileMaterial;
    84.         projectile.transform.localScale = Vector3.one * 0.05f;
    85.         projectile.transform.localPosition = calcStartPosition(startVelocity); // New
    86.    
    87.         var collider = projectile.GetComponent<SphereCollider>();
    88.         collider.radius = 0.05f;
    89.    
    90.         var rb = projectile.AddComponent<Rigidbody>();
    91.         rb.AddForce(startVelocity, ForceMode.VelocityChange);
    92.     }
    93.  
    94.     Vector3 calcStartVelocity(float distance, float time)
    95.     {
    96.         var startPos = Vector3.zero;
    97.         var targetPos = new Vector3(0f, 0f, distance);
    98.         var distanceVector = targetPos - startPos;
    99.    
    100.         // With this it is always a bit short
    101.         float g = Physics.gravity.y;
    102.    
    103.         // Derived from equation for motion under constant acceleration.
    104.         var velocity = distanceVector / time - new Vector3(0, g, 0) * 0.5f * time;
    105.         return velocity;
    106.     }
    107. }
    108.  
     
    Unifikation likes this.
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    3,899
    TL;DR; but I immediately spotted "time". ;)

    I think you are on the right track. For one, using fixedDeltaTime is a must. But ... how are you going to know how many fixedDeltaTime you need to calculate in advance?

    Since fixedDeltaTime is a constant and probably 0.2 (I think) you will have some deviation for any time that is not a multiple of 0.2 like 1.19s may be a little short whereas 1.01s may noticably overshoot (or vice versa). I think you get what I'm thinking? Not sure if that is the issue but my gut says it is. ;)
     
    _geo__ likes this.
  4. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    Thanks, yes, it may to have something to do with the physics simulation step. Though, I am pretty sure time or delta time should not have any effect on the outcome of the simulation. The shape of the parabola that is approximated should be the same in both scenarios (math and simlulation). At least to the point where it breaks apart due to float errors (long distance or big delta time).

    The result of the physics simulation simply does not match the mathematical formula. The parabolas do not intersect the ground plane at the same position. The red target is where the mathematical formula hits the ground plane. The white projectile (green below) is where the simulation hits it.

    In the screenshots in my first post you can see the white projectile nicely intersects the ground plane at y = 0. That, of course, does not happen with all distances as the physics step may hit a bit earlier or later. I chose the distance to get a nice intersection image. The thing is it should (if interpolated in between two steps) approximately hit the red target, yet it does not.

    I expected the same outcode (same parabola with the math equation and the physics simulation). Yet I do get different ones. My current "solution" simply starts the parabola at another spot than the mathematical model and tbh, that bothers me as it's not a logically sound fix but more a cover-up of the effects I see.

    Here is a quick image of what I mean (I hope it explains it a bit better):
    upload_2023-6-20_13-28-11.png

    My current theory is that the physics engine already calculated one tick for the projectile when I add the force and therefore it is a bit displaced (downwards dueto gravity) causing the shortened parabola effect which I then counteract with the changing the start position (move upwards). Though I still have to confirm that (it's just an idea at the moment). - I understand this may all be hard to follow if not familiar with the code. Thanks to anyone who follows me down into that rabbit hole :D
     
    Last edited: Jun 20, 2023
  5. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    Turns out reducing the delta time fixes it too. I admit I have not yet tried that as it's not my favourite solution (costly in terms of performance).

    It seems the simulation simply diverges a lot even with the default delta time of 0.02f. I did not expect that. If set to 0.002f it all just works.

    upload_2023-6-20_14-10-7.png

    I guess my question has now become about how to adequatly predict the simulation divergence and counteract it so that it gives reasonably accurate results.

    In my tests with dT = 0.02 if have seen it always shoots too short (never too far). Maybe that can be used to "fix" it. Hm, hmm ..

    Update:
    I have now tried the "start-position-fix" with various Unity versions and distances and I can report (at least in the simple example posted above) it works very accurately up to at least a distance of 1000 units (hits objects with a size of 0.04). Whether or not this will hold true for other scenarios I can not say but it's good enough for me. The neat thing is, since the fix is based on Time.fixedDeltaTime, it scales down automatically as delta time is reduced.
     
    Last edited: Jun 20, 2023
  6. Unifikation

    Unifikation

    Joined:
    Jan 4, 2023
    Posts:
    1,026
    I've found a happy medium between performance and accuracy at quadruple an ascertainable target rate.

    So if you want accuracy at 1/60th of a second "accuracy", set fixedUpdate to 1/240th of a second.
     
    _geo__ likes this.
  7. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    Yes. Sadly that also means 4x the physics calculations are done, which may not be feasible in all scenarios.
     
  8. Unifikation

    Unifikation

    Joined:
    Jan 4, 2023
    Posts:
    1,026
    Yeah... it's a pain in the arse. There's an OnAudioFilterRead that you can set to callback more frequently, but has complications of its own. However it's in its own thread, so you can do as much as you like in there without causing too many issues to anything else.

    Or, if you wanna go nuts, you can spin up your own thread and set a timer of your own.
     
  9. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,111
    For anyone interested. Here is the final "proof of concept" implementation. The code is a bit messy but it includes different solution strategies like calc start properties based on different known properties:
    * distance
    * speed
    * angle
    * duration
    * lowest used energy (this one I like very much as it gives you a nicely shooting turret without much thinking)

    HittingTargetsWithBallisitics.gif
    Code:
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using UnityEngine;
    4. using Quaternion = UnityEngine.Quaternion;
    5. using Vector3 = UnityEngine.Vector3;
    6.  
    7. public class PhysicsPredictionTest : MonoBehaviour
    8. {
    9.     private Material targetMaterial;
    10.     private Material groundMaterial;
    11.     private Material projectileMaterial;
    12.  
    13.     public Transform Source;
    14.     public Transform Target;
    15.     public float FlightDuration = 2f;
    16.     public float Angle = 45f;
    17.     public float Speed = 20f;
    18.  
    19.     IEnumerator Start()
    20.     {
    21.         var shader = Shader.Find("Standard");
    22.    
    23.         targetMaterial = new Material(shader);
    24.         targetMaterial.color = Color.red;
    25.    
    26.         groundMaterial = new Material(shader);
    27.         groundMaterial.color = new Color(0f, 0.7f, 0f);
    28.    
    29.         projectileMaterial = new Material(shader);
    30.         projectileMaterial.color = Color.white;
    31.  
    32.         var ground = GameObject.CreatePrimitive(PrimitiveType.Quad);
    33.         ground.name = "Ground";
    34.         ground.GetComponent<MeshRenderer>().sharedMaterial = groundMaterial;
    35.         ground.GetComponent<MeshCollider>().enabled = false;
    36.         ground.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
    37.         ground.transform.localScale = Vector3.one * 1000f;
    38.  
    39.         var target = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    40.         target.name = "Target";
    41.         target.GetComponent<MeshRenderer>().sharedMaterial = targetMaterial;
    42.         target.GetComponent<SphereCollider>().enabled = false;
    43.         target.transform.localScale = Vector3.one * 0.04f;
    44.    
    45.         // Spawn target
    46.         target.transform.localPosition = Target.position;
    47.    
    48.         // Spawn projectiles
    49.         for (int i = 0; i < 1900; i++)
    50.         {
    51.             yield return new WaitForSeconds(0.1f);
    52.             _spawnInNextUpdate = true;
    53.         }
    54.     }
    55.  
    56.     IEnumerator deleteProjectile(GameObject go)
    57.     {
    58.         yield return new WaitForSeconds(FlightDuration + 1f);
    59.         GameObject.Destroy(go);
    60.     }
    61.  
    62.  
    63.     bool _spawnInNextUpdate = false;
    64.     Rigidbody _rb;
    65.  
    66.     void FixedUpdate()
    67.     {
    68.         if (_spawnInNextUpdate)
    69.         {
    70.             _spawnInNextUpdate = false;
    71.             var rb = spawnProjectile(Source, Target, FlightDuration, Angle);
    72.             if (_rb == null)
    73.                 _rb = rb;
    74.             if(rb != null)
    75.                 StartCoroutine(deleteProjectile(rb.gameObject));
    76.         }
    77.     }
    78.  
    79.     protected bool useHighSolution = false;
    80.  
    81.     Rigidbody spawnProjectile(Transform source, Transform target, float time, float angle)
    82.     {
    83.         // By time
    84.         //var startVelocity = calcStartVelocityByTime(source, target, time);
    85.  
    86.         // By angle
    87.         /*
    88.         bool possible = calcStartVelocityByAngle(out var startVelocity, source, target, angle);
    89.         if (!possible)
    90.             return null;
    91.         */
    92.  
    93.         // By start speed
    94.         /*
    95.         useHighSolution = !useHighSolution;
    96.         bool possible = calcStartVelocityByStartSpeed(out var startVelocity, source, target, Speed, useHighSolution);
    97.         if (!possible)
    98.             return null;
    99.         */
    100.  
    101.         // By lowest energy needed
    102.         var startVelocity = calcLowestStartVelocity(source, target);
    103.  
    104.         var projectile = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    105.         projectile.name = "projectile";
    106.         projectile.GetComponent<MeshRenderer>().sharedMaterial = projectileMaterial;
    107.         projectile.transform.localScale = Vector3.one * 0.1f;
    108.         projectile.transform.localPosition = source.transform.position + calcStartPositionDelta(startVelocity); // New
    109.    
    110.         var collider = projectile.GetComponent<SphereCollider>();
    111.         collider.radius = 0.1f;
    112.    
    113.         var rb = projectile.AddComponent<Rigidbody>();
    114.         rb.AddForce(startVelocity, ForceMode.VelocityChange);
    115.  
    116.         return rb;
    117.     }
    118.  
    119.     Vector3 calcStartPositionDelta(Vector3 startVelocity)
    120.     {
    121.         // Start at the position at half a physics update.
    122.         float dT = Time.fixedDeltaTime * 0.5f;
    123.    
    124.         return new Vector3(
    125.             0f,
    126.             dT * startVelocity.y + Physics.gravity.y * dT * dT * 0.5f,
    127.             dT * startVelocity.z
    128.         );
    129.     }
    130.  
    131.     Vector3 calcStartVelocityByTime(Transform source, Transform target, float time)
    132.     {
    133.         var path = target.position - source.position;
    134.         var distanceXZ = new Vector3(path.x, 0f, path.z); // Project onto xz plane
    135.         float distance = distanceXZ.magnitude;
    136.         float altitude = path.y;
    137.    
    138.         // Solve in 2D (ZY plane)
    139.         //   Derived from equation for motion under constant acceleration.
    140.         //   var velocity = distanceVector / time - new Vector3(0, g, 0) * 0.5f * time;
    141.         float g = Physics.gravity.y;
    142.         var velocity = new Vector3(
    143.             0f
    144.             , altitude / time - g * time * 0.5f
    145.             , distance / time
    146.         );
    147.    
    148.         // Rotate back to 3D and return
    149.         float angleY = Vector3.SignedAngle(distanceXZ, Vector3.forward, Vector3.up);
    150.         return Quaternion.Euler(0f, -angleY, 0f) * velocity;
    151.     }
    152.  
    153.     public bool calcStartVelocityByAngle(out Vector3 velocity, Transform source, Transform target, float angle)
    154.     {
    155.         if (source == null || target == null || float.IsNaN(angle))
    156.             throw new Exception("Missing data source, target or angle.");
    157.  
    158.         var path = target.position - source.position;
    159.         var distanceXZ = new Vector3(path.x, 0f, path.z);  // Project onto xz plane
    160.         float distance = distanceXZ.magnitude;
    161.         float altitude = path.y;
    162.    
    163.         // Solve in 2D (ZY plane)
    164.         float g = Physics.gravity.y;
    165.         var cos = Mathf.Cos(angle * Mathf.Deg2Rad);
    166.         var value = distance * distance * -g /
    167.                     (distance * Mathf.Sin(2f * angle * Mathf.Deg2Rad) - 2 * altitude * cos * cos);
    168.         // If value < 0f then the target can not be reached with the given angle.
    169.         if (value < 0)
    170.         {
    171.             velocity = Vector3.zero;
    172.             return false;
    173.         }
    174.    
    175.         var speed = Mathf.Sqrt(value);
    176.  
    177.         // Rotate back to 3D and return
    178.         float angleY = Vector3.SignedAngle(distanceXZ, Vector3.forward, Vector3.up);
    179.         velocity = Quaternion.Euler(-angle, -angleY, 0f) * new Vector3(0f, 0f, speed);
    180.  
    181.         return true;
    182.     }
    183.  
    184.     /// <summary>
    185.     /// This has two possible solutions. One with a high altitude (useHighSolution = true) and one with a low altitude (useHighSolution = false).
    186.     /// </summary>
    187.     /// <param name="startAngle"></param>
    188.     /// <param name="source"></param>
    189.     /// <param name="target"></param>
    190.     /// <param name="speed"></param>
    191.     /// <param name="useHighSolution"></param>
    192.     /// <returns></returns>
    193.     /// <exception cref="Exception"></exception>
    194.     public bool calcStartVelocityByStartSpeed(out Vector3 startAngle, Transform source, Transform target, float speed, bool useHighSolution = false)
    195.     {
    196.         if (source == null || target == null || float.IsNaN(speed))
    197.             throw new Exception("Missing data source, target or speed.");
    198.  
    199.         var path = target.position - source.position;
    200.         var distanceXZ = new Vector3(path.x, 0f, path.z);  // Project onto xz plane
    201.         float distance = distanceXZ.magnitude;
    202.         float altitude = target.position.y - source.position.y;
    203.    
    204.         // Solve in 2D (ZY plane)
    205.         var sqrSpeed = Mathf.Pow(speed, 2);
    206.         var qadSpeed = Mathf.Pow(speed, 4);
    207.         var x = distance;
    208.         var y = path.y;
    209.         float g = Physics.gravity.y;
    210.         var sqrValue = qadSpeed - g * (g * x * x + 2 * -y * sqrSpeed); // Not sure why -y (wikipedia says +y but hey, it works).
    211.    
    212.         // If value < 0f then the target can not be reached with the given angle.
    213.         if (sqrValue < 0f)
    214.         {
    215.             startAngle = Vector3.zero;
    216.             return false;
    217.         }
    218.    
    219.         // Has two solutions, see: https://en.wikipedia.org/wiki/Projectile_motion
    220.         var angleA = Mathf.Atan((sqrSpeed + Mathf.Sqrt(sqrValue)) / (g * x)) * Mathf.Rad2Deg;
    221.         var angleB = Mathf.Atan((sqrSpeed - Mathf.Sqrt(sqrValue)) / (g * x)) * Mathf.Rad2Deg;
    222.    
    223.         // Rotate back to 3D and return
    224.         float angleY = Vector3.SignedAngle(distanceXZ, Vector3.forward, Vector3.up);
    225.         startAngle = Quaternion.Euler(useHighSolution ? angleA : angleB, -angleY, 0f) * new Vector3(0f, 0f, speed);
    226.         return true;
    227.     }
    228.  
    229.     /// <summary>
    230.     /// Find the angle with the lowest needed start speed and use that.
    231.     /// </summary>
    232.     /// <param name="velocity"></param>
    233.     /// <param name="source"></param>
    234.     /// <param name="target"></param>
    235.     /// <returns></returns>
    236.     Vector3 calcLowestStartVelocity(Transform source, Transform target)
    237.     {
    238.         // Wikipedia: The launch should be at the angle halfway between the target and Zenith (vector opposite to Gravity)
    239.         // See: https://en.wikipedia.org/wiki/Projectile_motion
    240.         var path = target.position - source.position;
    241.         var angle = Vector3.Angle(path, Physics.gravity) / 2f;
    242.         calcStartVelocityByAngle(out var velocity, source, target, angle);
    243.         return velocity;
    244.     }
    245. }
    246.  

    May it be useful to all who come after :)
     
    Last edited: Jun 20, 2023
    Unifikation likes this.
  10. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    530
    Game physiscs engines don't use the exact formulas, they incrementally update the solution with some timestep.

    I am slightly simplifying thing but roughly speaking every physics engine step does something like this.
    Code (CSharp):
    1. f(n) = gravity + other forces
    2. a(n) =  f(n)/m
    3. v(n)=v(n-1)  + a(n)* dt
    4. position(n) = position(n-1)+ v(n) * dt
    Assuming you had any calculus class, even if you were half a sleep, you probably remember drawings of approximatimg integral by drawing a bunch of rectangles. Physics engine is doing essentially the same, every update it draws a new rectangle to approximate the integral of motion equation. But that's that - an approximation. It doesn't produce the same result as exact formula even if you ignore floating point stuff. If you reduce the timestep (draw more thinner rectangles) result you get a better approximation which is closer to the exact formula, but in general case you don't get exact result.

    There is even whole sub-field/problem of math studying how to better approximate functions by incrementally calculating them. Read https://en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations for more details. Different techniques produce slightly different results, and some of them can produce more accurate approximations with less steps (potentially at the cost of each step being more complex). For example when calculating position(n) do you use v(n), v(n-1), (v(n)+v(n-1))/2 or something more complex.

    Not every math problem has formula for exact solution, so sometimes such incremental approaches are necessary to do any kind of calculations. Also for real world engineerring problems, objects often have complex shapes and many different forces interacting, making it difficult to apply the exact formulas directly. In such situation only solution is splitting things into smaller chunks and timesteps until you get a good enough approximation.

    Situation with game physics engines is slightly different. Simple motion equations have the formulas for exact solutions, but those are only usable in simple textbook situations where you know ahead of time everything that will happen. Games are typically interactive, so at any point of time player can potentially perform an action changing the conditions of simulation, other problem that in complex environments with many objects bouncing around you don't know what will collide with what and when. Unlike simple ball thrown in empty space where only that can change it's trajectory is hitting ground.

    A phyisics engine needs to choose a tradeoff (and corresponding methods) whether it's faster to do more simpler steps, or less but more complicated steps. The optimal point on real hardware may vary depending on microarchitectural details of specific cpu.

    Number of steps and compuation speed isn't only tradeoff, there is also accuracy and stability.

    For example if you simulate an object attached to a spring, it should keep oscilating with fixed amplitude, and if there is little bit of damping it after oscillating for a while should settle. Even if the behavior doesn't perfectly match with exact formula you would probably want your simulation to have similar behavior. But if the numerical method is chosen or implemented badly you may endup in situation, where with large simulation step instead of settling, integration error accumulates, and bigger the error faster the error keeps growing until physics simulation explodes.
    Here is a website https://myphysicslab.com/ https://myphysicslab.com/springs/single-spring-en.html for simple physics simulations which allows you to experiment with different timesteps and numerical methods . With the default settings you should see the spring settle. But if you increase the timestep and switch to Eulers method, you might observe that instead of settling the amplitude increases.



    For a specific situation if you fully expand and add up the formulas for an incremental approach, you will likely result with a formula that's very similar to the exact solution but has small additional term on the scale of single timestep. If you know which exact numerical method a physics engine is using, you can cancel out this factor which is what you are trying to do above with your offset (or you can randomly guess until it matches). But that only works under very specific conditions, and if the situation slightly changes (for example there is ceiling and ball bounces once, wind resistance get introduced) not only you would have to change the formula for exact solution, you would also likely need a different formula for the adjustment factor.

    It might not even be considered an API breaking change if physics engine update changed the numeric method it internally uses, but it would result in requiring a different adjustment factor to compensate between exact solution and the physics engine approximation. Although in practice a physics engine probably wouldn't change it carelessly since people tend to depend on behavior not changing even if it's result of nondocumented internal design decisions.
     
    Last edited: Jun 21, 2023
    _geo__ likes this.