Camera

Hi,



Does anyone has an working camera such as used in CAD tools, rotating the view around center with eg left mouse button, zoom in and out with mouse wheel, pan with the right mouse button? (Of course I had a look at the examples) I would be grateful for any hints…

Yes. I will try to put it up with a working test this weekend.



In the meantime, you can pick apart the ChaseCamera class to see how it handles camera movement around a node using spherical coordinates. Maybe you’ll be able write your own from that in short time

Thank you, that would be great!

As promised…



The test will show you how it all works.


  • Hold the right mouse button to orbit the camera

  • Hold SHIFT+ right mouse button to translate the camera (and the focus point)

  • Mouse wheel moves towards or away (zoom)

  • Double right click to select an object, the camera will focus on it and move towards it



and here's the test:

/**
 * Copyright (c) 2009, Andrew Carter 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 Andrew Carter nor the names of
 * 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 cam.acarter.test.orbit;

import jmetest.renderer.TestSkybox;

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Image;
import com.jme.image.Texture;
import com.jme.input.MouseInput;
import com.jme.math.Vector3f;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Skybox;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.VBOInfo;
import com.jme.scene.Spatial.TextureCombineMode;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Sphere;
import com.jme.scene.shape.Torus;
import com.jme.util.TextureManager;

public class TestOrbitInput extends SimpleGame {

   InputManager inputManager = new InputManager();
   
   Skybox skybox;

   public static void main(String[] args) {

      TestOrbitInput app = new TestOrbitInput();
      app.setConfigShowMode(ConfigShowMode.AlwaysShow);
      app.start();
   }

   @Override
   protected void initSystem() {

      super.initSystem();
   }

   @Override
   protected void simpleInitGame() {

      display.setTitle("Orbit Camera");

      // Replace the first person handler in BaseSimpleGame
      super.input = inputManager;

      display.setTitle("Orbit Camera");
      MouseInput.get().setCursorVisible(true);
      
      cam.setLocation(new Vector3f(100, -100, 200));
      cam.update();

      inputManager.setup(cam);
      inputManager.getOrbit().setWheelDirection(true);
      // The target position is the point the camera orbits around
      inputManager.getOrbit().setTargetPosition(new Vector3f(), true);

      Node pickNode = new Node();
      rootNode.attachChild(pickNode);
      
      // Set the node used when picking
      inputManager.getCameraFocus().setNode(pickNode);
      
       // Pop a few objects into our scene.
       Torus t = new Torus("Torus", 50, 50, 5, 10);
       t.setModelBound(new BoundingBox());
       t.updateModelBound();
       t.setLocalTranslation(new Vector3f(-40, 0, 10));
       t.setVBOInfo(new VBOInfo(true));
       pickNode.attachChild(t);
       t.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

       Sphere s = new Sphere("Sphere", 63, 50, 25);
       s.setModelBound(new BoundingBox());
       s.updateModelBound();
       s.setLocalTranslation(new Vector3f(40, 0, -10));
       pickNode.attachChild(s);
       s.setVBOInfo(new VBOInfo(true));
       s.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

       Box b = new Box("box", new Vector3f(-25, 70, -45), 20, 20, 20);
       b.setModelBound(new BoundingBox());
       b.updateModelBound();
       b.setVBOInfo(new VBOInfo(true));
       pickNode.attachChild(b);
       b.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

       // Create a skybox
       skybox = new Skybox("skybox", 10, 10, 10);

       Texture north = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/north.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);
       Texture south = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/south.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);
       Texture east = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/east.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);
       Texture west = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/west.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);
       Texture up = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/top.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);
       Texture down = TextureManager.loadTexture(
           TestSkybox.class.getClassLoader().getResource(
           "jmetest/data/texture/bottom.jpg"),
           Texture.MinificationFilter.BilinearNearestMipMap,
           Texture.MagnificationFilter.Bilinear, Image.Format.GuessNoCompression, 0.0f, true);

       skybox.setTexture(Skybox.Face.North, north);
       skybox.setTexture(Skybox.Face.West, west);
       skybox.setTexture(Skybox.Face.South, south);
       skybox.setTexture(Skybox.Face.East, east);
       skybox.setTexture(Skybox.Face.Up, up);
       skybox.setTexture(Skybox.Face.Down, down);
       skybox.preloadTextures();
      
       // Attach the skybox to the root node, not our pick node
       rootNode.attachChild(skybox);
      
       // Finally lets add some instructions
       Text text = Text.createDefaultTextLabel("Test Label", "Mouse Wheel: Zoom");
        text.setCullHint(Spatial.CullHint.Never);
        text.setTextureCombineMode(TextureCombineMode.Replace);
        text.setLocalTranslation(new Vector3f(1, 60, 0));
       
        statNode.attachChild(text);
       
        text = Text.createDefaultTextLabel("Test Label", "Hold RMB: Orbit");
        text.setCullHint(Spatial.CullHint.Never);
        text.setTextureCombineMode(TextureCombineMode.Replace);
        text.setLocalTranslation(new Vector3f(1, 45, 0));
       
        statNode.attachChild(text);
       
        text = Text.createDefaultTextLabel("Test Label", "Shift + RMB: Translate");
        text.setCullHint(Spatial.CullHint.Never);
        text.setTextureCombineMode(TextureCombineMode.Replace);
        text.setLocalTranslation(new Vector3f(1, 30, 0));
       
        statNode.attachChild(text);
       
        text = Text.createDefaultTextLabel("Test Label", "Double Click RMB: Focus on Object");
        text.setCullHint(Spatial.CullHint.Never);
        text.setTextureCombineMode(TextureCombineMode.Replace);
        text.setLocalTranslation(new Vector3f(1, 15, 0));
       
        statNode.attachChild(text);
   }

   @Override
   protected void simpleUpdate() {
      
      skybox.setLocalTranslation(cam.getLocation());
   }
}



the Input Handler:

/**
 * Copyright (c) 2009, Andrew Carter 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 Andrew Carter nor the names of
 * 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 cam.acarter.test.orbit;

import com.jme.input.InputHandler;
import com.jme.renderer.Camera;

/**
 * A special input handler for using the camera Orbit action and the Camera Focus action.
 *
 * @author Andrew Carter
 */
