Draw 3D arc in Gizmos

I know how to draw an arc… However, I’d like to draw a 3D arc based on start-point, endpoint, and angle.

My problem is that the start and end are not necessarily on the same X/Z values, meaning the arc might just go through 3 axis and not just 2.

a good example of what I’m looking for is a grenade projectile path.
You can throw the grenade diagonally and the arc will spread both on the X and Z axis (and obviously go up and down in a parabolic way on the Y axis)

I tried looking over the internet for an arc formula in a 3D space however I did not find answers.

Appreciating your comments :smile:

I can help you with that.
Every arc is a part of a circle. And circle is a rotated point.
Can you define the axis of that rotation?

This is the approach I was taking, but I encountered the same problem, I don’t know how to define a circle in a 3D space, not one that involves all 3 axis

Ok I’ll assume you need a general solution, so here’s one

static public void DrawArc(Vector3 center, Vector3 point, Vector3 axis, float revFactor1 = 0f, float revFactor2 = 1f, int segments = 48, Color color = default) {
  segments = Mathf.Max(1, segments);

  var rad1 = revFactor1 * 2f * Mathf.PI;
  var rad2 = revFactor2 * 2f * Mathf.PI;
  var delta = rad2 - rad1;

  var fsegs = (float)segments;
  var inv_fsegs = 1f / fsegs;

  var vdiff = point - center;
  var length = vdiff.magnitude;
  vdiff.Normalize();

  var prevPoint = point;
  var nextPoint = Vector3.zero;

  if(Mathf.Abs(rad1) >= 1E-6f) prevPoint = pivotAround(center, axis, vdiff, length, rad1);

  var oldColor = Gizmos.color;
  if(color != default) Gizmos.color = color;

  for(var seg = 1f; seg <= fsegs; seg++) {
    nextPoint = pivotAround(center, axis, vdiff, length, rad1 + delta * seg * inv_fsegs);
    Gizmos.DrawLine(prevPoint, nextPoint);
    prevPoint = nextPoint;
  }

  Gizmos.color = oldColor;
}

static Vector3 pivotAround(Vector3 center, Vector3 axis, Vector3 dir, float radius, float radians)
  => center + radius * (Quaternion.AngleAxis(radians * Mathf.Rad2Deg, axis) * dir);

center is the center of the circle (a point)
point is some arbitrary point in space that’s rotated around the center
axis is a direction (a unit vector) that describes the inclination of the circle (axis of rotation)
revFactor1/2 are start / end angles in full circle revolutions (0f = 0 degrees; 1f = 360 degrees)
segments is the amount of segments, short lines which constitute the arc
color is gizmo line color

this was adapted from my own codebase, where I use various extensions to make the code more readable etc, maybe I’ve rewritten something badly, so if it doesn’t work, tell me, I’ll fix it. I can also explain the inner workings if you’re interested.

call this from within OnDrawGizmos to work

1 Like
Vector3 startPos = Vector3.zero;
Vector3 startSpeed = new Vector3(100, 100, 100);
float gravity = -9.8f;

public Vector3 GetCurrentPos(float totalTime)
{
    float x = startPos.x + startSpeed.x * totalTime;
    float z = startPos.z + startSpeed.z * totalTime;
    float y = startPos.y + startSpeed.y * totalTime + 0.5f * gravity * totalTime * totalTime;
    return new Vector3(x, y, z);
}

A parabola is not the same as a circle, you can divide the parabola into many straight lines to draw

1 Like

A circle or parabola, depending on what you need exactly, can be defined in 3D in several ways, but it always boils down to one basic thing: all points are contained within a plane.

To define a plane in 3D, you need its orientation in space, consider this image
6457105--723808--upload_2020-10-26_9-32-18.png

This is called a surface normal, and it has a length of 1
Once you have this orientation (represented by a vector that is also known as a direction vector), then you need another point which describes the offset at which the plane must lie so that it contains this point. Imagine like a building with floors, all of them have the same orientation, but are parallel to each other, so this point is different to each one of them.

In the case of a building, its base plane would have orientation of Vector3.up and would contain point Vector3.zero.
The floor above it would have orientation of Vector3.up and would contain point (0, 1, 0) and so on.

Now, if you want a circle on this plane, this surface normal is your axis of rotation. Imagine if that point (x,y,z) on the image was used to revolve around the point (x0,y0,z0) on this plane. This is how you define a circle in 3D.

Same goes for parabola or any other 2D shape.

To arbitrarily rotate points that you know how to compute for XY plane (thus in 2D), so that they align with some plane in 3D space, you need a quaternion to describe this rotation.

The easiest way to compute this quaternion is to take two surface normals, one that belongs to your original 2D space (Vector3.back, an arrow that points to you), and the other is your plane’s surface normal. Quaternion.FromToRotation will give you the result. Apply the rotation to your point like so:

var rotation = Quaternion.FromToRotation(Vector3.back, Vector3.up);
var point3d = rotation * (Vector3)point2d; // here you have to make sure that (x,y) -> (x,y,0)

The other way to compute the quaternion is by using Quaternion.AngleAxis, which considers the angle of rotation in degrees, and the axis of rotation. This is useful if you don’t know the plane, but you have a set angle instead. For example you want to rotate your 2D circle 45 degrees on the X axis, to get the 3D one.

var rotation = Quaternion.AngleAxis(45, Vector3.right);
var point3d = rotation * (Vector3)point2d;

Do not attempt to modify quaternions by hand, they use complex numbers and have nothing to do with angles.

Here a simple way to adapt NweTau’s code above to work in 3D, by rotating this XY plane freely so that the ground level is on the XZ plane.

