CameraControl and ChaseCamera

Hi there :slight_smile:



I'm trying to port the ChaseCamera from jME2 to jME3.

Not sure if you noticed, but I recently ported CameraNode to jME3 and implemented a CameraControl.



I would now like to implement a ChaseCameraControl.

But I'm a bit stuck, because I don't quite understand the dependencies of ChaseCamera.



It seems a bit more complex now, than it seems first. The ThirdPersonMouseLook is really confusing me.



Maybe someone can clear things out?



Would be glad!

Because I think it sure would be great to have ChaseCamera working in jME3!



Cheers,

Tim

I made an adaptation of the flyby camera 1 month ago to behave like a ChaseCamera.



CameraNode didn't exists back then so the rotation of the camera around the target is handled by basic math calculation.



The camera rotates on a scalable hemisphere around the target (rotate with leftmouseclick or rightmouseclick)

You can zoomin/out with the mouse wheel.

If you zoom completely in, it turns in a first person camera and you can look around.



It implements a SpatialMovementListener to be notified when the target moves so it can update.

It could be a lot better, but it can be a good start for a real JME3 ChaseCamera implementation



/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.nehon.seventhsun.camera;

/**
 *
 * @author nehon
 */
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.binding.BindingListener;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;

/**
 * the chasecamera stays always behind its target.
 */
public class ChaseCam implements BindingListener, SpatialMovementListener {

    /**
     * the target.
     */
    private Spatial target = null;
    /**
     * distance in the Y-Axis to the target.
     */
    private float distance = 20;
    private float minHeight = 0.15f;
    private float maxHeight = FastMath.PI / 2;
    private float minDistance = 1.0f;
    private float maxDistance = 40.0f;
    private float zoomSpeed = 2.0f;
    private float rotationSpeed = 1.0f;
    /**
     * the camera.
     */
    private Camera cam = null;
    private InputManager inputManager;
    private Vector3f initialUpVec;
    private boolean canRotate;
    private float rotation = FastMath.PI / 2;
    private float vRotation = 0;

    /**
     * Attaches the ChaseCam with an Y- and Z-Axis offset to the target.
     * @param target target to follow
     * @param y distance to the target on the Y-Axis
     * @param z distance to the target on the Z-Axis
     */
    public ChaseCam(Camera cam, final Spatial target) {
        this.target = target;
        initialUpVec = cam.getUp().clone();

        this.cam = cam;
        updateCamera();

    }

    /**
     * Registers the FlyByCamera to recieve input events from the provided
     * Dispatcher.
     * @param dispacher
     */
    public void registerWithInput(InputManager inputManager) {
        this.inputManager = inputManager;

        inputManager.registerMouseAxisBinding("FLYCAM_Up", 1, false);
        inputManager.registerMouseAxisBinding("FLYCAM_Down", 1, true);

        inputManager.registerMouseAxisBinding("FLYCAM_ZoomIn", 2, true);
        inputManager.registerMouseAxisBinding("FLYCAM_ZoomOut", 2, false);


        inputManager.registerKeyBinding("FLYCAM_Key_Left", KeyInput.KEY_LEFT);
        inputManager.registerKeyBinding("FLYCAM_Key_Right", KeyInput.KEY_RIGHT);
        inputManager.registerKeyBinding("FLYCAM_Key_Up", KeyInput.KEY_UP);
        inputManager.registerKeyBinding("FLYCAM_Key_Down", KeyInput.KEY_DOWN);

        inputManager.addBindingListener(this);
    }

    private void rotateCamera(float value) {

        if (!canRotate) {
            return;
        }
        rotation += value * rotationSpeed;
        updateCamera();
    }

    private void zoomCamera(float value) {
        distance += value * zoomSpeed;
        if (distance > maxDistance) {
            distance = maxDistance;
        }
        if (distance < minDistance) {
            distance = minDistance;
        }
        if ((vRotation < minHeight) && (distance > (minDistance + 1.0f))) {
            vRotation = minHeight;
        }
        updateCamera();
    }

    private void vRotateCamera(float value) {
        if (!canRotate) {
            return;
        }
        vRotation += value * rotationSpeed;
        if (vRotation > maxHeight) {
            vRotation = maxHeight;
        }
        if ((vRotation < minHeight) && (distance > (minDistance + 1.0f))) {
            vRotation = minHeight;
        }
        updateCamera();
    }

    private void updateCamera() {

        float hDistance = distance * FastMath.sin((FastMath.PI / 2) - vRotation);
        Vector3f pos = new Vector3f(hDistance * FastMath.cos(rotation), distance * FastMath.sin(vRotation), hDistance * FastMath.sin(rotation));
        pos = pos.add(target.getLocalTranslation());
        cam.setLocation(pos);
        cam.lookAt(target.getLocalTranslation(), initialUpVec);

    }