public class InputManager extends InputHandler {

   private OrbitAction orbit = null;
   
   private CameraFocusInputAction cameraFocus = null;
   
   /**
    *
    * @param camera
    */
   public void setup(Camera camera) {
      
      orbit = new OrbitAction(camera);
      addAction(orbit, InputHandler.DEVICE_ALL, InputHandler.BUTTON_ALL, InputHandler.AXIS_ALL, false);
      
      cameraFocus = new CameraFocusInputAction(camera, orbit);
      addAction(cameraFocus, InputHandler.DEVICE_MOUSE, InputHandler.BUTTON_ALL, InputHandler.AXIS_ALL, false);
   }
   
   /**
    *
    */
   @Override
   public void update(float time) {
      
      super.update(time);
      
      cameraFocus.getController().update(time);
   }
   
   /**
    *
    * @return
    */
   public OrbitAction getOrbit() {
      
      return orbit;
   }
   
   /**
    *
    * @return
    */
   public CameraFocusInputAction getCameraFocus() {
      
      return cameraFocus;
   }
}



... continued below

the Orbit Input Action:

/**
 * Copyright (c) 2009, Andrew Carter 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 Andrew Carter nor the names of
 * 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 cam.acarter.test.orbit;

import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;

/**
 * Orbits the camera around a specified target position and also handles
 * translation of the target position.
 *
 * @author Andrew Carter
 */
public class OrbitAction extends InputAction {

   private Camera camera = null;
   private Vector3f targetPosition = null;

   private boolean enabled = true;
   private boolean updated = false;