public Vector3 Rotate2DPlot(Vector2 point, float rotation) // rotation in degrees
  => Quaternion.AngleAxis(rotation, Vector3.up) * (Vector3)point;

for a 2D variant of a parabolic ballistic path

// terminalVelocity in units/second
// angle in degrees
// time in seconds
public Vector2 ComputeBallisticPoint(float terminalVelocity, float angle, float time) {
  var vec = PolarVector(angle) * terminalVelocity * time;
  var grav = new Vector2(0f, GRAVITY_ACC * time * time);
  return vec + grav;
}

public Vector2 PolarVector(float angle) { // angle in degrees
  var rads = angle * Mathf.Deg2Rad;
  return new Vector2(Mathf.Cos(rads), Mathf.Sin(rads));
}

where

const float GRAVITY_ACC = -9.81f;

I decided instead to go for a parabola, as it serves my purpose better.
I made a Parabola class that receives 3 points (Vector3) and builds a Parabola that goes through those 3 points, achieving the parameters A, B, C, and a Vector3 Plane.

I use Vector3.ProjectToPlane to project my arc onto the plane, however. It only works well when the arc is going on the X axis, the more you move it on the Z axis the less accurate it gets until it loses accuracy altogether. I have a feeling I know why however I have no idea how to fix it… so if anyone has any ideas please let me know

The problem I suspect is that I only use the X and Y values to determine a 2D parabola and then attempt to project it onto the Z axis using the plane, as this is the only way to draw a parabola in a 3D space (as far as I know).

Here is the code:
(Image)

By now I’ve already figured out what to do.
I gave prioritization only to the X axis in the GetPoint function, as I return a Vector2 with a value of 0 in the Z.
So I realized I have to divide the prioritization of the value between the X and Z axis depending on the angle between the start and end point.

so I calculated this angle using Mathf.Atan2 and the angle is correct (checked with debugging)
Then I used Cos and Sin with the angle multiplied by the value in order to prioritize it right.
However, while the arc is correct sometimes, at some angles it glitches to the other side, as if for 180 deg the other direction.

I can’t understand why that happens, it shouldn’t, the calculations are correct…

Here’s the calculation

public Vector3 GetPoint(float x)
        {
            float angle = Mathf.Atan2(pc.z - pa.z, pc.x - pa.x) * Mathf.Rad2Deg;
            return new Vector3(Mathf.Cos(angle) * x, a * Mathf.Pow(x, 2) + b * x + c, Mathf.Sin(angle) * x);
        }

@RoeeHerzovich Have you seen my last post?

I have, the reason it won’t work however is very simple, You are calculating a 2D only on 2 axis(as far as I can see from the code, perhaps I’m wrong). I intend to draw a Parabola on all 3 axis meaning I need to project it onto a properly rotated plane, which is why your setup won’t work.
But I truly appreciate your will to help :slight_smile:

Yes, but I’ve offered the first method to let you arbitrarily rotate this plane to your 3D space. That was the key takeaway.

Of course, you can bundle all of this together, I just thought it was more readable this way.
If you want to deform parabola in 3D space (say include lateral motion such as wind), then go with NweTau’s original code. I just thought you maybe prefer to start from 2D and work your way to 3D.

Oh, and don’t use Math.Pow(anything, 2)
It’s around 10-12x slower than just doing anything * anything, consistently across all platforms.

public Vector3 Rotate2DPlot(Vector2 point, float rotation) // rotation in degrees
  => Quaternion.AngleAxis(rotation, Vector3.up) * (Vector3)point;

If you’re making tanks on XZ plane, as an example, you use Math.Atan2 if you want to find this angle of rotation. Normally, I expect this rotation is something you have control of, literally from player input. But if not, then

var difference = enemyPos - playerPos;
var rotationInDegrees = Math.Atan2(difference.z, difference.x) * Mathf.Rad2Deg;

If you bundle all of this together

public Vector3 ComputeBallisticPointBetween(Transform player, Transform enemy, float terminalVelocity, float gunInclination, float time) {
  var diff = enemy.position - player.position;
  var rotation = Math.Atan2(diff.z, diff.x) * Mathf.Rad2Deg;
  var projectile = PolarVector(gunInclination) * terminalVelocity * time;
  var gravity = new Vector2(0f, GRAVITY_ACC * time * time);
  return Quaternion.AngleAxis(rotation, Vector3.up) * (Vector3)(projectile + gravity);
}

Then you iterate through all 3D points by incrementing time. (or you simply reposition the projectile in each frame, where time is += Time.deltaTime)

(Obviously, this is just an example, perhaps a poor one, if you change positions while the projectile is moving, the whole thing will change accordingly.)

1 Like

Note that Quaternion.AngleAxis(rotation, Vector3.up) is effectively the same thing as PolarVector (only in another plane of reference), so this can be simplified massively. I’d just have to implement this to make sure it behaves correctly, something I have no time to do right now.

1 Like

Eventually, I figured out a different solution.

Before messing with parabolas on 3D I did the same with Lines, and unlike Parabolas, lines do have a vectorial equation. I made a line equation with an addition with a max height (for apex point).
Then I calculated the apex point using that height, calculated the A B and C values of a parabola that would contain those 3 points, calculated the trajectory as for a linear equations, only changing the Y value based on the parabola’s
It created a perfect parabola that is defined by a start point, an end point an apex height

Btw, here’s the code(images)

FIX:
in the Linear’s GetX(float x) is should be:

public Vector3 GetX(float x) => r0 + (x - r0.x)/v.x * v;

I found I had an error with the point calculation and I quickly fixed it, now it works perfectly.

Thank you all for helping :slight_smile: