十四、相机

透视投影

一个图理解透视投影。

image.png

相机位置.posiiotn和.lookAt(相机拍摄目标位置)

image.png

默认的 cam 相机

在 com.jme3.app.Application 中默认了一个相机cam,其默认属性如下:

  • 相机的宽高默认为当前Application的settings.getWidth() 和 settings.getHeight()的值。
  • 视角:
    • 视觉fov为沿Y轴45度角。
    • 视窗的宽高比例aspect为宽除以高度。
    • 近景面near为1 wu。
    • 远景面far为1000 wu。
  • 初始位置在(0f, 0f, 10f)处。
  • 看向原点。
方法 描述
cam.getLocation(), setLocation() 摄像机的位置
cam.getRotation(), setRotation() 摄像机旋转
cam.getLeft(), setLeft() 照相机的左轴
cam.getUp(), setUp() 机的上轴,通常为矢量3f (0,1,0)
cam.getDirection() 相机面对的矢量
cam.getAxes(), setAxes(left,up,dir) 左、上、方向三个属性的一个访问器。
cam.getFrame(), setFrame(loc,left,up,dir) location、left、up、direction 四个属性的一个访问器。
cam.resize(width, height, fixAspect) 调整现有摄像机对象的大小,同时保留所有其他设置。将 fixAspect 设置为 true 以调整长宽比
cam.setFrustum( near, far, left, right, top, bottom ) 视截头体由近/远平面、左/右平面、顶/底平面(所有距离都是浮点值)定义
cam.setFrustumPerspective( fovY, aspect ratio, near, far) 视锥体由沿 y 轴的视角(以度为单位)、长宽比和近/远平面定义。
cam.lookAt(target,up) 用来指定相机拍摄对象的目标坐标位置,target为目标的坐标,up为相机的上轴旋转方向。
cam.setParallelProjection(false) 正常的视角
cam.setParallelProjection(true) 平行投影透视
cam.getScreenCoordinates(worldPos) 将给定位置从世界空间转换为屏幕空间。

更改视图端口、视图锥体或帧之后,调用 cam.update () ;

flyBy 相机

flyCam 扩展了 com.jme3.app.SimpleApplicatio 中的相机功能,使其很轻松地就能访问其AppState。

flyCam 类字段允许您访问一个 AppState,该 AppState 扩展了 com.jme3.app.SimpleApplication 中的默认相机。默认配置了其 InputManager 输入管理。使其能响应 WASD 键,用于向前和向后走,以及向两侧扫射; 移动鼠标旋转摄像机 ,滚动鼠标滚轮进行放大或缩小。QZ 键垂直升起或降低相机。

Q  W             up   forw
A  S  D    -->  left  back  right
Z               down

相机跟随

当玩家以第一人称视角操纵游戏角色时,这个实现方式是直接操纵flyCame摄像机(flyCam.setEnabled(true);) ,这时玩家永远看不到角色本身。然而,在第三人称视角的游戏中,玩家看到的是角色走路,而你(游戏开发者)想让摄像机跟着角色走路。

有两种方法可以让相机做到这一点:

  • 注册一个追踪摄像机。
  • 将摄像机绑定到角色上。

注册一个追踪的摄像机示例

Jme3支持可选的 Chase 相机,它可以跟踪移动目标 Spatial (com.jme3.input.ChaseCamera)。当你需要相机跟随游戏中的角色时,可以使用这个相机。

flyCam.setEnabled(false);
ChaseCamera chaseCam = new ChaseCamera(cam, target, inputManager);
方法 描述
chaseCam.setSmoothMotion(true); 当摄像机移动时,插入一个更平滑的加速/减速。
chaseCam.setChasingSensitivity(5f) 追踪灵敏度越低,摄像机移动时跟踪目标的速度就越慢。
chaseCam.setTrailingSensitivity(0.5f) 随后的灵敏度越低,当目标移动时,摄像机开始追踪目标的速度就越慢。默认值为0.5;
chaseCam.setRotationSensitivity(5f) 灵敏度越低,当拖动鼠标时,相机围绕目标旋转的速度就越慢。默认值为5。
chaseCam.setTrailingRotationInertia(0.1f) 这样可以防止当目标在到达目标的尾随位置之前停止旋转时,摄像机突然停止。默认值为0.1 f。
chaseCam.setDefaultDistance(40); 应用程序开始时到目标的默认距离。
chaseCam.setMaxDistance(40); 最大缩放距离。默认值为40f。
chaseCam.setMinDistance(1); 最小缩放距离。默认值为1 f。
chaseCam.setMinVerticalRotation(-FastMath.PI/2); 相机围绕目标的最小垂直旋转角度。默认值为0。
chaseCam.setDefaultVerticalRotation(-FastMath.PI/2); 在应用程序开始时,相机围绕目标的默认垂直旋转角度。
chaseCam.setDefaultHorizontalRotation(-FastMath.PI/2); 在应用程序开始时,相机围绕目标的默认水平旋转角度。

要禁用鼠标旋转和缩放追逐相机,您可以使用以下方法。

