Camera like in Windows 3D Builder

I want the same behavior like in each simple 3D Viewer, where the model is in the middle of the scene and I can zoom and turnaroud it. The zoom works via the mouse wheel, the ratation via mouse left and translation via mouse right button.
I’ve implemented my own camera extened from ChaseCamera but due to the rotation node the behavoir isn’t the same.
If I move the rotation center(by mouse right click) and next try to turn the model of course it doesn’t work correctly because the center has moved. What I want is to move the camera around the model and not moving the rotation center itself.
I hope this is just common use case and there is an easy solution for that.

public class MyCamera extends ChaseCamera {

private static final float MOVE_SPEED = 6;

private Node rotationCenterNode;

Vector3f tmp = new Vector3f();
Quaternion tmpQuat = new Quaternion();
boolean shift;
boolean panning;

public MyCamera(SimpleApplication app, Node rotationCenterNode) {
	this(app.getCamera(), rotationCenterNode, app.getInputManager());
}

public MyCamera(Camera cam, Node rotationCenterNode, InputManager inputManager) {
	super(cam, rotationCenterNode, inputManager);
	this.rotationCenterNode = rotationCenterNode;
	
    inputManager.addMapping("rightClick", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
    inputManager.addMapping("up", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
    inputManager.addMapping("down", new MouseAxisTrigger(MouseInput.AXIS_Y, false));

	inputManager.addListener(new AnalogListener() {
		public void onAnalog(String name, float value, float tpf) {
			if (panning) {
				value *= MOVE_SPEED;
				if (name.equals("up")) {
					tmp.set(cam.getUp()).multLocal(value);
					rotationCenterNode.move(tmp);
				}
				if (name.equals("down")) {
					tmp.set(cam.getUp()).multLocal(-value);
					rotationCenterNode.move(tmp);
				}
			}
		}
	}, "up", "down");

    inputManager.addListener(new ActionListener() {
        public void onAction(String name, boolean isPressed, float tpf) {
            panning = name.equals("rightClick") && isPressed;
        }
    }, "click", "rightClick");
}

public void setRotationCenter(Vector3f rotationCenter) {
	rotationCenterNode.setLocalTranslation(rotationCenter);
}

public void setDefaultSettings() {
    setZoomSensitivity(0.03f);
    setMinVerticalRotation(-FastMath.HALF_PI / 2f);
    setInvertVerticalAxis(true);
    setHideCursorOnRotate(false);
    setRotationSpeed(7f);
    setDragToRotate(true);
}

}

Don’t extend chasecamera. Your camera will have a target to lookAt and you just rotate around it. You could even attach a cameraNode to it to save you the math hassle.

Yep. Start with nothing. Don’t extend the existing camera crap.

Put a node where you want your rotation to be. Call this “orbit” or something.

Node orbit = new Node(“orbitOrigin”);
orbit.setLocalTranslation(model.getWorldTranslation());

…attach it to your root node.

Make a child node from that node to be where you want the camera to be… probably child.setLocalTranslation(0, 0, zoomDistance); Rotate the child to face 0, 0, 0,… child.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);

Add a CameraNode to child to move the camera wherever child is.

When you want to rotate the camera around the object, rotate the orbit node.

When you want to zoom the camera in and out, move the z position of child.

The way I did it was to start with FlyByCamera and add orbit functions.

The CameraNode was a good hint.
The following code shows exactly the behavior I want. Zoom by the mouse wheel, right click and mouse move translates the object and left click and mouse move rotates the object.

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
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.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.shape.Box;
import com.jme3.texture.Texture;

public class MainTest extends SimpleApplication implements AnalogListener, ActionListener {

    boolean rotate = false;
    boolean translate = false;
    Vector3f direction = new Vector3f();

    private Node cubeNode;
    private CameraNode camNode;

