Java软光栅渲染器-摄像机控制器
目标
为了进一步确认变换矩阵的作用,我需要一个摄像机控制器,用来操纵摄像机在场景中运动。
- 记录用户输入
- 按AD键,摄像机左右运动
- 按WS键,摄像机前后运动
- 按QZ键,摄像机上下运动
- 按左右方向键,摄像机绕Y轴左右旋转
- 按上下方向键,摄像机绕X轴上下旋转
- 鼠标拖拽旋转摄像机
主要源代码:
实现
调整摄像机姿态
rotate
在Camera类中,添加使摄像机按欧拉角旋转的方法。这个方法的主要作用是改变相机的观察方向。
/**
* 使摄像机按欧拉角旋转(弧度制)
* @param xAngle
* @param yAngle
* @param zAngle
*/
public void rotate(float xAngle, float yAngle, float zAngle) {
// 计算旋转后的uvn系统
Quaternion rot = new Quaternion().fromAngles(xAngle, yAngle, zAngle);
// 计算旋转后的视线方向
rot.multLocal(direction);
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
在编写数学库时,我没有实现四元数的欧拉角计算,现在需要在Quaternion中补上下面这个方法,它实质上等同于三个四元数相乘。
/**
* 欧拉角旋转(弧度制)
* @param xAngle
* @param yAngle
* @param zAngle
* @return
*/
public Quaternion fromAngles(float xAngle, float yAngle, float zAngle) {
float angle;
float sinY, sinZ, sinX, cosY, cosZ, cosX;
angle = zAngle * 0.5f;
sinZ = (float)Math.sin(angle);
cosZ = (float)Math.cos(angle);
angle = yAngle * 0.5f;
sinY = (float)Math.sin(angle);
cosY = (float)Math.cos(angle);
angle = xAngle * 0.5f;
sinX = (float)Math.sin(angle);
cosX = (float)Math.cos(angle);
// variables used to reduce multiplication calls.
float cosYXcosZ = cosY * cosZ;
float sinYXsinZ = sinY * sinZ;
float cosYXsinZ = cosY * sinZ;
float sinYXcosZ = sinY * cosZ;
w = (cosYXcosZ * cosX - sinYXsinZ * sinX);
x = (cosYXcosZ * sinX + sinYXsinZ * cosX);
y = (sinYXcosZ * cosX + cosYXsinZ * sinX);
z = (cosYXsinZ * cosX - sinYXcosZ * sinX);
normalizeLocal();
return this;
}
lookAt
lookAt方法可以让摄像机“凝视”指定的坐标。根据target和location可以计算出direction,然后重新计算viewMatrix即可。
/**
* 使摄像机观察指定位置
* @param target
* @param up
*/
public void lookAt(Vector3f target, Vector3f up) {
target.subtract(location, direction);
direction.normalizeLocal();
this.up.set(up);
this.up.normalizeLocal();
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
/**
* 使摄像机观察指定位置
* @param location
* @param target
* @param up
*/
public void lookAt(Vector3f location, Vector3f target, Vector3f up) {
this.location.set(location);
target.subtract(location, direction);
this.direction.normalizeLocal();
this.up.set(up);
this.up.normalizeLocal();
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
lookAtDirection
lookAtDirection方法更直白,根据用户设置的direction来更新viewMatrix。
/**
* 使摄像机观察指定方向
* @param direction
* @param up
*/
public void lookAtDirection(Vector3f direction, Vector3f up) {
this.direction.set(direction);
this.direction.normalizeLocal();
this.up.set(up);
this.up.normalizeLocal();
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
/**
* 使摄像机观察指定方向
* @param location
* @param direction
* @param up
*/
public void lookAtDirection(Vector3f location, Vector3f direction, Vector3f up) {
this.location.set(location);
this.direction.set(direction);
this.direction.normalizeLocal();
this.up.set(up);
this.up.normalizeLocal();
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
记录用户的输入
定义一个Input类,用一个boolean数组来记录用户的按键:按下状态为true,释放状态为false。
为了实现鼠标拖拽,鼠标的按键状态以及光标的位置也要记录下来。使用另一个boolean数组来记录鼠标的按键状态,并用Vector2f变量来存储光标在屏幕上的位置。
package net.jmecn;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import net.jmecn.math.Vector2f;
/**
* 用户输入
*
* @author yanmaoyuan
*
*/
public class Input implements KeyListener, MouseListener, MouseMotionListener {
private boolean[] keys = new boolean[65536];
private boolean[] mouse = new boolean[4];
private Vector2f start = new Vector2f();
private Vector2f current = new Vector2f();
private Vector2f delta = new Vector2f();
public boolean getKey(int key) {
return keys[key];
}
/**
* 返回鼠标的按键状态
* @param button
* @return
*/
public boolean getMouseButton(int button) {
return mouse[button];
}
/**
* 获得鼠标的起点坐标
* @return
*/
public Vector2f getStart() {
return start;
}
/**
* 获得鼠标的当前坐标
* @return
*/
public Vector2f getCurrent() {
return current;
}
/**
* 返回鼠标的相对位移
* @return
*/
public Vector2f getDelta() {
return delta;
}
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if (code > 0 && code < keys.length)
keys[code] = true;
}
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
if (code > 0 && code < keys.length)
keys[code] = false;
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
current.set(e.getX(), e.getY());
current.subtract(start, delta);
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mousePressed(MouseEvent e) {
mouse[e.getButton()] = true;
start.set(e.getX(), e.getY());
current.set(e.getX(), e.getY());
delta.set(0, 0);
}
@Override
public void mouseReleased(MouseEvent e) {
mouse[e.getButton()] = false;
}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}
在Screen类中声明这个Input成员,并将其注册到Canvas的监听器队列中,并对外提供getInput()方法。
/**
* 代表显示图像的窗口
*
* @author yanmaoyuan
*
*/
public class Screen {
// ...
// 用户输入
private Input input;
public Screen(int width, int height, String title) {
canvas = new Canvas();
// ...
input = new Input();
canvas.addKeyListener(input);
canvas.addMouseListener(input);
canvas.addMouseMotionListener(input);
// ...
}
/**
* 获得用户输入
* @return
*/
public Input getInput() {
return input;
}
}
控制器类
CameraController 类的主要作用,是根据Input中的输入改变camera的位置,然后重新计算观察变换矩阵。
package net.jmecn;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import net.jmecn.math.Vector2f;
import net.jmecn.math.Vector3f;
import net.jmecn.renderer.Camera;
/**
* 摄像机控制器
* @author yanmaoyuan
*
*/
public class CameraController {
private Camera camera;
private Input input;
// 运动速度
public float moveSpeed = 10f;
// 临时变量,用于计算移动的方向和距离。
private Vector3f step = new Vector3f();
private Vector3f forward = new Vector3f();
private Vector3f right = new Vector3f();
private Vector3f up = new Vector3f();
public CameraController(Camera camera, Input input) {
this.camera = camera;
this.input = input;
}
public void update(float delta) {
// 鼠标拖拽旋转摄像机
dragRotate();
// 按键旋转摄像机
keyRotate(delta);
// 按键移动摄像机
keyMove(delta);
}
// 记录鼠标的点击位置,用于计算鼠标在画布上的相对位移。
private Vector2f last = new Vector2f(-1, -1);
private Vector2f cur = new Vector2f();
// 鼠标灵敏度
private float sensivive = 0.003f;
/**
* 鼠标拖拽旋转摄像机
*/
private void dragRotate() {
if (input.getMouseButton(MouseEvent.BUTTON1)) {
// 首次按键
if (last.x == -1 && last.y == -1) {
last.set(input.getStart());
}
cur.set(input.getCurrent());
// 计算相对位移
float dx = cur.x - last.x;
float dy = cur.y - last.y;
if (dx*dx + dy*dy > 0) {
camera.rotate(-dy * sensivive, -dx * sensivive, 0);
last.set(cur);
}
} else {
last.set(-1, -1);
}
}
/**
* 按键旋转
* @param delta
*/
private void keyRotate(float delta) {
// 左右旋转
if (input.getKey(KeyEvent.VK_LEFT)) {
camera.rotate(0, delta, 0);
} else if (input.getKey(KeyEvent.VK_RIGHT)) {
camera.rotate(0, -delta, 0);
}
// 上下旋转
if (input.getKey(KeyEvent.VK_UP)) {
camera.rotate(delta, 0, 0);
} else if (input.getKey(KeyEvent.VK_DOWN)) {
camera.rotate(-delta, 0, 0);
}
}
/**
* QWASDZ移动
* @param delta
*/
private void keyMove(float delta) {
boolean changed = false;
step.set(0, 0, 0);
// 计算移动的方向
forward.set(camera.getDirection());
right.set(camera.getRightVector());
up.set(camera.getUpVector());
// 计算移动的距离
float movement = delta * moveSpeed;
forward.multLocal(movement);
right.multLocal(movement);
up.multLocal(movement);
// 前后平移
if (input.getKey(KeyEvent.VK_W)) {
step.addLocal(forward);
changed = true;
} else if (input.getKey(KeyEvent.VK_S)) {
step.subtractLocal(forward);
changed = true;
}
// 左右平移
if (input.getKey(KeyEvent.VK_A)) {
step.subtractLocal(right);
changed = true;
} else if (input.getKey(KeyEvent.VK_D)) {
step.addLocal(right);
changed = true;
}
// 上下平移
if (input.getKey(KeyEvent.VK_Z)) {
step.subtractLocal(up);
changed = true;
} else if (input.getKey(KeyEvent.VK_Q)) {
step.addLocal(up);
changed = true;
}
if (changed) {
// 更新摄像机位置
camera.getLocation().addLocal(step);
// 更新观察-投影矩阵
camera.updateViewProjectionMatrix();
}
}
}
使用控制器
在Application的start方法的主循环中,加入对摄像机控制器的调用。
/**
* 启动程序
*/
public void start() {
// ...
// 创建摄像机
camera = new Camera(width, height);
// 创建摄像机控制器
CameraController controller = new CameraController(camera, screen.getInput());// <---- 创建控制器
// 初始化
initialize();
while (isRunning) {
// 计算间隔时间..
// 更新FPS
updateFramePerSecond(delta);
controller.update(delta);// <---调用控制器
// 更新逻辑
update(delta);
// 更新画面
render(delta);
}
// ..
}
重新运行昨天编写的Test3DView程序,效果如下:
BUG
在测试该控制器时,我发现如果摄像机的姿态发生了改变,旋转的结果就不正确了。原因是计算摄像机旋转时,应该用uvn坐标系,而我却用了世界坐标系。
/**
* 使摄像机按欧拉角旋转(弧度制)
* @param xAngle
* @param yAngle
* @param zAngle
*/
public void rotate(float xAngle, float yAngle, float zAngle) {
// 计算旋转后的uvn系统
// 不能直接绕x、y、z轴旋转,而是应该绕uvn系统的三轴旋转。
//Quaternion rot = new Quaternion().fromAngles(xAngle, yAngle, zAngle);
Quaternion rot = new Quaternion(uAxis, xAngle);
rot.multLocal(new Quaternion(vAxis, yAngle));
rot.multLocal(new Quaternion(nAxis, zAngle));
// 计算旋转后的视线方向
rot.multLocal(direction);
direction.normalizeLocal();
updateViewMatrix();
projectionMatrix.mult(viewMatrix, viewProjectionMatrix);
}
这样改过之后,旋转就正常了。
总结
目标达成。