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

这样改过之后,旋转就正常了。

总结

目标达成。