   private float maxZenith = 89.5f * FastMath.DEG_TO_RAD;
   private float moveLeftMultiplier = 0.5f;
   private float moveDownMultiplier = 0.5f;
   private float mouseXMultiplier = 2.0f;
   private float mouseYMultiplier = 2.0f;
   private float mouseRollMultiplier = 0.1f;
   private float translateHorizontalSpeed = 1.0f;
   private float translateVerticalSpeed = 1.0f;
   private float mouseXSpeed = 1.0f;
   private float mouseYSpeed = 1.0f;
   private float radialSpeed = 1.0f;

   // Temp variables
   private final Vector3f difTemp = new Vector3f();
   private final Vector3f sphereTemp = new Vector3f();
   private final Vector3f rightTemp = new Vector3f();

   private boolean mouseButtonPressed = false;
   private boolean keyModifierPressed = false;

   /**
    * Constructor.
    *
    * @param camera
    *            That camera that will be controlled by this input handler.
    */
   public OrbitAction(Camera camera) {

      this.camera = camera;
      targetPosition = new Vector3f();

      setSpeed(1);
   }

   /**
    * <code>setSpeed</code> sets the sensitivity of the input.
    *
    * @param speed
    *            sensitivity of the input.
    */
   public void setSpeed(float speed) {
      super.setSpeed(speed);

      translateHorizontalSpeed = moveLeftMultiplier * speed;
      translateVerticalSpeed = moveDownMultiplier * speed;
      mouseXSpeed = mouseXMultiplier * speed;
      mouseYSpeed = mouseYMultiplier * speed;
      radialSpeed = mouseRollMultiplier * speed;
   }

   /**
    * Reverse the direction of the mouse wheel input. The mouse wheel trigger
    * delta is opposite when used with a canvas.
    *
    * @param reversed
    */
   public void setWheelDirection(boolean reversed) {

      if(reversed)
         radialSpeed = Math.abs(radialSpeed) * -1.0f;
      else
         radialSpeed = Math.abs(radialSpeed);
   }

   /**
    * Handles camera updates based on input events.
    */
   public void performAction(InputActionEvent evt) {

      if(!enabled)
         return;

      updated = false;
      
      if((evt.getTriggerName().compareToIgnoreCase("MOUSE1") == 0) || (evt.getTriggerName().compareToIgnoreCase("BUTTON1") == 0)) {
         mouseButtonPressed = evt.getTriggerPressed();
      }
      // If the SHIFT key is pressed
      else if((evt.getTriggerName().compareToIgnoreCase("key") == 0) && ((evt.getTriggerIndex() == 42) || (evt.getTriggerIndex() == 54))) {
         keyModifierPressed = evt.getTriggerPressed();
      }
      else if(evt.getTriggerName().compareToIgnoreCase("X Axis") == 0) {

         if(mouseButtonPressed) {

            if(keyModifierPressed)
               translateHorizontal(evt.getTriggerDelta());
            else
               rotateAzimuth(evt.getTriggerDelta());

            updated = true;
         }
      }
      else if(evt.getTriggerName().compareToIgnoreCase("Y Axis") == 0) {

         if(mouseButtonPressed) {

            if(keyModifierPressed)
               translateVertically(evt.getTriggerDelta());
            else
               rotateZenith(evt.getTriggerDelta());

            updated = true;
         }
      }
      else if(evt.getTriggerName().compareToIgnoreCase("Wheel") == 0) {

         translateRadial(evt.getTriggerDelta());
         updated = true;
      }

      // If we have moved the camera in any way, update it
      if(updated)
         camera.update();
   }

