Java 3D LWJGL GitBook 〜カメラChapter08:カメラを動かすとは〜

カメラ

この章では、レンダリングされた 3D シーン内を移動する方法を学習します。この機能は、3D ワールド内を移動できるカメラを持つようなものであり、実際、3D ワールドを参照するために使用される用語です。

参照するドキュメントとソースは以下になります。

カメラ紹介

OpenGL で特定のカメラ機能を検索しようとすると、カメラの概念がないことがわかります。つまり、カメラは常に固定されており、画面の中心にある (0, 0, 0) の位置に中心があります。そこで、3D シーン内を移動できるカメラがあるかのような印象を与えるシミュレーションを行います。どうすればこれを達成できますか? カメラを移動できない場合は、3D 空間に含まれるすべてのオブジェクトを一度に移動する必要があります。つまり、カメラを動かすことができなければ、世界全体を動かすことになります。

したがって、カメラの位置を z 軸に沿って開始位置 (Cx、Cy、Cz) から位置 (Cx、Cy、Cz+dz) に移動して、座標 (Ox、Oy、Oz)。

<カメラの概念がある場合> => カメラを動かす

実際に行うことは、オブジェクト (実際には 3D 空間内のすべてのオブジェクト) を、カメラが移動する方向とは反対の方向に移動することです。トレッドミルに置かれているオブジェクトのように考えてください。

<カメラの概念がない場合> => 世界を動かす

カメラは 3 つの軸 (x、y、z) に沿って変位することができ、それらに沿って回転することもできます (ロール、ピッチ、ヨー)。

ロール(X軸)、ピッチ(Y軸)、ヨー(Z軸)

※参考サイト:3次元ベクトルのロール、ピッチ、ヨー=(roll, pitch and yaw).
 上記リンクのサイトに以下のような図がありました。
ロール(X軸)、ピッチ(Y軸)、ヨー(Z軸)

そして、右手と、左手の2種類あるようです。親指(X軸), 人差し指(Y軸), 中指(Z軸)の形です。
なので、上記の画像は「右手」タイプのものになりマス。

右手 左手

したがって、基本的に私たちがしなければならないことは、3D ワールドのすべてのオブジェクトを移動および回転できるようにすることです。これをどのように行うのですか?答えは、すべてのオブジェクトのすべての頂点をカメラの動きの反対方向に移動し、カメラの回転に従ってそれらを回転させる別の変換を適用することです。もちろん、これは別のマトリックス、いわゆるビューマトリックスで行われます。このマトリックスは、最初に平行移動を実行し、次に軸に沿って回転を実行します。

その行列を構築する方法を見てみましょう。変換の章を覚えているなら、私たちの変換方程式は次のようでした:

射影行列を乗算する前にビュー行列を適用する必要があるため、式は次のようになります。

成すべきこと

3D ワールドのすべてのオブジェクトを移動および回転できるようにすること
つまり、「すべてのオブジェクトのすべての頂点をカメラの動きの反対方向に移動し、カメラの回転に従ってそれらを回転させる別の変換を適用することです。」
具体的な方法としては、頂点座標を持っているオブジェクトにマトリックス(行列)を掛け算などして、移動・回転するということです。
その計算方法として、上記の計算式を使用できます。

実装部分

今回は、カメラとマウスの入力を追加します。

カメラの実装

それでは、カメラをサポートするようにコードを変更してみましょう。Camera最初に、カメラの位置と回転状態、およびそのビュー マトリックスを保持する、という名前の新しいクラスを作成します。クラスは次のように定義されます。

この章で、Cameraクラスが追加されます。カメラを動かすときの処理(マトリックスの計算処理)が実装されています。
※ ビュー行列は「ワールドに置いたあらゆる物をカメラの世界に移動させる行列」(参考サイト)

package org.lwjglb.engine.scene;

import org.joml.*;

public class Camera {

    private Vector3f direction;
    private Vector3f position;
    private Vector3f right;
    private Vector2f rotation;
    private Vector3f up;
    private Matrix4f viewMatrix;

    public Camera() {
        direction = new Vector3f();
        right = new Vector3f();
        up = new Vector3f();
        position = new Vector3f();
        viewMatrix = new Matrix4f();
        rotation = new Vector2f();
    }

    public void addRotation(float x, float y) {
        rotation.add(x, y);
        recalculate();
    }

    public Vector3f getPosition() {
        return position;
    }

    public Matrix4f getViewMatrix() {
        return viewMatrix;
    }

    public void moveBackwards(float inc) {
        viewMatrix.positiveZ(direction).negate().mul(inc);
        position.sub(direction);
        recalculate();
    }

