diff --git a/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java b/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java new file mode 100644 index 000000000..e15cee0df --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java @@ -0,0 +1,484 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.app; + +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import com.jme3.input.CameraInput; +import com.jme3.input.InputManager; +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.input.controls.Trigger; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.CameraNode; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.control.CameraControl; +import com.jme3.util.TempVars; + +/** + * This class is a camera controler that allow the camera to follow a target + * Spatial. + * + * @author Nehon + */ +public class ChaseCameraAppState extends AbstractAppState implements ActionListener, AnalogListener { + + protected Spatial spatial; + protected Node target; + protected CameraNode camNode; + protected InputManager inputManager; + protected boolean invertYaxis = false; + protected boolean invertXaxis = false; + protected boolean hideCursorOnRotate = true; + protected boolean canRotate; + protected boolean dragToRotate = true; + protected float rotationSpeed = 1.0f; + protected float zoomSpeed = 2.0f; + //protected boolean zoomin; + protected float minDistance = 1.0f; + protected float maxDistance = 40.0f; + protected float distance = 20; + protected float maxVerticalRotation = 1.4f; + protected float verticalRotation = 0f; + protected float minVerticalRotation = 0f; + protected float horizontalRotation = 0f; + //protected float distanceLerpFactor = 0; + protected Vector3f upVector; + protected Vector3f leftVector; + protected Trigger[] zoomOutTrigger = {new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)}; + protected Trigger[] zoomInTrigger = {new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)}; + protected Trigger[] toggleRotateTrigger = {new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)}; + +// +// protected boolean rotating = false; +// protected float rotation = 0; +// protected float targetRotation = rotation; + public ChaseCameraAppState() { + } + + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + this.inputManager = app.getInputManager(); + target = new Node("ChaseCamTarget"); + camNode = new CameraNode("ChaseCameraNode", app.getCamera()); + camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera); + target.attachChild(camNode); + camNode.setLocalTranslation(0, 0, distance); + upVector = app.getCamera().getUp().clone(); + leftVector = app.getCamera().getLeft().clone(); + registerWithInput(); + rotateCamera(); + } + + /** + * Registers inputs with the input manager + * + * @param inputManager + */ + public final void registerWithInput() { + + String[] inputs = {CameraInput.CHASECAM_TOGGLEROTATE, + CameraInput.CHASECAM_DOWN, + CameraInput.CHASECAM_UP, + CameraInput.CHASECAM_MOVELEFT, + CameraInput.CHASECAM_MOVERIGHT, + CameraInput.CHASECAM_ZOOMIN, + CameraInput.CHASECAM_ZOOMOUT}; + initVerticalAxisInputs(); + initZoomInput(); + initHorizontalAxisInput(); + initTogleRotateInput(); + + inputManager.addListener(this, inputs); + } + + public void onAction(String name, boolean keyPressed, float tpf) { + if (isEnabled()) { + if (dragToRotate) { + if (name.equals(CameraInput.CHASECAM_TOGGLEROTATE) && isEnabled()) { + if (keyPressed) { + canRotate = true; + if (hideCursorOnRotate) { + inputManager.setCursorVisible(false); + } + } else { + canRotate = false; + if (hideCursorOnRotate) { + inputManager.setCursorVisible(true); + } + } + } + } + } + + } + + public void onAnalog(String name, float value, float tpf) { + if (isEnabled()) { + if (canRotate) { + if (name.equals(CameraInput.CHASECAM_MOVELEFT)) { + horizontalRotation -= value * rotationSpeed; + rotateCamera(); + } else if (name.equals(CameraInput.CHASECAM_MOVERIGHT)) { + horizontalRotation += value * rotationSpeed; + rotateCamera(); + } else if (name.equals(CameraInput.CHASECAM_UP)) { + verticalRotation += value * rotationSpeed; + rotateCamera(); + } else if (name.equals(CameraInput.CHASECAM_DOWN)) { + verticalRotation -= value * rotationSpeed; + rotateCamera(); + } + } + if (name.equals(CameraInput.CHASECAM_ZOOMIN)) { + zoomCamera(-value * zoomSpeed); + } else if (name.equals(CameraInput.CHASECAM_ZOOMOUT)) { + zoomCamera(+value * zoomSpeed); + } + } + } + + /** + * rotate the camera around the target + */ + protected void rotateCamera() { + verticalRotation = FastMath.clamp(verticalRotation, minVerticalRotation, maxVerticalRotation); + TempVars vars = TempVars.get(); + Quaternion rot = vars.quat1; + Quaternion rot2 = vars.quat2; + rot.fromAngleNormalAxis(verticalRotation, leftVector); + rot2.fromAngleNormalAxis(horizontalRotation, upVector); + rot2.multLocal(rot); + target.setLocalRotation(rot2); + vars.release(); + } + + /** + * move the camera toward or away the target + */ + protected void zoomCamera(float value) { + distance = FastMath.clamp(distance + value, minDistance, maxDistance); + camNode.setLocalTranslation(new Vector3f(0, 0, distance)); + } + + public void setTarget(Spatial targetSpatial) { + spatial = targetSpatial; + } + + @Override + public void update(float tpf) { + if (spatial == null) { + throw new IllegalArgumentException("The spatial to follow is null, please use the setTarget method"); + } + target.setLocalTranslation(spatial.getWorldTranslation()); + camNode.lookAt(target.getWorldTranslation(), upVector); + + target.updateLogicalState(tpf); + target.updateGeometricState(); + } + + /** + * Sets custom triggers for toggleing the rotation of the cam deafult are + * new MouseButtonTrigger(MouseInput.BUTTON_LEFT) left mouse button new + * MouseButtonTrigger(MouseInput.BUTTON_RIGHT) right mouse button + * + * @param triggers + */ + public void setToggleRotationTrigger(Trigger... triggers) { + toggleRotateTrigger = triggers; + if (inputManager != null) { + inputManager.deleteMapping(CameraInput.CHASECAM_TOGGLEROTATE); + initTogleRotateInput(); + inputManager.addListener(this, CameraInput.CHASECAM_TOGGLEROTATE); + } + } + + /** + * Sets custom triggers for zomming in the cam default is new + * MouseAxisTrigger(MouseInput.AXIS_WHEEL, true) mouse wheel up + * + * @param triggers + */ + public void setZoomInTrigger(Trigger... triggers) { + zoomInTrigger = triggers; + if (inputManager != null) { + inputManager.deleteMapping(CameraInput.CHASECAM_ZOOMIN); + inputManager.addMapping(CameraInput.CHASECAM_ZOOMIN, zoomInTrigger); + inputManager.addListener(this, CameraInput.CHASECAM_ZOOMIN); + } + } + + /** + * Sets custom triggers for zomming out the cam default is new + * MouseAxisTrigger(MouseInput.AXIS_WHEEL, false) mouse wheel down + * + * @param triggers + */ + public void setZoomOutTrigger(Trigger... triggers) { + zoomOutTrigger = triggers; + if (inputManager != null) { + inputManager.deleteMapping(CameraInput.CHASECAM_ZOOMOUT); + inputManager.addMapping(CameraInput.CHASECAM_ZOOMOUT, zoomOutTrigger); + inputManager.addListener(this, CameraInput.CHASECAM_ZOOMOUT); + } + } + + /** + * Returns the max zoom distance of the camera (default is 40) + * + * @return maxDistance + */ + public float getMaxDistance() { + return maxDistance; + } + + /** + * Sets the max zoom distance of the camera (default is 40) + * + * @param maxDistance + */ + public void setMaxDistance(float maxDistance) { + this.maxDistance = maxDistance; + zoomCamera(distance); + } + + /** + * Returns the min zoom distance of the camera (default is 1) + * + * @return minDistance + */ + public float getMinDistance() { + return minDistance; + } + + /** + * Sets the min zoom distance of the camera (default is 1) + */ + public void setMinDistance(float minDistance) { + this.minDistance = minDistance; + zoomCamera(distance); + } + + /** + * @return The maximal vertical rotation angle in radian of the camera + * around the target + */ + public float getMaxVerticalRotation() { + return maxVerticalRotation; + } + + /** + * Sets the maximal vertical rotation angle in radian of the camera around + * the target. Default is Pi/2; + * + * @param maxVerticalRotation + */ + public void setMaxVerticalRotation(float maxVerticalRotation) { + this.maxVerticalRotation = maxVerticalRotation; + rotateCamera(); + } + + /** + * + * @return The minimal vertical rotation angle in radian of the camera + * around the target + */ + public float getMinVerticalRotation() { + return minVerticalRotation; + } + + /** + * Sets the minimal vertical rotation angle in radian of the camera around + * the target default is 0; + * + * @param minHeight + */ + public void setMinVerticalRotation(float minHeight) { + this.minVerticalRotation = minHeight; + rotateCamera(); + } + + /** + * returns the zoom speed + * + * @return + */ + public float getZoomSpeed() { + return zoomSpeed; + } + + /** + * Sets the zoom speed, the lower the value, the slower the camera will zoom + * in and out. default is 2. + * + * @param zoomSpeed + */ + public void setZoomSpeed(float zoomSpeed) { + this.zoomSpeed = zoomSpeed; + } + + /** + * Returns the rotation speed when the mouse is moved. + * + * @return the rotation speed when the mouse is moved. + */ + public float getRotationSpeed() { + return rotationSpeed; + } + + /** + * Sets the rotate amount when user moves his mouse, the lower the value, + * the slower the camera will rotate. default is 1. + * + * @param rotationSpeed Rotation speed on mouse movement, default is 1. + */ + public void setRotationSpeed(float rotationSpeed) { + this.rotationSpeed = rotationSpeed; + } + + /** + * Sets the default distance at start of applicaiton + * + * @param defaultDistance + */ + public void setDefaultDistance(float defaultDistance) { + distance = defaultDistance; + } + + /** + * sets the default horizontal rotation in radian of the camera at start of + * the application + * + * @param angleInRad + */ + public void setDefaultHorizontalRotation(float angleInRad) { + horizontalRotation = angleInRad; + } + + /** + * sets the default vertical rotation in radian of the camera at start of + * the application + * + * @param angleInRad + */ + public void setDefaultVerticalRotation(float angleInRad) { + verticalRotation = angleInRad; + } + + /** + * @return If drag to rotate feature is enabled. + * + * @see FlyByCamera#setDragToRotate(boolean) + */ + public boolean isDragToRotate() { + return dragToRotate; + } + + /** + * @param dragToRotate When true, the user must hold the mouse button and + * drag over the screen to rotate the camera, and the cursor is visible + * until dragged. Otherwise, the cursor is invisible at all times and + * holding the mouse button is not needed to rotate the camera. This feature + * is disabled by default. + */ + public void setDragToRotate(boolean dragToRotate) { + this.dragToRotate = dragToRotate; + this.canRotate = !dragToRotate; + inputManager.setCursorVisible(dragToRotate); + } + + /** + * invert the vertical axis movement of the mouse + * + * @param invertYaxis + */ + public void setInvertVerticalAxis(boolean invertYaxis) { + this.invertYaxis = invertYaxis; + if (inputManager != null) { + inputManager.deleteMapping(CameraInput.CHASECAM_DOWN); + inputManager.deleteMapping(CameraInput.CHASECAM_UP); + initVerticalAxisInputs(); + inputManager.addListener(this, CameraInput.CHASECAM_DOWN, CameraInput.CHASECAM_UP); + } + } + + /** + * invert the Horizontal axis movement of the mouse + * + * @param invertXaxis + */ + public void setInvertHorizontalAxis(boolean invertXaxis) { + this.invertXaxis = invertXaxis; + if (inputManager != null) { + inputManager.deleteMapping(CameraInput.CHASECAM_MOVELEFT); + inputManager.deleteMapping(CameraInput.CHASECAM_MOVERIGHT); + initHorizontalAxisInput(); + inputManager.addListener(this, CameraInput.CHASECAM_MOVELEFT, CameraInput.CHASECAM_MOVERIGHT); + } + } + + private void initVerticalAxisInputs() { + if (!invertYaxis) { + inputManager.addMapping(CameraInput.CHASECAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, true)); + inputManager.addMapping(CameraInput.CHASECAM_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, false)); + } else { + inputManager.addMapping(CameraInput.CHASECAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, false)); + inputManager.addMapping(CameraInput.CHASECAM_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, true)); + } + } + + private void initHorizontalAxisInput() { + if (!invertXaxis) { + inputManager.addMapping(CameraInput.CHASECAM_MOVELEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true)); + inputManager.addMapping(CameraInput.CHASECAM_MOVERIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, false)); + } else { + inputManager.addMapping(CameraInput.CHASECAM_MOVELEFT, new MouseAxisTrigger(MouseInput.AXIS_X, false)); + inputManager.addMapping(CameraInput.CHASECAM_MOVERIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, true)); + } + } + + private void initZoomInput() { + inputManager.addMapping(CameraInput.CHASECAM_ZOOMIN, zoomInTrigger); + inputManager.addMapping(CameraInput.CHASECAM_ZOOMOUT, zoomOutTrigger); + } + + private void initTogleRotateInput() { + inputManager.addMapping(CameraInput.CHASECAM_TOGGLEROTATE, toggleRotateTrigger); + } +} diff --git a/jme3-examples/src/main/java/jme3test/input/TestChaseCameraAppState.java b/jme3-examples/src/main/java/jme3test/input/TestChaseCameraAppState.java new file mode 100644 index 000000000..38c3651cb --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/input/TestChaseCameraAppState.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3test.input; + +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.FlyCamAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.AnalogListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Quad; + +/** A 3rd-person chase camera orbits a target (teapot). + * Follow the teapot with WASD keys, rotate by dragging the mouse. */ +public class TestChaseCameraAppState extends SimpleApplication implements AnalogListener, ActionListener { + + private Geometry teaGeom; + + public static void main(String[] args) { + TestChaseCameraAppState app = new TestChaseCameraAppState(); + app.start(); + } + + public void simpleInitApp() { + // Load a teapot model + teaGeom = (Geometry) assetManager.loadModel("Models/Teapot/Teapot.obj"); + Material mat_tea = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); + teaGeom.setMaterial(mat_tea); + rootNode.attachChild(teaGeom); + + // Load a floor model + Material mat_ground = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat_ground.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg")); + Geometry ground = new Geometry("ground", new Quad(50, 50)); + ground.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); + ground.setLocalTranslation(-25, -1, 25); + ground.setMaterial(mat_ground); + rootNode.attachChild(ground); + + //disable the flyCam + stateManager.detach(stateManager.getState(FlyCamAppState.class)); + + // Enable a chase cam + ChaseCameraAppState chaseCamAS = new ChaseCameraAppState(); + chaseCamAS.setTarget(teaGeom); + stateManager.attach(chaseCamAS); + + //Uncomment this to invert the camera's vertical rotation Axis + //chaseCamAS.setInvertVerticalAxis(true); + + //Uncomment this to invert the camera's horizontal rotation Axis + //chaseCamAS.setInvertHorizontalAxis(true); + + //Uncomment this to enable rotation when the middle mouse button is pressed (like Blender) + //WARNING : setting this trigger disable the rotation on right and left mouse button click + //chaseCamAS.setToggleRotationTrigger(new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE)); + + //Uncomment this to set mutiple triggers to enable rotation of the cam + //Here space bar and middle mouse button + //chaseCamAS.setToggleRotationTrigger(new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE),new KeyTrigger(KeyInput.KEY_SPACE)); + + //registering inputs for target's movement + registerInput(); + + } + + public void registerInput() { + inputManager.addMapping("moveForward", new KeyTrigger(KeyInput.KEY_UP), new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("moveBackward", new KeyTrigger(KeyInput.KEY_DOWN), new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("moveRight", new KeyTrigger(KeyInput.KEY_RIGHT), new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("moveLeft", new KeyTrigger(KeyInput.KEY_LEFT), new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("displayPosition", new KeyTrigger(KeyInput.KEY_P)); + inputManager.addListener(this, "moveForward", "moveBackward", "moveRight", "moveLeft"); + inputManager.addListener(this, "displayPosition"); + } + + public void onAnalog(String name, float value, float tpf) { + if (name.equals("moveForward")) { + teaGeom.move(0, 0, -5 * tpf); + } + if (name.equals("moveBackward")) { + teaGeom.move(0, 0, 5 * tpf); + } + if (name.equals("moveRight")) { + teaGeom.move(5 * tpf, 0, 0); + } + if (name.equals("moveLeft")) { + teaGeom.move(-5 * tpf, 0, 0); + + } + + } + + public void onAction(String name, boolean keyPressed, float tpf) { + if (name.equals("displayPosition") && keyPressed) { + teaGeom.move(10, 10, 10); + + } + } + + @Override + public void simpleUpdate(float tpf) { + super.simpleUpdate(tpf); + + // teaGeom.move(new Vector3f(0.001f, 0, 0)); + // pivot.rotate(0, 0.00001f, 0); + // rootNode.updateGeometricState(); + } +// public void update() { +// super.update(); +//// render the viewports +// float tpf = timer.getTimePerFrame(); +// state.getRootNode().rotate(0, 0.000001f, 0); +// stateManager.update(tpf); +// stateManager.render(renderManager); +// renderManager.render(tpf); +// } +}