   /**
    * <code>rotateAzimuth</code> updates the azimuth value of the camera's
    * spherical coordinates.
    *
    * @param amount
    */
   private void rotateAzimuth(float amount) {

      Vector3f cameraPosition = camera.getLocation();

      float azimuthAccel = (amount * mouseXSpeed);
      difTemp.set(cameraPosition).subtractLocal(targetPosition);

      FastMath.cartesianToSpherical(difTemp, sphereTemp);
      sphereTemp.y = FastMath.normalize(sphereTemp.y + (azimuthAccel), -FastMath.TWO_PI, FastMath.TWO_PI);
      FastMath.sphericalToCartesian(sphereTemp, rightTemp);

      rightTemp.addLocal(targetPosition);
      cameraPosition.set(rightTemp);

      camera.lookAt(targetPosition, Vector3f.UNIT_Y);
   }

   /**
    * <code>rotateZenith</code> updates the zenith/polar value of the camera's
    * spherical coordinates.
    *
    * @param amount
    */
   private void rotateZenith(float amount) {

      Vector3f cameraPosition = camera.getLocation();

      float thetaAccel = (-amount * mouseYSpeed);
      difTemp.set(cameraPosition).subtractLocal(targetPosition);

      FastMath.cartesianToSpherical(difTemp, sphereTemp);
      sphereTemp.z = FastMath.normalize(clampZenith(sphereTemp.z + (thetaAccel)), -FastMath.TWO_PI, FastMath.TWO_PI);
      FastMath.sphericalToCartesian(sphereTemp, rightTemp);

      rightTemp.addLocal(targetPosition);
      cameraPosition.set(rightTemp);

      camera.lookAt(targetPosition, Vector3f.UNIT_Y);
   }

   /**
    * <code>translateRadius</code> updates the radius value of the camera's
    * spherical coordinates.
    *
    * @param amount
    */
   private void translateRadial(float amount) {

      Vector3f cameraPosition = camera.getLocation();
      cameraPosition.subtractLocal(camera.getDirection().mult(radialSpeed * amount * getDistanceModifier(), difTemp));
   }

   /**
    * <code>translateVertically</code> updates the camera and target position
    * along a vertical plane.
    *
    * @param amount
    */
   private void translateVertically(float amount) {

      Vector3f cameraPosition = camera.getLocation();
      cameraPosition.subtractLocal(camera.getUp().mult(translateVerticalSpeed * amount * getDistanceModifier(), difTemp));
      targetPosition.subtractLocal(difTemp);
   }

   /**
    * <code>translateHorizontal</code> updates the camera and target position
    * along a horizontal plane.
    *
    * @param amount
    */
   private void translateHorizontal(float amount) {

      Vector3f cameraPosition = camera.getLocation();
      cameraPosition.addLocal(camera.getLeft().mult(translateHorizontalSpeed * amount * getDistanceModifier(), difTemp));
      targetPosition.addLocal(difTemp);
   }

   /**
    * <code>clampZenith</code> limits the rotation of the polar angle.
    *
    * @param zenith
    *            float
    * @return float
    */
   private float clampZenith(float zenith) {

      if(Float.isInfinite(zenith) || Float.isNaN(zenith))
         return zenith;

      if(zenith > maxZenith)
         zenith = maxZenith;
      else if(zenith < -maxZenith)
         zenith = -maxZenith;

      return zenith;
   }

   /**
    * Returns a scaler based on the camera distance from the target. Used to
    * increase sensitivity as the distance form the camera to the target
    * increases.
    *
    * @return float
    */
   private float getDistanceModifier() {

      return camera.getLocation().distance(targetPosition);
   }

   /**
    * Sets the enabled state of this input action.
    *
    * @param enabled
    *            True to enable
    */
   public void setEnabled(boolean enabled) {
      this.enabled = enabled;
   }

   /**
    * Returns the enabled state of this input action.
    *
    * @return True if enabled
    */
   public boolean getEnabled() {
      return enabled;
   }

   /**
    * Sets the orbit target position in world coordinates.
    *
    * @param position
    *            Values are set from this position
    * @param updateCamera
    *            Forces the camera to update its direction
    */
   public void setTargetPosition(Vector3f position, boolean updateCamera) {

      targetPosition.set(position);

      if(updateCamera)
         camera.lookAt(targetPosition, Vector3f.UNIT_Y);
   }

