Convert Follow Camera From Unity Script To JME3

Hello Guys,
Looking at this Unity article:

Unity3D: Third-Person Cameras

I’m trying to create a JME Camera App State according to the Follow Camera presented in the article.
So, the original Unity code looks like this:

void LateUpdate() {
        float currentAngle = transform.eulerAngles.y;
        float desiredAngle = target.transform.eulerAngles.y;
        float angle = Mathf.LerpAngle(currentAngle, desiredAngle, Time.deltaTime * damping);
         
        Quaternion rotation = Quaternion.Euler(0, angle, 0);
        transform.position = target.transform.position - (rotation * offset);
         
        transform.LookAt(target.transform);
    }

Where transform is the camera object and target is the target object which the camera tries to follow.
I’m interested especially in:

  1. how to get the euler angles of the camera and the target
  2. how to lerp (I guess lrep==interpolate?) between angles
  3. this is not mandatory - can I control my app state to run last after all the other updates on the scene graph has finished their jobs. Note - I’m going to add & remove app states dynamically during the program execution.

Thanks!

2 Likes

Afaik, to get the angles between a coordinate vector(x or y or z) & other one (Euler Angles) use : transform.normalize().angleBetween(Vector3f.UNIT_Y); in Rads
Quaternion rotation = new Quaternion().fromAngles(0, interpolatedAngle, 0);

yes, lerp is Linear(ler) Interpolation(p), so Vector3f#interpolate(vec3f, vec3f, float)