    public void moveDown(float inc) {
        viewMatrix.positiveY(up).mul(inc);
        position.sub(up);
        recalculate();
    }

    public void moveForward(float inc) {
        viewMatrix.positiveZ(direction).negate().mul(inc);
        position.add(direction);
        recalculate();
    }

    public void moveLeft(float inc) {
        viewMatrix.positiveX(right).mul(inc);
        position.sub(right);
        recalculate();
    }

    public void moveRight(float inc) {
        viewMatrix.positiveX(right).mul(inc);
        position.add(right);
        recalculate();
    }

    public void moveUp(float inc) {
        viewMatrix.positiveY(up).mul(inc);
        position.add(up);
        recalculate();
    }

    private void recalculate() {
        viewMatrix.identity()
                .rotateX(rotation.x)
                .rotateY(rotation.y)
                .translate(-position.x, -position.y, -position.z);
    }

    public void setPosition(float x, float y, float z) {
        position.set(x, y, z);
        recalculate();
    }

    public void setRotation(float x, float y) {
        rotation.set(x, y);
        recalculate();
    }
}

ご覧のとおり、回転と位置に加えて、前方上方向と右方向を定義するいくつかのベクトルを定義します。これは、自由空間移動カメラを実装しているためです。前方に移動したい場合にカメラを回転させると、定義済みの軸ではなく、カメラが指している場所に移動したいだけです。次の位置がどこに配置されるかを計算するには、これらのベクトルを取得する必要があります。そして最後に、カメラの状態が 4x4 マトリックス (ビュー マトリックス) に格納されるため、位置や回転を変更するたびに更新する必要があります。ご覧のとおり、ビュー マトリックスを更新するときは、最初に回転を行い、次に平行移動を行う必要があります。逆にすると、カメラの位置に沿って回転するのではなく、座標の原点に沿って回転します。

このCameraクラスは、前方、上、または右に移動するときに位置を更新するメソッドも提供します。これらのメソッドでは、ビュー マトリックスを使用して、現在の状態に応じて前方、上、または右のメソッドがどこにあるべきかを計算し、それに応じて位置を増やします。コードを非常にシンプルに保ちながら、これらの計算に素晴らしい JOML ライブラリを使用します。

JOMOLライブラリは、以下のクラスのこと

  • org.joml.Vector2f;

他にもありますが、つまりは「org.joml.*;」のクラスということです。これらのJOMOLライブラリクラス群を使用することが、ライブラリを使用するということです。まぁ、そのままですね。。。

カメラの使用(呼び出し元)

Cameraクラスにインスタンスを保存するSceneので、変更に進みましょう。

public class Scene {
    ...
    private Camera camera;
    ...
    public Scene(int width, int height) {
        ...
        camera = new Camera();
    }
    ...
    public Camera getCamera() {
        return camera;
    }
    ...
}

MouseInput

マウスでカメラを操作できるといいですね。そのために、マウス イベントを処理する新しいクラスを作成し、それらを使用してカメラの回転を更新できるようにします。これがそのクラスのコードです。

package org.lwjglb.engine;

import org.joml.Vector2f;

import static org.lwjgl.glfw.GLFW.*;

public class MouseInput {

    private Vector2f currentPos;
    private Vector2f displVec;
    private boolean inWindow;
    private boolean leftButtonPressed;
    private Vector2f previousPos;
    private boolean rightButtonPressed;

    public MouseInput(long windowHandle) {
        previousPos = new Vector2f(-1, -1);
        currentPos = new Vector2f();
        displVec = new Vector2f();
        leftButtonPressed = false;
        rightButtonPressed = false;
        inWindow = false;

        glfwSetCursorPosCallback(windowHandle, (handle, xpos, ypos) -> {
            currentPos.x = (float) xpos;
            currentPos.y = (float) ypos;
        });
        glfwSetCursorEnterCallback(windowHandle, (handle, entered) -> inWindow = entered);
        glfwSetMouseButtonCallback(windowHandle, (handle, button, action, mode) -> {
            leftButtonPressed = button == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS;
            rightButtonPressed = button == GLFW_MOUSE_BUTTON_2 && action == GLFW_PRESS;
        });
    }

    public Vector2f getCurrentPos() {
        return currentPos;
    }

    public Vector2f getDisplVec() {
        return displVec;
    }

    public void input() {
        displVec.x = 0;
        displVec.y = 0;
        if (previousPos.x > 0 && previousPos.y > 0 && inWindow) {
            double deltax = currentPos.x - previousPos.x;
            double deltay = currentPos.y - previousPos.y;
            boolean rotateX = deltax != 0;
            boolean rotateY = deltay != 0;
            if (rotateX) {
                displVec.y = (float) deltax;
            }
            if (rotateY) {
                displVec.x = (float) deltay;
            }
        }
        previousPos.x = currentPos.x;
        previousPos.y = currentPos.y;
    }