   /**
    * Returns a copy of the target position in world coordinates.
    *
    * @return Cloned target position
    */
   public Vector3f getTargetPosition() {

      return targetPosition.clone();
   }
}



and the Camera Focus Input Action:

/**
 * Copyright (c) 2009, Andrew Carter 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 Andrew Carter nor the names of
 * 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 cam.acarter.test.orbit;

import com.jme.bounding.BoundingSphere;
import com.jme.bounding.BoundingVolume;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.intersection.TrianglePickResults;
import com.jme.math.Ray;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.scene.Controller;
import com.jme.scene.Geometry;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.system.DisplaySystem;

/**
 * Uses the mouse to select a scene object, and smoothly move the camera towards
 * the selected object focusing at its center. This is used with the OrbitAction.
 *
 * @author Andrew Carter
 */
public class CameraFocusInputAction extends InputAction {

   // Max time(ms) for double click
   private long doubleClick = 500;

   private long clickTime = doubleClick;

   private Camera camera = null;

   private Vector2f screen = null;
   
   private Vector3f temp = null;
   
   private Node node = null;

   private CameraController controller = null;

   private boolean enabled = true;
   
   /**
    * Constructor.
    */
   public CameraFocusInputAction(Camera camera, OrbitAction orbit) {

      this.camera = camera;
      screen = new Vector2f();
      temp = new Vector3f();
      controller = new CameraController(camera, orbit);
   }

   /**
    * Returns the camera controller that is used to focus the camera on a specific object in the scene.
    *
    * @return CameraController
    */
   public CameraController getController() {

      return controller;
   }

   /**
    * @see com.jme.input.action.InputActionInterface#performAction(com.jme.input.action.InputActionEvent)
    */
   public void performAction(InputActionEvent evt) {

      if(!enabled)
         return;
      
      if(evt.getTriggerName().compareToIgnoreCase("X AXIS") == 0) {
           screen.x = evt.getTriggerPosition() * DisplaySystem.getDisplaySystem().getWidth();
      }
      else if(evt.getTriggerName().compareToIgnoreCase("Y AXIS") == 0) {
           screen.y = evt.getTriggerPosition() * DisplaySystem.getDisplaySystem().getHeight();
      }
      else if((evt.getTriggerName().compareToIgnoreCase("MOUSE1") == 0) || (evt.getTriggerName().compareToIgnoreCase("BUTTON1") == 0)) {

         if(evt.getTriggerPressed()) {

            // check if its a valid double click
            if((System.currentTimeMillis() - clickTime < doubleClick)) {

               Geometry geometry = pickGeometry(screen);
               moveCameraToSpatial(geometry);

               clickTime = doubleClick;
            }
            else
               clickTime = System.currentTimeMillis();
         }
      }
   }

   /**
    * Performs a mouse pick in the scene with supplied screen coordinates of
    * the mouse. The screen coordinates are converted into a ray in the scene
    * and used for triangle accuracy picking. The geometry closest to the
    * camera is returned.
    *
    * @param screenPos
    *            Screen coordinate of the mouse cursor
    * @return The geometry nearest the camera that was picked, or null
    */
   public Geometry pickGeometry(Vector2f screenPos) {

      temp = camera.getWorldCoordinates(screenPos, 1.0f);
      temp.subtractLocal(camera.getLocation());
      
      final Ray mouseRay = new Ray(camera.getLocation(), temp);
      mouseRay.getDirection().normalizeLocal();

      TrianglePickResults results = new TrianglePickResults();
      results.setCheckDistance(true);

      if(node != null)
         node.findPick(mouseRay, results);

      Geometry geometry = null;

      if(results.getNumber() > 0)
         geometry = results.getPickData(0).getTargetMesh();

      results.clear();

      return geometry;
   }

