2

I wrote this function in Ruby to find the target angle between two 2D (x,y) vectors, but now I want to find out how to do this in 3D in a similar way:

def target_angle(point1, point2)
    x1 = point1[0]
    y1 = point1[1]
    x2 = point2[0]
    y2 = point2[1]
    delta_x = x2 - x1
    delta_y = y2 - y1
    return Math.atan2(delta_y, delta_x)
  end

Given an object (like a bullet in this case), I can shoot the object given a target_angle between the player (x,y) and the mouse (x,y), as such in the bullet update function:

  def update
    wall_collision
    # the angle here is the target angle where point 1 is the player and
    # point 2 is the mouse
    @x += Math.cos(angle)*speed
    @y += Math.sin(angle)*speed
  end

Is there a similar method to calculate a target angle in 3D and use that angle in a similar manner as my update function (to shoot a bullet in 3D)? How can I make this work for two 3D vectors (x, y, z), where you have the player position (x,y,z) and some other arbitrary 3d point away from the player.

karamazovbros
  • 950
  • 1
  • 11
  • 40

2 Answers2

0

I recently published a vector/matrix gem for just such things, though it is written in C, I will attempt to translate it to pure Ruby.

There are actually a few different ways you can calculate an angle between two vectors in 3D space. Using the typical acos function is common, but has large precision issues when the slope is near +/- 1.0, so it best to compute using angle bisectors. Even light precision errors with the cross-product cause even larger errors with the angle using acos, so after some research, I found this method is always consistent.

Given 2 vectors defined as v1 and v2:

# Normalize vectors
inv = 1.0 / Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)
n1 = Vector.new(v1.x * inv, v1.y * inv, v1.z * inv)

inv = 1.0 / Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z)
n2 = Vector.new(v2.x * inv, v2.y * inv, v2.z * inv)

ratio = n1.x * n2.x + n1.y * n2.y + n1.z * n2.z


if ratio < 0.0
  x = -n1.x - n2.x
  y = -n1.y - n2.y
  z = -n1.z - n2.z
  length = Math.sqrt(x * x + y * y + z * z)
  theta = Math::PI - 2.0 * Math.asin(length / 2.0)
else
  x = n1.x - n2.x
  y = n1.y - n2.y
  z = n1.z - n2.z
  length = Math.sqrt(x * x + y * y + z * z)
  theta = 2.0 * asin(length / 2.0)
end

# Convert from radians to degrees
angle = theta * (180.0 / Math::PI)

I didn't run/test this code, and I am unsure what exact vector implementation you are using, I am just assuming an object with x, y, and z values for the point of illustration. There could be a few minor enhancements to be made, such as multiplying by 0.5 instead of dividing by 2.0, as division is slower, but the basic premise should hopefully help.

I converted this code from a C project, so if you are interested in other vector functions, see it here (shameless self-advertising).

ForeverZer0
  • 2,379
  • 1
  • 24
  • 32
  • That's awesome you published a gem! Once you have the angle, I also have an the update function, is there a way to increment the z position of the Bullet towards the point, in the same manner as x and y? Or is there another method do this? – karamazovbros Aug 16 '18 at 21:22
  • Since a bullet travels in a straight line, you could simply calculate once, and from that you can compute its trajectory, as it won't (shouldn't?) be changing. Any change, such as player movement, etc. is going to be handled by your world matrix. – ForeverZer0 Aug 16 '18 at 21:27
  • Yes, the angle is only calculated once when a bullet is spawned. What I mean is how do you make a bullet move towards the point using the bullets x,y, z position? By that, I mean is there a similar way to this like in my update function above, but with z as well? – karamazovbros Aug 16 '18 at 21:30
  • 1
    Something like `m = Math.sqrt(x1 * x2 + y1 * y2 + z1 * z2)` computes distance, with which you can use (on normalized vectors) to alter position/speed. These need computed with your world/model/projection matrices, as it they also depend on player speed/direction, camera, etc. – ForeverZer0 Aug 16 '18 at 21:39
  • If your "gem" contains if statements, then you are doing it wrong. When the dot product (what you call ratio) is negative the angle is more than 90° and the atan2() function is perfectly adept at handling such cases. Also, the division is superfluous as it cancels out. It is just there to slow your code down. – John Alexiou Aug 17 '18 at 02:15
  • Benchmarking says otherwise (in C), as well as being consistently accurate. Do you truly believe that a single `if` statement containing a `<` is a bottleneck in my "gem"? (We use that term loosely, I obviously am not enlightened). – ForeverZer0 Aug 17 '18 at 02:27
  • Never said the `<` was a bottleneck. The problem is that the `if` statement isn't needed and it has the possibility of inconsistent results between the two branches. What if the ratio is `1e-15` or `-1e15` which are close to zero. It is not a robust solution to change the logic based on a floating point evaluation _unless you have to_. Also, division takes several CPU cycles compared to multiplication or addition, and when not needed it should be removed. It also introduces further rouding errors. This might as well be a performance critical procedure (gaming) and every tick counts. – John Alexiou Aug 17 '18 at 20:21
  • @ForeverZer0 - your method wold fail for a near zero vector. The method I posted can handle such cases as `atan2()` is robust enough for fringe cases such as that. – John Alexiou Aug 17 '18 at 20:28
  • @ja72 I sure am happy opinion does not change reality or reverse the results of actual tests. Some of the large-scale companies, such as Microsoft that utilize this exact method would need to get a hold of hold you so you could set them straight on how to write software the "correct" way. :-) I am happy with my code, you can be happy with your "code". We all win. – ForeverZer0 Aug 17 '18 at 20:40
  • @ForeverZer0 - like Microsoft has never failed because of a bug in an `if` statement (see [Zune Bug](https://techcrunch.com/2008/12/31/zune-bug-explained-in-detail/)). My point is that the `if` statement is superfluous and can cause inconsistencies on edge cases. I did not see the test, so I cannot comment on how well they cover all possible cases. – John Alexiou Aug 18 '18 at 22:42
0

[pseudo code]

The dot product of two vectors relates to the cosine of the angle between them.

COS(angle) = dot(a,b)/( |a|*|b| ) 

The cross product of two vectors relates to the sine of the angle between them.

SIN(angle) = | cross(a,b) |/( |a|*|b| )

So the tangent is just the ratio of sine to cosine (the denominators cancel each other out).

angle = atan2( magnitude(cross(a,b)), dot(a.b) )  % returns angle in radians

Note the convention for atan2(Δy,Δx).

Finally, define the following functions

magnitude(c) = sqrt(c.x^2+c.y^2+c.z^2)
dot(a,b) = a.x*b.x + a.y*b.y + a.z*b.z
cross(a,b) = [ a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a>x*b.y - a.y*b.x ]
John Alexiou
  • 28,472
  • 11
  • 77
  • 133