    public boolean isLeftButtonPressed() {
        return leftButtonPressed;
    }

    public boolean isRightButtonPressed() {
        return rightButtonPressed;
    }
}

このMouseInputクラスは、そのコンストラクターで、マウス イベントを処理するための一連のコールバックを登録します。

・glfwSetCursorPosCallback: マウスが移動したときに呼び出されるコールバックを登録します。
・glfwSetCursorEnterCallback: マウスがウィンドウに入ったときに呼び出されるコールバックを登録します。マウスがウィンドウ内にない場合でも、マウス イベントを受け取ります。このコールバックを使用して、マウスがウィンドウ内にあるときを追跡します。
・glfwSetMouseButtonCallback: マウス ボタンが押されたときに呼び出されるコールバックを登録します。
このMouseInputクラスは、ゲーム入力が処理されるときに呼び出される入力メソッドを提供します。このメソッドは、前の位置からのマウスの変位を計算し、それをdisplVec変数に格納して、ゲームで使用できるようにします。

クラスはMouseInputクラスでインスタンス化され、Windowそのインスタンスを返すゲッターも提供されます。また、イベントがポーリングされるたびに入力メソッドを呼び出します。

Windowクラスの追加

public class Window {
    ...
    private MouseInput mouseInput;
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        ...
        mouseInput = new MouseInput(windowHandle);
    }
    ...
    public MouseInput getMouseInput() {
        return mouseInput;
    }
    ...    
    public void pollEvents() {
        ...
        mouseInput.input();
    }
    ...
}

これで、 のビュー マトリックスを使用するように頂点シェーダーを変更できます。これは、ご想像のとおりCamera、uniform として渡されます。

<scene.vert>

#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec2 texCoord;

out vec2 outTextCoord;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

void main()
{
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    outTextCoord = texCoord;
}

SceneRender

したがって、次のステップは、クラスでユニフォームを適切に作成し、各呼び出しSceneRenderでその値を更新することです。render

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("viewMatrix");
        ...
    }    
    ...
    public void render(Scene scene) {
        ...
        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        uniformsMap.setUniform("viewMatrix", scene.getCamera().getViewMatrix());
        ...
    }
}

Main

以上で、基本コードはカメラの概念をサポートします。今、それを使用する必要があります。入力の処理方法を変更して、カメラを更新できます。次のコントロールを設定します。

・キー「A」と「D」は、カメラをそれぞれ左と右 (x 軸) に移動します。
・「W」キーと「S」キーは、それぞれカメラを前後 (z 軸) に移動します。
・「Z」キーと「X」キーは、それぞれカメラを上下 (y 軸) に移動します。
マウスの右ボタンが押されたときに、マウスの位置を使用して x 軸と y 軸に沿ってカメラを回転させます。

Mainこれで、クラスを更新してキーボードとマウスの入力を処理する準備が整いました。

public class Main implements IAppLogic {

    private static final float MOUSE_SENSITIVITY = 0.1f;
    private static final float MOVEMENT_SPEED = 0.005f;
    ...

    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-08", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void input(Window window, Scene scene, long diffTimeMillis) {
        float move = diffTimeMillis * MOVEMENT_SPEED;
        Camera camera = scene.getCamera();
        if (window.isKeyPressed(GLFW_KEY_W)) {
            camera.moveForward(move);
        } else if (window.isKeyPressed(GLFW_KEY_S)) {
            camera.moveBackwards(move);
        }
        if (window.isKeyPressed(GLFW_KEY_A)) {
            camera.moveLeft(move);
        } else if (window.isKeyPressed(GLFW_KEY_D)) {
            camera.moveRight(move);
        }
        if (window.isKeyPressed(GLFW_KEY_UP)) {
            camera.moveUp(move);
        } else if (window.isKeyPressed(GLFW_KEY_DOWN)) {
            camera.moveDown(move);
        }

        MouseInput mouseInput = window.getMouseInput();
        if (mouseInput.isRightButtonPressed()) {
            Vector2f displVec = mouseInput.getDisplVec();
            camera.addRotation((float) Math.toRadians(-displVec.x * MOUSE_SENSITIVITY),
                    (float) Math.toRadians(-displVec.y * MOUSE_SENSITIVITY));
        }
    }

UML(クラス図)

プログラムの実行

プログラムを実行してみました。

<<<前回 次回 >>>

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す