    public void onBinding(String binding, float value) {
        if (binding.equals("mouseLeft")) {
            rotateCamera(-value);
        } else if (binding.equals("mouseRight")) {
            rotateCamera(value);
        } else if (binding.equals("leftClick")) {
            canRotate = true;

        } else if (binding.equals("rightClick")) {
            canRotate = true;

        } else if (binding.equals("FLYCAM_Up")) {
            vRotateCamera(value);
        } else if (binding.equals("FLYCAM_Down")) {
            vRotateCamera(-value);
        } else if (binding.equals("FLYCAM_ZoomIn")) {
            zoomCamera(value);
        } else if (binding.equals("FLYCAM_ZoomOut")) {
            zoomCamera(-value);
        }


    }

    public void onPreUpdate(float tpf) {
        canRotate = false;

    }

    public void onPostUpdate(float tpf) {
        inputManager.setCursorVisible(!canRotate);
    }

    public float getMaxDistance() {
        return maxDistance;
    }

    public void setMaxDistance(float maxDistance) {
        this.maxDistance = maxDistance;
    }

    public float getMinDistance() {
        return minDistance;
    }

    public void setMinDistance(float minDistance) {
        this.minDistance = minDistance;
    }

    public void onSpatialMove(Spatial spatial) {
        if (spatial == target) {
            updateCamera();
        }
    }
}


Some of the binding is made in the main app init, just bind mouseLeft,mouseRight,leftClick,rightClick

here is the code of the SpatialMovementListener


package org.nehon.seventhsun.camera;

import com.jme3.scene.Spatial;

/**
 *
 * @author nehon
 */
public interface SpatialMovementListener {


    public void onSpatialMove(Spatial spatial);
}




Hope that helps

Hey!



I have tried using your chasecamera class - however the camera flys upwards forever away from the object its meant to be chasing?!



Any ideas why?

After a bit of investigation it appears to be because the onSpatialMove method is never called.



I am obviously not passing in the correct object for the node I want to follow?



The onSpatialMove method is never called infact.

yeah i kinda forgot a detail.



to init the chase cam you need this settlement int the simpleInitApp method:


        flyCam.setEnabled(false);
        ChaseCam chaser=new ChaseCam(cam, model);
        chaser.registerWithInput(inputManager);
        character.addMovementListener(chaser);



"model" is the target of the cam (a Spatial or a Geometry)
Character is a Handler on the model (mainly inspired by ModelAnimHandler in jme3tools.preview)
basicaly it's a class holding the model,a list of SpatialMovementListener, and a move method that move the model and notify the listeners :


public class CharacterHandler{
     protected Spatial model;
     private List<SpatialMovementListener> movementListeners= new ArrayList<SpatialMovementListener>();


     public void move(float value) {
        notifyMove();
      //Move your spatial here
        

    }

    void notifyMove() {
        for (int i = 0; i < movementListeners.size(); i++) {
            movementListeners.get(i).onSpatialMove(model);
        }
    }

     public void addMovementListener(SpatialMovementListener listener) {      
        movementListeners.add(listener);
    }

    public Spatial getModel() {
        return model;
    }

    public void setModel(Spatial model) {
        this.model = model;
    }
}



this should work

Okay, thanks very much nehon.



I'll try to adapt this, so it can be used without explicit input and make it implement Control, so you can just call:

new ChaseCamera(spatial);



But your code will help :slight_smile:



Cheers,

Tim

@tim8dev.



How is it going with the ChaseCamera ?



I really would like to use this class too in jME3.  :)



Maybe you should update the IssueTracker with your progress, so everybody knows that you're busy with this.



Thanks in advance.

Oh, I'm a bit busy atm with my own project.



But, since it will need a ChaseCamera sooner or later, I'll be implementing it soon.



It won't be like the one in jME2, but more like the one nehon posted.



I don't think the IssueTracker is the right place for this, I'll post my progress here, for the moment.



Cheers,

Tim

Hi time8dev,

here is a new version of my ChaseCam working with the new input system.

I had a hard time debugging it, so just to spare you the trouble, here it is


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.nehon.seventhsun.camera;

/**
 *
 * @author nehon
 */
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Spatial;

/**
 * the chasecamera stays always behind its target.
 */
public class ChaseCam implements SpatialMovementListener {

    /**
     * the target.
     */
    private Spatial target = null;
    /**
     * distance in the Y-Axis to the target.
     */
    private float distance = 20;
    private float minHeight = 0.05f;
    private float maxHeight = FastMath.PI / 2;
    private float minDistance = 1.0f;
    private float maxDistance = 40.0f;
    private float zoomSpeed = 2.0f;
    private float rotationSpeed = 1.0f;
    /**
     * the camera.
     */
    private Camera cam = null;
    private InputManager inputManager;
    private Vector3f initialUpVec;
    private boolean canRotate;
    private float rotation = FastMath.PI / 2;
    private float vRotation = 0;

    /**
     * Attaches the ChaseCam with an Y- and Z-Axis offset to the target.
     * @param target target to follow
     * @param y distance to the target on the Y-Axis
     * @param z distance to the target on the Z-Axis
     */
    public ChaseCam(Camera cam, final Spatial target) {
        this.target = target;
        initialUpVec = cam.getUp().clone();

        this.cam = cam;
        updateCamera();

    }