   /**
    * Calculates the distance from the object the camera will move to, and
    * initializes and starts the camera controller to move towards the given
    * spatial.
    *
    * @param spatial
    */
   public void moveCameraToSpatial(Spatial spatial) {

      // Magic number
      float distance = 3.0f;
      
      Vector3f target = null;

      if(spatial != null) {

         BoundingVolume worldBound = spatial.getWorldBound();

         if(worldBound != null) {
            
            target = worldBound.getCenter();
            BoundingSphere boundingSphere = new BoundingSphere(0.0f, target);
            boundingSphere.mergeLocal(worldBound);
            
            // This puts the camera at just the right distance to fill the screen
            distance *= boundingSphere.getRadius();
         }
      }

      if(controller.initialize(target, distance))
         controller.setActive(true);
   }
   
   /**
    * Returns true if this input action is enabled.
    * @return
    */
   public boolean isEnabled() {
      
      return enabled;
   }

   /**
    * Enable or disable this input action.
    * @param enabled
    */
   public void setEnabled(boolean enabled) {
      
      this.enabled = enabled;
   }
   
   /**
    * Set the node that is used when picking in the scene.
    *
    * @param node
    */
   public void setNode(Node node) {
      
      this.node = node;
   }

   /**
    * Controller used in conjuction with the Orbit handler to smoothly move the
    * camera towards a scene object.
    *
    * @author Andrew Carter
    */
   public class CameraController extends Controller {

      private static final long serialVersionUID = 1L;

      private Camera camera;

      private OrbitAction orbit;

      private float distance = 0.0f;

      private Vector3f cameraStart = new Vector3f();

      private Vector3f cameraEnd = new Vector3f();

      private Vector3f targetStart = new Vector3f();

      private Vector3f targetEnd = new Vector3f();

      private Vector3f temp1 = new Vector3f();

      private Vector3f temp2 = new Vector3f();

      private float elapsedTime = 0.0f;

      private boolean initialized;

      /**
       * Constructor.
       *
       * @param camera
       * @param orbit
       */
      public CameraController(Camera camera, OrbitAction orbit) {

         this.camera = camera;
         this.orbit = orbit;

         float time = 1.5f;

         setMaxTime(time);
         initialized = false;
      }

      /**
       * Initialize the controller.
       *
       * @param target The new focus point
       * @param distance Move until this far from the target
       * @return True if the controller is initialized
       */
      public boolean initialize(Vector3f target, float distance) {

         if(target != null) {

            this.distance = distance;
            elapsedTime = 0.0f;

            calculateCoordinates(target);

            initialized = true;
         }

         return initialized;
      }

      /**
       * Calculates path locations used for moving the camera.
       */
      private void calculateCoordinates(Vector3f target) {

         cameraStart.set(camera.getLocation().clone());
         temp1.set(target.subtract(cameraStart));

         float delta = 1 - (distance / temp1.length());
         temp1.multLocal(delta);

         cameraEnd.set(cameraStart.add(temp1));

         targetStart.set(orbit.getTargetPosition());
         targetEnd.set(target);
      }

      /**
       * @see com.jme.scene.Controller#update(float)
       */
      @Override
      public void update(float tpf) {

         if(initialized && isActive()) {

            elapsedTime += tpf;
            float delta = elapsedTime / getMaxTime();

            if(delta > 1.0f)
               delta = 1.0f;

            temp1.interpolate(cameraStart, cameraEnd, delta);
            camera.setLocation(temp1);

            camera.update();

            temp2.interpolate(targetStart, targetEnd, delta);
            orbit.setTargetPosition(temp2, true);

            if(delta == 1) {

               setActive(false);
               initialized = false;
            }
         }
      }
   }

}



Thats exactly what i was looking for. Thank you very much, great work!

hello,

i use your camera and it works fine, really great work  :slight_smile: !

but i have a probleme:

i have 3 boxes but i just can focus the first box, when i double click on the other boxes nothing happens!