//to disable rotation
inputManager.deleteMapping(CameraInput.CHASECAM_TOGGLEROTATE);
//to disable zoom out
inputManager.deleteMapping(CameraInput.CHASECAM_ZOOMOUT);
//to disable zoom in
inputManager.deleteMapping(CameraInput.CHASECAM_ZOOMIN);

代码示例:

import com.jme3.app.SimpleApplication;
import com.jme3.input.ChaseCamera;
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.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.RectangleMesh;

public class TestChaseCamera extends SimpleApplication implements AnalogListener, ActionListener {

	private Geometry teaGeom;

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

	@Override
	public void simpleInitApp() {
		// 加载一个茶壶模型
		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);

		// 加载一个地板模型
		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 RectangleMesh(new Vector3f(-25, -1, 25), new Vector3f(25, -1, 25), new Vector3f(-25, -1, -25)));
		ground.setMaterial(mat_ground);
		rootNode.attachChild(ground);

		// 禁用默认的第一人称视觉相机
		flyCam.setEnabled(false);

		// 启用追踪相机
		ChaseCamera chaseCam = new ChaseCamera(cam, teaGeom, inputManager);
		// 反转相机的垂直旋转轴
		// chaseCam.setInvertVerticalAxis(true);
		// 反转相机的水平旋转轴
		// chaseCam.setInvertHorizontalAxis(true);

		// 开启/禁用平滑的摄像头运动
		chaseCam.setSmoothMotion(true);

		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");
	}

	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		if (name.equals("displayPosition") && isPressed) {
			teaGeom.move(10, 10, 10);
		}
	}

	@Override
	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);
		}
	}
}

将摄像机绑定到角色上

是相机跟随角色的另外一种实现方式为使用 CameraNode,将相机绑定到角色上。可以将此 CameraNode 代码添加到 init 方法(例如 simpleInitApp ())。

//禁用flyCam
flyCam.setEnabled(false);
//创建相机节点
camNode = new CameraNode("Camera Node", cam);
//这种模式意味着摄像机会拷贝目标的动作:
camNode.setControlDir(ControlDirection.SpatialToCamera);
//将 camNode 附加到目标:
target.attachChild(camNode);
//调整 camNode 相对于目标的位置,使其可以看清目标
camNode.setLocalTranslation(new Vector3f(0, 5, -5));
//使其相机看向目标
camNode.lookAt(target.getLocalTranslation(), Vector3f.UNIT_Y);

如上代码显示, camNode.setLocalTranslation (new Vector3f (0,5,-5); ,必须为相机提供自己的起始位置。这取决于你的目标(玩家角色)的大小和它在特定场景中的位置。最理想的情况是,你把它设置在目标后面和上面一点的位置。

import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.*;
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.ControlDirection;
import com.jme3.scene.shape.RectangleMesh;
import com.jme3.system.AppSettings;

/**
 * 第三人称摄像机节点跟随目标(茶壶)。用 WASD 键跟随茶壶,通过拖动鼠标旋转。
 */
public class TestCameraNode extends SimpleApplication implements AnalogListener, ActionListener {

  private Node teaNode;
  private boolean rotate = false;
  final private Vector3f direction = new Vector3f();

  public static void main(String[] args) {
    TestCameraNode app = new TestCameraNode();
    AppSettings s = new AppSettings(true);
    s.setFrameRate(100);
    app.setSettings(s);
    app.start();
  }

  @Override
  public void simpleInitApp() {
    // 加载一个茶壶模型
    Geometry teaGeom 
            = (Geometry) assetManager.loadModel("Models/Teapot/Teapot.obj");
    Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
    teaGeom.setMaterial(mat);
		
    //创建一个节点来连接几何体和摄像头节点
    teaNode = new Node("teaNode");
    teaNode.attachChild(teaGeom);
    rootNode.attachChild(teaNode);
		
    // 创建地板
    mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
    Geometry ground = new Geometry("ground", new RectangleMesh(
            new Vector3f(-25, -1, 25),
            new Vector3f(25, -1, 25),
            new Vector3f(-25, -1, -25)));
    ground.setMaterial(mat);
    rootNode.attachChild(ground);

    // 创建相机节点
    CameraNode camNode = new CameraNode("CamNode", cam);
    // 设置方向为 SpatialToCamera,这意味着摄像机将复制节点的移动。
    camNode.setControlDir(ControlDirection.SpatialToCamera);
    // 将 camNode 附加到 teaNode
    teaNode.attachChild(camNode);
    // 设置位置,使其稍微远离茶壶节点
    camNode.setLocalTranslation(new Vector3f(-10, 0, 0));
    // 设置 camNode 看向 teaNode
    camNode.lookAt(teaNode.getLocalTranslation(), Vector3f.UNIT_Y);

    //禁用默认的第一人称相机
    flyCam.setEnabled(false);

    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("toggleRotate", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addMapping("rotateRight", new MouseAxisTrigger(MouseInput.AXIS_X, true));
    inputManager.addMapping("rotateLeft", new MouseAxisTrigger(MouseInput.AXIS_X, false));
    inputManager.addListener(this, "moveForward", "moveBackward", "moveRight", "moveLeft");
    inputManager.addListener(this, "rotateRight", "rotateLeft", "toggleRotate");
  }

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

  }

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