    private void createBox() {
        Box cube1Mesh = new Box(1f, 1f, 1f);
        Geometry cube1Geo = new Geometry("My Textured Box", cube1Mesh);
        Material cube1Mat = new Material(assetManager,
                "Common/MatDefs/Misc/Unshaded.j3md");
        Texture cube1Tex = assetManager.loadTexture(
                "Interface/Logo/Monkey.jpg");
        cube1Mat.setTexture("ColorMap", cube1Tex);
        cube1Geo.setMaterial(cube1Mat);
        cubeNode = new Node();
        cubeNode.attachChild(cube1Geo);
        rootNode.attachChild(cubeNode);
    }

    private void createCameraNode() {
        camNode = new CameraNode("CamNode", cam);
        camNode.setControlDir(CameraControl.ControlDirection.CameraToSpatial);
        camNode.setLocalTranslation(new Vector3f(0, 1, -5));
        camNode.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
        cubeNode.attachChild(camNode);
    }

    public void registerInput() {
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping("toggleTranslate", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
        inputManager.addMapping("rotateRight", new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping("rotateLeft", new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping("rotateUp", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
        inputManager.addMapping("rotateDown", new MouseAxisTrigger(MouseInput.AXIS_Y, false));

        inputManager.addMapping("zoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        inputManager.addMapping("zoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

        inputManager.addListener(this, "rotateRight", "rotateLeft", "toggleRotate", "toggleTranslate",
                "rotateUp", "rotateDown", "zoomIn", "zoomOut");
    }

    @Override
    public void onAnalog(String name, float value, float tpf) {
        direction.set(cam.getDirection()).normalizeLocal();
        if (name.equals("zoomIn")) {
            direction.multLocal(-20 * tpf);
            cubeNode.move(direction);
        }
        if (name.equals("zoomOut")) {
            direction.multLocal(20 * tpf);
            cubeNode.move(direction);
        }
        if (name.equals("moveBackward")) {
            direction.multLocal(-5 * tpf);
            cubeNode.move(direction);
        }
        if (name.equals("moveRight")) {
            direction.crossLocal(Vector3f.UNIT_Y).multLocal(5 * tpf);
            cubeNode.move(direction);
        }
        if (name.equals("moveLeft")) {
            direction.crossLocal(Vector3f.UNIT_Y).multLocal(-5 * tpf);
            cubeNode.move(direction);
        }
        if (name.equals("rotateRight")) {
            if (rotate) {
                cubeNode.rotate(0, -5 * tpf, 0);
            }
            if (translate) {
                direction.crossLocal(Vector3f.UNIT_Y).multLocal(-5 * tpf);
                cubeNode.move(direction);
            }

        }
        if (name.equals("rotateLeft")) {
            if (rotate) {
                cubeNode.rotate(0, 5 * tpf, 0);
            }
            if (translate) {
                direction.crossLocal(Vector3f.UNIT_Y).multLocal(5 * tpf);
                cubeNode.move(direction);
            }
        }
        if (name.equals("rotateUp")) {
            if (rotate) {
                cubeNode.rotate(5 * tpf, 0, 0);
            }
            if (translate) {
                direction.crossLocal(Vector3f.UNIT_X).multLocal(5 * tpf);
                cubeNode.move(direction);
            }
        }
        if (name.equals("rotateDown")) {
            if (rotate) {
                cubeNode.rotate(-5 * tpf, 0, 0);
            }
            if (translate) {
                direction.crossLocal(Vector3f.UNIT_X).multLocal(-5 * tpf);
                cubeNode.move(direction);
            }
        }
    }

    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
        if (name.equals("toggleRotate") && keyPressed) {
            rotate = true;
        }
        if (name.equals("toggleRotate") && !keyPressed) {
            rotate = false;
        }
        if (name.equals("toggleTranslate") && keyPressed) {
            translate = true;
        }
        if (name.equals("toggleTranslate") && !keyPressed) {
            translate = false;
        }

    }

    @Override
    public void simpleInitApp() {
        createBox();
        createCameraNode();
        flyCam.setEnabled(false);
        registerInput();
        inputManager.setCursorVisible(true);
    }

    public static void main(String[] args) {
        new MainTest().start();
    }
}

Now I have just an other little problem. If I rotate my object then it rotates in all directions but I want limited on the x and y axis of the object. The code from my first example does this behavior.
Imaging the model is a human and the camera should just moveable around his x and y axis to avoid an uncontrolled rotating.

1 Like

I have realized that camNode isn’t necessary in my example. :frowning:
I moved the object and not the camera. This wasn’t intended of course I want control the camera.

Attach a cameraNode to the spatial. Moving the mouse left/right rotates the cameraNode on the Y axis. Moving it up/down rotates it on the X axis. Ignore the Z axis. You won’t need it unless you want to “roll” (tilt your head like a confused dog).

float[] angles = new float[3];
float maxRotY = FastMath.HALF_PI;
float minRotY = -FastMath.HALF_PI;
float speed = 1.0f; // the speed at which I'll rotate when I move.

if (im moving the mouse left/right) { // rotate the Y axis
    // value will be either +1 or -1 from the inputMapper.
    angles[1] += (value * speed);

    // limit camera rotation.
    if (angles[1] > maxRotY) {
        angles[1] = maxRotY;
    }
    else if (angles[1] < minRotY) {
        angles[1] = minRotY;
    }

    // and the same for the X axis when i'm moving up/down.

    cameraNode.setLocalRotation(new Quaternion().fromAngles(angles);
}

I’m pretty sure you can just use the Z localTranslation of the cameraNode to dictate “zoom”. It’s all off the top of my head, but that’s how I see it.

Thanks for your hint but it doesn’t work, I don’t know what I’m doing wrong. I think the camera isn’t looking in the right direction.
Here are some refactorings about my test and the implementation of your idea.

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
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.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.shape.Box;
import com.jme3.texture.Texture;

public class MainTest extends SimpleApplication implements AnalogListener, ActionListener {

    boolean rotate = false;
    boolean translate = false;
    Vector3f direction = new Vector3f();

    private Node cubeNode;
    private CameraNode camNode;

    private void createBox() {
        Box cube1Mesh = new Box(1f, 1f, 1f);
        Geometry cube1Geo = new Geometry("My Textured Box", cube1Mesh);
        Material cube1Mat = new Material(assetManager,
                "Common/MatDefs/Misc/Unshaded.j3md");
        Texture cube1Tex = assetManager.loadTexture(
                "Interface/Logo/Monkey.jpg");
        cube1Mat.setTexture("ColorMap", cube1Tex);
        cube1Geo.setMaterial(cube1Mat);
        cubeNode = new Node();
        cubeNode.attachChild(cube1Geo);
        rootNode.attachChild(cubeNode);
    }

    private void createCameraNode() {
        camNode = new CameraNode("CamNode", cam);
        camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
        camNode.setLocalTranslation(new Vector3f(0, 1, -5));
        camNode.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
        cubeNode.attachChild(camNode);
    }

    private static final String MOUSE_MOVE_RIGHT = "Right";
    private static final String MOUSE_MOVE_LEFT = "Left";
    private static final String MOUSE_MOVE_UP = "Up";
    private static final String MOUSE_MOVE_DOWN = "Down";

    public void registerInput() {
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping("toggleTranslate", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
        inputManager.addMapping(MOUSE_MOVE_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping(MOUSE_MOVE_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping(MOUSE_MOVE_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, true));
        inputManager.addMapping(MOUSE_MOVE_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, false));

        inputManager.addMapping("zoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        inputManager.addMapping("zoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

        inputManager.addListener(this, MOUSE_MOVE_RIGHT, MOUSE_MOVE_LEFT, "toggleRotate", "toggleTranslate",
                MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, "zoomIn", "zoomOut");
    }

    float[] angles = new float[3];
    float maxRotY = FastMath.HALF_PI;
    float minRotY = -FastMath.HALF_PI;

    @Override
    public void onAnalog(String name, float value, float tpf) {
        speed = 25f;
        direction.set(cam.getDirection()).normalizeLocal();
        if (name.equals("zoomIn")) {
            direction.multLocal(-20 * tpf);
            camNode.move(direction);
        }
        if (name.equals("zoomOut")) {
            direction.multLocal(20 * tpf);
            camNode.move(direction);
        }

        if (translate) {            
            Vector3f v = camNode.getLocalTranslation();
            switch (name) {
                case MOUSE_MOVE_RIGHT:
                    camNode.setLocalTranslation(v.x - value * speed, v.y, v.z);
                    break;
                case MOUSE_MOVE_LEFT:
                    camNode.setLocalTranslation(v.x + value * speed, v.y, v.z);
                    break;
                case MOUSE_MOVE_UP:
                    camNode.setLocalTranslation(v.x, v.y + value * speed, v.z);
                    break;
                case MOUSE_MOVE_DOWN:
                    camNode.setLocalTranslation(v.x, v.y - value * speed, v.z);
                    break;
                default:
                    break;
            }
        }
        if (rotate) {
            if (MOUSE_MOVE_RIGHT.equals(name) || MOUSE_MOVE_LEFT.equals(name)) { // rotate the Y axis
                // value will be either +1 or -1 from the inputMapper.
                angles[1] += (value * speed);

                // limit camera rotation.
                if (angles[1] > maxRotY) {
                    angles[1] = maxRotY;
                } else if (angles[1] < minRotY) {
                    angles[1] = minRotY;
                }

                // and the same for the X axis when i'm moving up/down.
                camNode.setLocalRotation(new Quaternion().fromAngles(angles));
            }
        }
    }

    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
        if (name.equals("toggleRotate") && keyPressed) {
            rotate = true;
        }
        if (name.equals("toggleRotate") && !keyPressed) {
            rotate = false;
        }
        if (name.equals("toggleTranslate") && keyPressed) {
            translate = true;
        }
        if (name.equals("toggleTranslate") && !keyPressed) {
            translate = false;
        }

    }

    @Override
    public void simpleInitApp() {
        createBox();
        createCameraNode();
        flyCam.setEnabled(false);
        registerInput();
        inputManager.setCursorVisible(true);
    }

    public static void main(String[] args) {
        new MainTest().start();
    }
}

Does it rotate as intended? If you want the camera to look at something, tell it to .lookAt() it.

I have tried:
cam.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
and some other things but I’m unable to lookAt the right direction.

I just wrote my own. It rotates up and down and limits the X rotation so you can’t go “upside down”.

package com.jayfella.test;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
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.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.debug.Arrow;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;

public class TestMouseRotate extends SimpleApplication implements AnalogListener, ActionListener {

    boolean rotate = false;
    boolean translate = false;
    Vector3f direction = new Vector3f();

    private Node cubeNode;

    public static void main(String[] args) {

        TestMouseRotate testMouseRotate = new TestMouseRotate();
        AppSettings appSettings = new AppSettings(true);
        appSettings.setFrameRate(120);
        appSettings.setResolution(1280, 720);
        testMouseRotate.setSettings(appSettings);

        testMouseRotate.setShowSettings(false);
        testMouseRotate.start();
    }

    private Geometry createBox() {
        Box cube1Mesh = new Box(1f, 1f, 1f);
        Geometry cube1Geo = new Geometry("My Textured Box", cube1Mesh);
        Material cube1Mat = new Material(assetManager,
                "Common/MatDefs/Misc/Unshaded.j3md");
       // Texture cube1Tex = assetManager.loadTexture(
                //"Interface/Logo/Monkey.jpg");
        //cube1Mat.setTexture("ColorMap", cube1Tex);
        cube1Geo.setMaterial(cube1Mat);
        cubeNode = new Node();
        cubeNode.attachChild(cube1Geo);
        rootNode.attachChild(cubeNode);

        return cube1Geo;
    }

    private void attachCoordinateAxes(Vector3f pos, float length) {
        Arrow arrow = new Arrow(Vector3f.UNIT_X.mult(length));
        putShape(arrow, ColorRGBA.Red).setLocalTranslation(pos);

        arrow = new Arrow(Vector3f.UNIT_Y.mult(length));
        putShape(arrow, ColorRGBA.Green).setLocalTranslation(pos);

        arrow = new Arrow(Vector3f.UNIT_Z.mult(length));
        putShape(arrow, ColorRGBA.Blue).setLocalTranslation(pos);
    }

    private Geometry putShape(Mesh shape, ColorRGBA color) {
        Geometry g = new Geometry("coordinate axis", shape);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.getAdditionalRenderState().setWireframe(true);
        mat.getAdditionalRenderState().setLineWidth(4);
        mat.setColor("Color", color);
        g.setMaterial(mat);
        rootNode.attachChild(g);
        return g;
    }

    private static final String MOUSE_MOVE_RIGHT = "Right";
    private static final String MOUSE_MOVE_LEFT = "Left";
    private static final String MOUSE_MOVE_UP = "Up";
    private static final String MOUSE_MOVE_DOWN = "Down";

    public void registerInput() {
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping("toggleTranslate", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
        inputManager.addMapping(MOUSE_MOVE_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping(MOUSE_MOVE_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping(MOUSE_MOVE_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, true));
        inputManager.addMapping(MOUSE_MOVE_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, false));

        inputManager.addMapping("zoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        inputManager.addMapping("zoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

        inputManager.addListener(this, MOUSE_MOVE_RIGHT, MOUSE_MOVE_LEFT, "toggleRotate", "toggleTranslate",
                MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, "zoomIn", "zoomOut");
    }

    float[] angles = new float[3];
    float maxRotX = FastMath.HALF_PI;
    float minRotX = -FastMath.PI;

    @Override
    public void onAnalog(String name, float value, float tpf) {
        speed = 25f;
        direction.set(cam.getDirection()).normalizeLocal();

        if (rotate) {

            if (MOUSE_MOVE_UP.equals(name) || MOUSE_MOVE_DOWN.equals(name)) { // rotate the Y axis

                int dirState = MOUSE_MOVE_UP.equals(name) ? 1 : -1;
                angles[0] += (dirState * (FastMath.DEG_TO_RAD * 180 * tpf));

                // 179 degrees. Avoid the "flip" problem.
                float maxRotX = FastMath.HALF_PI - FastMath.DEG_TO_RAD;

                // limit camera rotation.
                if (angles[0] < -maxRotX) {
                    angles[0] = -maxRotX;
                }

                if (angles[0] > maxRotX) {
                    angles[0] = maxRotX;
                }
            }

            if (MOUSE_MOVE_RIGHT.equals(name) || MOUSE_MOVE_LEFT.equals(name)) { // rotate the Y axis

                int dirState = MOUSE_MOVE_RIGHT.equals(name) ? 1 : -1;
                angles[1] += (dirState * (FastMath.DEG_TO_RAD * 180 * tpf));

                // stop the angles from becoming too big.
                if (angles[1] > FastMath.TWO_PI) {
                    angles[1] -= FastMath.TWO_PI;
                } else if (angles[1] < -FastMath.TWO_PI) {
                    angles[1] += FastMath.TWO_PI;
                }
            }

            Quaternion rotation = new Quaternion().fromAngles(angles);

            Vector3f direction = rotation.mult(Vector3f.UNIT_Z);
            Vector3f loc = direction.mult(15); // this is your ZOOM value
            cam.setLocation(loc);

            cam.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
        }
    }

    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
        if (name.equals("toggleRotate") && keyPressed) {
            rotate = true;
        }
        if (name.equals("toggleRotate") && !keyPressed) {
            rotate = false;
        }
        if (name.equals("toggleTranslate") && keyPressed) {
            translate = true;
        }
        if (name.equals("toggleTranslate") && !keyPressed) {
            translate = false;
        }

    }

    @Override
    public void simpleInitApp() {
        createBox();
        attachCoordinateAxes(new Vector3f(), 5);
        Geometry box2 = createBox();
        box2.setLocalTranslation(3, 0, 0);

        flyCam.setEnabled(false);
        registerInput();
        inputManager.setCursorVisible(true);
    }


}
1 Like

Thank you very much, jayfella. You really saved my weekend.
Below is the entire implementation with zoom and translation. Only thing that doesn’t work is the rotation of a moved node. I tried to multiply the loc vector with current camera postion but it doesn’t worked.

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
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.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
import com.jme3.texture.Texture;

public class MainTest extends SimpleApplication {

    boolean rotate = false;
    boolean translate = false;
    Vector3f direction = new Vector3f();

    private Node cubeNode;
    private CameraNode camNode;
    Node orbit = new Node("orbitOrigin");

    private void createBox() {
        Box cube1Mesh = new Box(1f, 1f, 1f);
        Geometry cube1Geo = new Geometry("My Textured Box", cube1Mesh);
        Material cube1Mat = new Material(assetManager,
                "Common/MatDefs/Misc/Unshaded.j3md");
        Texture cube1Tex = assetManager.loadTexture(
                "Interface/Logo/Monkey.jpg");
        cube1Mat.setTexture("ColorMap", cube1Tex);
        cube1Geo.setMaterial(cube1Mat);
        cubeNode = new Node();
        cubeNode.attachChild(cube1Geo);
        rootNode.attachChild(cubeNode);
    }

    private void createCameraNode() {
        camNode = new CameraNode("CamNode", cam);
        camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
        camNode.setLocalTranslation(new Vector3f(0, 1, -5));
        camNode.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
        cubeNode.attachChild(camNode);

    }

    private static final String MOUSE_MOVE_RIGHT = "Right";
    private static final String MOUSE_MOVE_LEFT = "Left";
    private static final String MOUSE_MOVE_UP = "Up";
    private static final String MOUSE_MOVE_DOWN = "Down";

    public void registerInput() {
        inputManager.addMapping("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping("toggleTranslate", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
        inputManager.addMapping(MOUSE_MOVE_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping(MOUSE_MOVE_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping(MOUSE_MOVE_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, true));
        inputManager.addMapping(MOUSE_MOVE_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, false));

        inputManager.addMapping("zoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        inputManager.addMapping("zoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

        inputManager.addListener(createAnalogListner(), MOUSE_MOVE_RIGHT, MOUSE_MOVE_LEFT,
                MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, "zoomIn", "zoomOut");
        inputManager.addListener(createActionListner(), "toggleRotate", "toggleTranslate");
    }

    float[] angles = new float[3];
    float maxRotY = FastMath.HALF_PI;
    float minRotY = -FastMath.HALF_PI;

    private AnalogListener createAnalogListner() {
        return new AnalogListener() {
            @Override
            public void onAnalog(String name, float value, float tpf) {
                speed = 25f;
                direction.set(cam.getDirection()).normalizeLocal();
                camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
                if (name.equals("zoomIn")) {
                    direction.multLocal(-80 * tpf);
                    camNode.move(direction);
                }
                if (name.equals("zoomOut")) {
                    direction.multLocal(80 * tpf);
                    camNode.move(direction);
                }

                if (translate) {
                    camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
                    Vector3f v = camNode.getLocalTranslation();
                    Vector3f transVec = new Vector3f();
                    switch (name) {
                        case MOUSE_MOVE_RIGHT:
                            transVec = new Vector3f(v.x - value * speed, v.y, v.z);
                            break;
                        case MOUSE_MOVE_LEFT:
                            transVec = new Vector3f(v.x + value * speed, v.y, v.z);
                            break;
                        case MOUSE_MOVE_UP:
                            transVec = new Vector3f(v.x, v.y + value * speed, v.z);
                            break;
                        case MOUSE_MOVE_DOWN:
                            transVec = new Vector3f(v.x, v.y - value * speed, v.z);
                            break;
                        default:
                            break;
                    }
                    System.out.println(name + " transVec: " + transVec);
                    camNode.setLocalTranslation(transVec);
                }
                if (rotate) {
                    if (MOUSE_MOVE_UP.equals(name) || MOUSE_MOVE_DOWN.equals(name)) { // rotate the Y axis

                        int dirState = MOUSE_MOVE_UP.equals(name) ? 1 : -1;
                        angles[0] += (dirState * (FastMath.DEG_TO_RAD * 180 * tpf));

                        float maxRotX = FastMath.HALF_PI - FastMath.DEG_TO_RAD;

                        // limit camera rotation.
                        if (angles[0] < -maxRotX) {
                            angles[0] = -maxRotX;
                        }

                        if (angles[0] > maxRotX) {
                            angles[0] = maxRotX;
                        }
                    }

                    if (MOUSE_MOVE_RIGHT.equals(name) || MOUSE_MOVE_LEFT.equals(name)) { // rotate the Y axis

                        int dirState = MOUSE_MOVE_RIGHT.equals(name) ? 1 : -1;
                        angles[1] += (dirState * (FastMath.DEG_TO_RAD * 180 * tpf));

                        // limit camera rotation.
                        if (angles[1] > FastMath.TWO_PI) {
                            angles[1] -= FastMath.TWO_PI;
                        } else if (angles[1] < -FastMath.TWO_PI) {
                            angles[1] += FastMath.TWO_PI;
                        }
                    }
                    Quaternion rotation = new Quaternion().fromAngles(angles);
                    
                    Vector3f dir1 = rotation.mult(Vector3f.UNIT_Z);
                    Vector3f loc = dir1.mult(5); // this is your ZOOM value
                    
                    System.out.println("====================================================================");
                    System.out.println("rotation: " + new Quaternion().fromAngles(angles));                    
                    System.out.println("dir1: " + dir1);
                    System.out.println("camNode.getLocalTranslation(): " + camNode.getLocalTranslation());
                    System.out.println("loc: " + loc);
                    
                    camNode.setLocalTranslation(loc);
                    System.out.println("camNode.setLocalTranslation(loc): " + camNode.getLocalTranslation());                    
                    camNode.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);
                }
            }
        };
    }

    public ActionListener createActionListner() {
        return new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (name.equals("toggleRotate") && isPressed) {
                    rotate = true;
                }
                if (name.equals("toggleRotate") && !isPressed) {
                    rotate = false;
                }
                if (name.equals("toggleTranslate") && isPressed) {
                    translate = true;
                }
                if (name.equals("toggleTranslate") && !isPressed) {
                    translate = false;
                }
            }
        };
    }

    @Override
    public void simpleInitApp() {
        flyCam.setEnabled(false);
        createBox();
        createCameraNode();
        registerInput();
        inputManager.setCursorVisible(true);
    }

    public static void main(String[] args) {
        MainTest app = new MainTest();
        AppSettings settings = new AppSettings(true);
        app.setSettings(settings);
        app.setShowSettings(false);
        app.start();
    }
}

Add the location of the thing you want to look at to “loc”. So something like…

Geometry someCubeToFocusOn = ...
Vector3f loc = dir1.mult(5); // this is your ZOOM value
loc.addLocal(someCubeToFocusOn.getLocalTranslation());
camNode.lookAt(someCubeToFocusOn.getLocalTranslation(), Vector3f.UNIT_Y);

I calculated the disance between the camNode and the object and multiply it to direction. It works as long as I don’t translate the object. After the rotation action is the object in the center of the view again. Of course that’s right because I assign the lookAt position to cubeNode. How can I say rotate and look at the previous direction? I tried to save the current rotation Quaternion and applied it again but it doesn’t work.

Vector3f dir1 = rotation.mult(Vector3f.UNIT_Z);
float dis = camNode.getLocalTranslation().distance(cubeNode.getLocalTranslation());                  
Vector3f loc = dir1.mult(dis); // this is your ZOOM value
camNode.setLocalTranslation(loc);
camNode.lookAt(cubeNode.getLocalTranslation(), Vector3f.UNIT_Y);

In my code there is a geometry it rotates around and a geometry it looks at. You can specify different geometries or locations for either of them.