I use it with a canvas implementor, and i set the "setDragOnly" to false because otherwise i get some strange camera behaviour



      class MyImplementor extends SimpleCanvasImpl {
         
         InputManager inputManager = new InputManager();
         
         private Text mousepos;
         private InputHandler input;
         private Node pickNode;
         
         private DisplaySystem display;
         private Box box;
         private Box box2;
         long startTime = 0;
         long fps = 0;
         
         public MyImplementor(int width, int height) {
            super(width, height);
         }
      
         public void simpleSetup() {   
         display = DisplaySystem
            .getDisplaySystem(LWJGLSystemProvider.LWJGL_SYSTEM_IDENTIFIER);

            input = inputManager;
         
            display.setTitle("Orbit Camera");
            MouseInput.get().setCursorVisible(true);
            
            cam.setLocation(new Vector3f(100, -100, 200));
            cam.update();

            inputManager.setup(cam);
            inputManager.getOrbit().setWheelDirection(true);
            // The target position is the point the camera orbits around
            inputManager.getOrbit().setTargetPosition(new Vector3f(), true);

            Node pickNode = new Node();
            rootNode.attachChild(pickNode);
            
            // Set the node used when picking
            inputManager.getCameraFocus().setNode(pickNode);
            
            Vector3f max = new Vector3f(3, 3, 3);
            Vector3f min = new Vector3f(-3, -3, -3);

            box = new Box("Box", min, max);
            box.setModelBound(new BoundingBox());
            box.updateModelBound();
            box.setLocalTranslation(new Vector3f(0, 0, 0));
            box.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
            pickNode.attachChild(box);
            box.setRandomColors();
                     
            box2 = new Box("Box2", min, max);
            box2.setModelBound(new BoundingBox());
            box2.updateModelBound();
            box2.setLocalTranslation(new Vector3f(-80, 50, 90));
            box2.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
            pickNode.attachChild(box2);
            box2.setRandomColors();

         }

         public void simpleUpdate() {            
            input.update(tpf);
                       }



thanx for any help

The picking code is likely going to be different when used in a canvas. You will have to find the mouse coordinates relative to the canvas to get the correct screen coordinates (CameraFocusInputAction.performAction()). I have been using this in a canvas, and made the changes to have it work in a regular window for this post. If you don't get it figured out, I will help you.

ok, i will try my best to figure that out 

thanks for your help

i think i got it,  but i don

This is what I have…



   /**
    * @see com.jme.input.action.InputActionInterface#performAction(com.jme.input.action.InputActionEvent)
    */
   public void performAction(InputActionEvent evt) {

      if(!enabled)
         return;
      
      f((evt.getTriggerName().compareToIgnoreCase("MOUSE1") == 0) || (evt.getTriggerName().compareToIgnoreCase("BUTTON1") == 0)) {

         if(evt.getTriggerPressed()) {

            if((System.currentTimeMillis() - clickTime < doubleClick)) {

               Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
               Point canvasLocation = CanvasView.getInstance().getCanvas().getLocationOnScreen();
               screen.set(mouseLocation.x - canvasLocation.x, mouseLocation.y - canvasLocation.y);

               // Adjust for flipped Y screen coordinate
               screen.y = CanvasView.getInstance().getCanvas().getHeight() - screen.y;

               Geometry geometry = pickGeometry(screen);

               moveCameraToSpatial(geometry);
               clickTime = doubleClick;
            }
            else
               clickTime = System.currentTimeMillis();
         }
      }
   }



Substitute CanvasView.getInstance().getCanvas() with a reference to your LWJGLCanvas and you should be good to go.

ok, this looks alot better :),

but CanvasView.getInstance().getCanvas() cannot be resolved,

do i need android for that? have tried it with Android 1.5 but it doesn

Substitute CanvasView.getInstance().getCanvas() with a reference to your LWJGLCanvas and you should be good to go.

how can i reference to my canvas? i tried alot of things but i really don