    /**
     * Registers the FlyByCamera to recieve input events from the provided
     * Dispatcher.
     * @param dispacher
     */
    public void registerWithInput(InputManager inputManager) {
        String[] ActionInputs = {"toggleRotate"};
        String[] AnalogInputs = {"Down", "Up", "mouseLeft", "mouseRight", "ZoomIn", "ZoomOut"};

        this.inputManager = inputManager;

        inputManager.addMapping("Down", new MouseAxisTrigger(1, true));
        inputManager.addMapping("Up", new MouseAxisTrigger(1, false));

        inputManager.addMapping("ZoomIn", new MouseAxisTrigger(2, true));
        inputManager.addMapping("ZoomOut", new MouseAxisTrigger(2, false));
        inputManager.addMapping("mouseLeft", new MouseAxisTrigger(0, true));
        inputManager.addMapping("mouseRight", new MouseAxisTrigger(0, false));
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(0));
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(1));



        ActionListener al = new ActionListener() {

            public void onAction(String name, boolean keyPressed, float tpf) {

                if (name.equals("toggleRotate")) {
                    if (keyPressed) {
                        canRotate = true;
                    } else {
                        canRotate = false;
                    }

                }
            }
        };

        AnalogListener anl = new AnalogListener() {

            public void onAnalog(String name, float value, float tpf) {
                //    System.out.println(name);
                if (name.equals("mouseLeft")) {
                    rotateCamera(-value);
                } else if (name.equals("mouseRight")) {
                    rotateCamera(value);
                } else if (name.equals("Up")) {
                    vRotateCamera(value);
                } else if (name.equals("Down")) {
                    vRotateCamera(-value);
                } else if (name.equals("ZoomIn")) {
                    zoomCamera(value);
                } else if (name.equals("ZoomOut")) {
                    zoomCamera(-value);
                }
            }
        };
        inputManager.addListener(al, ActionInputs);
        inputManager.addListener(anl, AnalogInputs);
    }

    private void rotateCamera(float value) {

        if (!canRotate) {
            return;
        }
        rotation += value * rotationSpeed;
        updateCamera();
    }

    private void zoomCamera(float value) {
        target.setCullHint(Spatial.CullHint.Dynamic);
        target.setShadowMode(ShadowMode.CastAndRecieve);
        distance += value * zoomSpeed;
        if (distance > maxDistance) {
            distance = maxDistance;
        }
        if (distance < minDistance) {
            distance = minDistance;
        }
        if ((vRotation < minHeight) && (distance > (minDistance + 1.0f))) {
            vRotation = minHeight;

        }
        if (distance <= (minDistance + 1.0f)) {
            target.setCullHint(Spatial.CullHint.Always);
            target.setShadowMode(ShadowMode.Cast);
        }
        updateCamera();
    }

    private void vRotateCamera(float value) {
        if (!canRotate) {
            return;
        }
        vRotation += value * rotationSpeed;
        if (vRotation > maxHeight) {
            vRotation = maxHeight;
        }
        if ((vRotation < minHeight) && (distance > (minDistance + 1.0f))) {
            vRotation = minHeight;
        }
        updateCamera();
    }

    private void updateCamera() {

        float hDistance = distance * FastMath.sin((FastMath.PI / 2) - vRotation);
        Vector3f pos = new Vector3f(hDistance * FastMath.cos(rotation), distance * FastMath.sin(vRotation), hDistance * FastMath.sin(rotation));
        pos = pos.add(target.getLocalTranslation());
        cam.setLocation(pos);
        cam.lookAt(target.getLocalTranslation(), initialUpVec);

    }

    public void onPreUpdate(float tpf) {
        canRotate = false;

    }

    public void onPostUpdate(float tpf) {
        inputManager.setCursorVisible(!canRotate);
    }

    public float getMaxDistance() {
        return maxDistance;
    }

    public void setMaxDistance(float maxDistance) {
        this.maxDistance = maxDistance;
    }

    public float getMinDistance() {
        return minDistance;
    }

    public void setMinDistance(float minDistance) {
        this.minDistance = minDistance;
    }

    public void onSpatialMove(Spatial spatial) {
        if (spatial == target) {
            updateCamera();
        }
    }
}



Nehon

If you think this ChaseCamera is stable enough, maybe it should be included in jME3? I was actually planning on porting it myself, but you guys done a great job already :slight_smile:

It's stable but not really easy to set up, i don't think it's ready yet :


  • it needs a wrapper over the target that holds a list of MovementListeners
  • the wrapper must notify the camera when moving. I think there must be a way to make the cam "smart" enough to detect target's movements (ie the distance between them have changed…maybe)
  • there is a cross references initialization that's not really great.


    CharacterHandler character = new CharacterHandler(model);
    ChaseCam chaser=new ChaseCam(cam, model);
    character.addMovementListener(chaser);



- The camera just follow the target from the angle the user defined, a nice to have effect would be that the cam smoothly move in the trail of the target while it's moving...

I will look into it to make it more user friendly.
1 Like