Search code examples
matrixrotationjoml

Oriented projectiles keep facing camera


I'm trying to render a 2d image that represent a projectile in a 3d world and i have difficulty to make the projectile face the camera without changing its direction. Im using JOML math library.

enter image description here

my working code to orient the projectile in his direction

public Quaternionf findRotation(Vector3f objectRay, Vector3f targetRay) {

    Vector3f oppositeVector = new Vector3f(-objectRay.x, -objectRay.y, -objectRay.z);
    
    // cas vecteur opposé
    if(oppositeVector.x == targetRay.x && oppositeVector.y == targetRay.y && oppositeVector.z == targetRay.z) {
        AxisAngle4f axis = new AxisAngle4f((float) Math.toRadians(180), 0, 0, 1);
        Quaternionf result = new Quaternionf(axis);
        return result;
    }
    
    
    objectRay = objectRay.normalize();
    targetRay = targetRay.normalize();

    double angleDif = Math.acos(new Vector3f(targetRay).dot(objectRay));
    if (angleDif!=0) {
        Vector3f orthoRay = new Vector3f(objectRay).cross(targetRay);
        orthoRay = orthoRay.normalize();
        
        AxisAngle4f deltaQ = new AxisAngle4f((float) angleDif, orthoRay.x, orthoRay.y, orthoRay.z);
        Quaternionf result = new Quaternionf(deltaQ);
        
        return result.normalize();
    }
    return new Quaternionf();
}

Now i want to add a vector3f cameraPosition parameter to rotate the projectile only on its x axis to face the camera but i dont know how to do it.

For example with this code the projectile correctly rotate around his x axis but not face the camera so i want to know how to find the correct angle.

this.lasers[i].getModel().rotate((float) Math.toRadians(5), 1, 0, 0);

I tried this to rotate around axis X with transforming vector before compute angle.

this.lasers[i] = new VisualEffect(this.position, new Vector3f(1,1,1), color, new Vector2f(0,0.33f));
this.lasers[i].setModel(new Matrix4f().scale(this.lasers[i].getScale()));
this.lasers[i].getModel().rotate(rotation);
this.lasers[i].getModel().translateLocal(this.lasers[i].getPosition());

Vector3f vec = new Vector3f(cameraPosition).sub(this.position);

Vector4f vecSpaceModel = this.lasers[i].getModel().transform(new Vector4f(vec, 1.0f));
Vector4f normalSpaceModel = this.lasers[i].getModel().transform(new Vector4f(normal, 1.0f));


float angleX = new Vector2f(vecSpaceModel.y, vecSpaceModel.z).angle(new Vector2f(normalSpaceModel.y, normalSpaceModel.z));


this.lasers[i].getModel().rotate(angleX, 1, 0, 0);

enter image description here


Solution

  • Since you are using JOML, you can massively simplify your whole setup.

    Let's assume that:

    • projectilePosition is the position of the projectile,
    • targetPosition is the position the projectile is flying at/towards, and
    • cameraPosition is the position of the "camera" (which we ultimately want the projectile to face)

    We will also assume that the local coordinate system of the projectile is such that its +X axis points along the projectile's path (like how you depicted it) and the +Z axis points away from the projectile towards the viewer when the viewer is "facing" the projectile. So, the projectile itself is defined as a quad on the XY plane within its own local coordinate system.

    What we must do now is create a basis transformation that will effectively transform the projectile such that its X axis points towards the "target" and its Z axis points "as best as we can" towards the camera.

    This is very reminiscent of what we know as the "lookAt" transformation in OpenGL. And in fact, we are just going to use that. However, since the common "lookAt" is the inverse of what we wanted to do, we will also just invert it.

    So, all in all, your complete model matrix/transformation for a single projectile will look like this (in JOML):

    Vector3f projectilePosition = ...;
    Vector3f cameraPosition = ...;
    Vector3f targetPosition = ...;
    Vector3f projectileToCamera = new Vector3f(cameraPosition).sub(projectilePosition);
    modelMatrix
      .setLookAt(projectilePosition, targetPosition, projectileToCamera)
      .invert()
      .rotateXYZ((float) Math.toRadians(-90), 0, (float) Math.toRadians(90));
    

    In case you do not want to use lookAt and invert, you can also do:

    Vector3f projectileToTarget = new Vector3f(targetPosition).sub(projectilePosition);
    modelMatrix
      .translation(projectilePosition)
      .rotateTowards(projectileToTarget, projectileToCamera)
      .rotateXYZ((float) Math.toRadians(-90), 0, (float) Math.toRadians(-90));
    

    yielding the same result as the above code.

    Note that nowhere do we actually need angles or trigonometric functions. This is very common when you already have all positions/directions given as vectors, you can simply use linear algebra without converting from/to angles.

    The last part with the rotateXYZ(90°, 0°, 90°) is to express that we do not want the -Z axis of the projectile to point towards the target (which is what lookAt will do by default), but we want the X axis to point to the target.

    Yet another way is to realize that what we do here is also known as a "cylindrical" or "axial" billboard, and can also be expressed like so:

    Vector3f projectileToTarget = new Vector3f(targetPosition).sub(quadPosition).normalize();
    modelMatrix
      .billboardCylindrical(projectilePosition, cameraPosition, projectileToTarget)
      .rotateZ((float) Math.toRadians(90));
    

    (Note that in this case projectileToTarget needs to be unit!)

    A test with a simple scene containing 24 projectiles all targeting "the center" with the camera hovering over them will look like this:

    enter image description here

    The corresponding simple LWJGL 3 / JOML demo generating this image.