I think may be using a static AtomicInteger(something similar to mutexes(mutual exclusive events) to be shared by all AppStates & keeps adding it when you execute every single BaseAppState & then when it reaches the appStates.length()-2 which is the last index to execute before your event, start executing your exclusive event or state, may be add that guard inside the update of this mutex state.

2 Likes

Thanks Pavl. I forgot to mention that I wrote this JME version of the above code:

cam.getRotation().getY();
float currentAngle = cam.getRotation().getY(); //transform.eulerAngles.y;
float desiredAngle = target.getWorldRotation().getY();//  target.transform.eulerAngles.y;
float angle = FastMath.interpolateLinear(tpf * damping,currentAngle,desiredAngle) ;// Mathf.LerpAngle(currentAngle, desiredAngle, Time.deltaTime * damping);

Quaternion rotation = new Quaternion().fromAngles(0,angle,0);//  Quaternion.Euler(0, angle, 0);
Vector3f position = target.getWorldTranslation().add(offset);

cam.setLocation(position.subtract(rotation.mult(offset))); //target.transform.position - (rotation * offset);
cam.lookAt(target.getWorldTranslation(), Vector3f.UNIT_Y);

But I feel I’m far from the correct translation especially this line:

float angle = FastMath.interpolateLinear(tpf * damping,currentAngle,desiredAngle) ;// Mathf.LerpAngle(currentAngle, desiredAngle, Time.deltaTime * damping);

I’ll try the normalize thing…

1 Like

Best way is to keep the euler angles yourself and construct the quaternion only when needed. That’s what Unity is doing.

quat.toAngles() can kind of give them to you but they may jump around because you cannot reliably get back euler angles from a quaternion… at least not the same ones you put in.

2 Likes

One thing I don’t understand and prevent me from using this camera script:
No matter what angle I’m setting in the “angle” variable. It kind of ignoring it so in this code:

float angle = 3.14f/45f;

Quaternion rotation = new Quaternion().fromAngles(0,angle,0);
//cam.setLocation(target.getWorldTranslation().subtract(offset)); 
cam.setLocation(target.getWorldTranslation().subtract(rotation.mult(offset))); 
cam.lookAt(target.getWorldTranslation(), Vector3f.UNIT_Y);

the line:

cam.setLocation(target.getWorldTranslation().subtract(rotation.mult(offset))); 

behaves the same as the line:

cam.setLocation(target.getWorldTranslation().subtract(offset));

And I don’t understand why. I would expect that the first version will maintain a specific angle related to the target location and it’s not happening.
I’m missing something with the vector’s math…

Add println or logging for the rotation and the offset. For example if offset is 0,0,0 then it won’t matter how much you rotate it.

We get a very narrow view of your code so we can’t tell if there is a mistake somewhere else… and anyway, if this were my program step 1 would be confirming all assumptions by logging.

1 Like

Thanks! I’ll post the entire class and a demonstration video. I just wanted to understand if my my assumption was right - given that the offset is not 0,0,0 , when using that math, should I expect the camera to follow the object in a constant angle?

This looks like you’re trying to get a 45 degree angle (?), but it’s actually only 4° which may be barely visible.
I’d use angle = 45 * FastMath.DEG_TO_RAD; to make it easier to understand and less bug prone.

1 Like

Thanks a lot! I’ll change that. No matter what angle I’m using (even if its 4 deg) My issue with this code is that the camera is not tracking the object in that constant angle. I’ll post a video sample soon.

Log this:

And this:

And this:

And this:

And this:

So it randomly looks all around? Or it is not tracking at the constant angle you expect it to?

Edit: and just in case, because your math was weird up there… but angles to radians is * PI / 180.
Or just multiply by FastMath.DEG_TO_RAD

1 Like

Not randomly. The offset seems right but whenever the object is rotating I expected the camera should rotate as well to maintain the same point of view and this doesn’t happen. I’ll post a demo video.

If your angle is constant then the offset is effectively constant… it won’t matter what direction the camera is facing unless you also include the camera rotation.

Here is your fundamental misunderstanding:

Quaternions are not euler angles. Quaternions are 4-dimensional black boxes. So the above line is effectively nonsense.

1 Like

Here is a sample video showing the problem. The angle is fixed on 45 degrees (45*FastMath.DEG_TO_RAD)
As you can see, the car is rotating according to the track’s path but the camera doesn’t follow the rotation of the car.

Here the entire code:

package com.scenemaxeng.projector;

import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.scenemaxeng.compiler.FpsCameraCommand;

public class FollowCameraAppState extends BaseAppState {

    private final SceneMaxThread thread;
    private final FpsCameraCommand cmd;
    private SceneMaxApp app;
    private Camera cam;

    public Spatial target;
    public Vector3f offset;

    public float damping = 1;

    public FollowCameraAppState(SceneMaxApp app, SceneMaxThread thread, FpsCameraCommand cmd, Spatial target) {
        this.target=target;
        this.thread=thread;
        this.cmd=cmd;
        this.app=app;
        this.cam = app.getCamera();
        this.offset = new Vector3f(0,1,-5);
        //this.offset=offset;
    }

    @Override
    public void update(float tpf) {

        Double offsetX=0d, offsetY = 0d, offsetZ=0d;
        if(cmd.offsetYExpr!=null) {
            offsetY = (Double)new ActionLogicalExpression(cmd.offsetYExpr,this.thread).evaluate();
        }

        if(cmd.offsetXExpr!=null) {
            offsetX = (Double)new ActionLogicalExpression(cmd.offsetXExpr,this.thread).evaluate();
        }

        if(cmd.offsetZExpr!=null) {
            offsetZ = (Double)new ActionLogicalExpression(cmd.offsetZExpr,this.thread).evaluate();
        }

        offset.set(offsetX.floatValue(),offsetY.floatValue(),offsetZ.floatValue());

        if(cmd.dampingExpr!=null) {
            damping = ((Double)new ActionLogicalExpression(cmd.dampingExpr,this.thread).evaluate()).floatValue();
        }

        float angle = 45*FastMath.DEG_TO_RAD;
        
        Quaternion rotation = new Quaternion().fromAngles(0,angle,0);

        //cam.setLocation(target.getWorldTranslation().subtract(offset)); //target.transform.position - (rotation * offset);
        cam.setLocation(target.getWorldTranslation().subtract(rotation.mult(offset)));
        cam.lookAt(target.getWorldTranslation(), Vector3f.UNIT_Y);


    }

    @Override
    protected void initialize(Application app) {

        Double offsetX=null, offsetY = null, offsetZ=null;
        if(cmd.offsetYExpr!=null) {
            offsetY = (Double)new ActionLogicalExpression(cmd.offsetYExpr,this.thread).evaluate();
        }

        if(cmd.offsetXExpr!=null) {
            offsetX = (Double)new ActionLogicalExpression(cmd.offsetXExpr,this.thread).evaluate();
        }

        if(cmd.offsetZExpr!=null) {
            offsetZ = (Double)new ActionLogicalExpression(cmd.offsetZExpr,this.thread).evaluate();
        }

        Vector3f desiredPosition = target.getWorldTranslation();
        if(offsetZ!=null) {
            Vector3f forward = target.getWorldRotation().mult(Vector3f.UNIT_Z);
            desiredPosition = desiredPosition.add(forward.mult(offsetZ.floatValue()));
        }

        if(offsetY!=null) {
            Vector3f vert = target.getWorldRotation().mult(Vector3f.UNIT_Y);
            desiredPosition = desiredPosition.add(vert.mult(offsetY.floatValue()));
        }

        if(offsetX!=null) {
            Vector3f horz = target.getWorldRotation().mult(Vector3f.UNIT_X);
            desiredPosition = desiredPosition.add(horz.mult(offsetX.floatValue()));
        }

        cam.setLocation(desiredPosition);

    }

    @Override
    protected void cleanup(Application app) {

    }

    @Override
    protected void onEnable() {

    }

    @Override
    protected void onDisable() {

    }
}

OK, maybe this is the root cause for my issue. Ill change that to get the Euler angles using toAngles(). something like that:

float[] angles = new float[3];
cam.getRotation().toAngles(angles);

Let me try to explain what your code is doing:
Create an angle rotation.
Rotate an offset by that rotation.
Get the world space translation of the target and add the WORLD SPACE rotated offset… irrespective of object or camera rotation.
Set the camera location to that.
Have the camera look at the object.

If you want the camera rotated relative to the object then you will have to include the object’s rotation in your calculation.

The original code probably got the object’s rotation for the angle.

1 Like

Here is the fixed version of the code. It includes a damping factor as well. Now the camera tracks the object as expected.

        float[] camAngles = new float[3];
        cam.getRotation().toAngles(camAngles);

        float[] targetAngles = new float[3];
        target.getWorldRotation().toAngles(targetAngles);

        float angle = FastMath.interpolateLinear(tpf*damping,camAngles[1],targetAngles[1]);

        Quaternion rotation = new Quaternion().fromAngles(0,angle,0);
        cam.setLocation(target.getWorldTranslation().subtract(rotation.mult(offset)));
        cam.lookAt(target.getWorldTranslation(), Vector3f.UNIT_Y);

3 Likes