How to implement projectile accuracy in 3D space? (3D vector rotation)

I am trying to calculate a deviated projectile direction from known direction, but I am a bit stuck.

Let’s take a look at simplified example.
Unit A is located at (0, 0, 0) and unit B is located at (2,1,2). Unit A shoots to unit B.
If A has perfect accuracy (deviation is 0), then projectile should follow along direction vector (2,1,2).
But I want to compute a deviated direction before shooting. Let’s assume that A actually shoots 10 degrees (Math.PI/18) to the right from target and 5 degrees (-Math.PI/36) above the target.
How to compute the new direction vector?
The same question, but in code

const initialDirectionVector = new BABYLON.Vector3(2, 1, 2);
const horizontalDeviation = Math.PI/18;
const verticalDeviation = -Math.PI/36;

//const deviatedVector = ???

Both A in B can be located anywhere in space, even at different altitude (Y coordinate), thus initialDirectionVector can be totally arbitrary.
I assume I need to use rotation quaternions, but I can’t figure out how to build one when the initial vector is totally random. I don’t know around which axis I should rotate a direction vector in this case. My math ain’t mathing.
The task would be easier if I could see from the vector perspective. In this case I could just add some values to x and y coordinates in LOCAL space or use 2D rotation formula for XZ plane and then for YZ planes. But how to switch from GLOBAL coordinates to LOCAL and then back when we are working with a single vector?

A Vector3 object by itself doesn’t have a local or world space, but a mesh does. Mesh.rotate() allows you to specify which space to use:


I’m sure there’s a way to specify this for a bare Vector3, probably by using the mesh’s rotationQuaternion for it’s local space and duplicating the Babylon source code in mesh.rotate().

I would use a (cached) simulation TransformNode. Before shoot, set TransformNode.position.copyFrom(sourceVec), call TransformNode.lookAt(targetVec), then use TransformNode.addRotation(…deviationAngles), so that TransformNode.forward is your new direction. (might need to call computeWorldMatrix(true) if numbers are off)

Like so:

This: (friendly console logs included).

If you compare initialDirectionVector and deviated vector everything makes sense. We turned right (slight X increase and slight Z decrease), and we turned up (slight Y increase).

We need to find two quaternions:
a) Quaternion to transform the Forward vector (0,0,1) to normalized initialDirectionVector.
b) Quaternion to transform the Forward vector (0,0,1) to LOCAL deviation vector.

Then the final rotation can be described by multiplication of quaternions a x b.

Finding deviation quaternion is easy, since we already know the deviation angles.
Finding the initial quaternion is a bit more challenging. I used school trigonometry knowledge to compute all angles. I wrote a helper function findQuaternionBetweenForwardAndGiven for that. (Note: we multiply the altitude by -1, because moving head up is considered as anti-clockwise rotation which is negative in BabylonJS).


  1. We already know initialDirectionVector, can we just apply the deviation quaternion to it? No, It will not work.
  2. Multiplication order is super important (!). For both main quaternions we multiply horizontal rotation quaternion by vertical rotation quaternion. Finally, we multiply initial quaternion by deviation quaternion. Think about the final rotation as composition of atomic rotations in one of two planes (XZ or YZ). Everything makes sense only in one possible order.

Final notes:

  1. Highly likely my math might be simplified.
  2. Highly likely there are native BabylonJS functions that can do the same with less code.
  3. I might be lucky with the initial direction that I picked, and maybe some other directions may lead to disaster. Not going to mark as solved yet, but from math point of view I don’t see flaws. Possibly it’s just not the most optimal way to do so.

It might not be the best solution, but at least it is a pure mathematical solution that I fully understand. Which is good for learning purposes.

I think I am overcomplicating a bit. Since we know all angles, we can just do [azimut+horizontaDeviation, altitude+verticalDeviation]. Then just do horizontalQuaternion.multiply(verticalQuaternion). Or maybe even issue just the one quaternion for both components. facepalm.
Be right back tomorrow or in couple of days.

Yep, I simplified my solution. Looks like BabylonJS does rotation in X,Y,Z order, which allowed me to use just one quaternion.

Now we have a simple and convenient function that accepts initial vector and deviation angles. Output value is even more precise if we re-compute the angles for the deviated vector.

But the initial idea is almost the same, just much faster now: find angles between forward and initial vector, then add deviation angles, then build the quaternion for new rotation angles. Finally apply the quaternion to forward vector to get the new vector.
Basically, what we do here is transition from Cartesian to Spherical coordinate system and then transition back to Cartesian system. I use a rotation quaternion to return back to Cartesian, but probably if I use trigonometry again like I did for the first transition, it may work even faster. Worth checking out, I guess.

@Joe_Kerr, your solution works as well, but it’s a bit hackier.

Cool. Full trigonometry approach without quaternions works about 1.7 times faster (all console logs should be disabled during perf tests). We also save memory here, because we don’t create unnecessary objects anymore.

The neatest solution so far:

We may see some discrepancies in results when using different approaches. It’s all related to how JS handles decimals when using different math operations (trigonometry, division, etc.). But if we compute angles for the new vector again and convert it to degrees, we can see, that practically discrepancies are less than 0.1 degree. With the given angle deviation 10 degrees (horizontally) + 5 degrees (vertically), it’s way less than 1%. Which is acceptable, I think.

Important note:
Keep in mind that output deviated vector with trigonometry approach is slightly longer than 1, because it’s hypotenuse above the unit vector that we just rotated in XZ field. For debugging purposes, I recommend normalizing both initial vector and deviated vector: see the commented console.log on line 47. Also, if you interpolate velocity for projectile, you MUST normalize your direction vector, otherwise you get different speed for different vector lengths.

I did testing and checked directions in all quadrants of Cartesian coordinate system.
Actually, I need to add corresponding signs to angles depending on which quadrant we are in. So, I made some adjustments to my code. Now it does it job properly, without memory or CPU time overhead as it could be with quaternions or cached TransformNode.

I streamed the testing process on YouTube in case if you can understand my language. (I live abroad 6 years already, but still use my native language on streams).

Final version:
Keep in mind, that function returns not normalized vector. For debugging purposes I recommend to normalize both input and output vector before compare. Also, if you are going to compute velocity from output vector, then output vector should be normalized first anyway.

1 Like