Java 3D LWJGL GitBook 〜パースペクティブ(遠近法)Chapter05〜

パースペクティブ(遠近法)

参照するドキュメントには次のように書いてあります(Google翻訳)

この章では、透視投影 (遠くにあるオブジェクトを近くのものよりも小さくレンダリングする) とユニフォーム (シェーダーに追加データを渡すためのバッファーのような構造) という 2 つの重要な概念を学習します。

透視投影

まずは、「透視投影」の意味から理解します。
Wikiによると次の通りです。イメージは以下

透視投影(とうしとうえい、英: perspective projection)は3次元物体を2次元平面に描画する図法(投影法)の一種である[1]。中心投影ともいう[2]。視点を設定して投影図を得るため、対象物を目で見た像と近い表現が得られるという特徴をもつ[3]。透視投影で得られた図は「透視図」あるいは英語の perspectiveから「パース」などと呼ばれる。

前の章で作成した素敵な色のクワッドに戻りましょう。注意深く見ると、四角形が歪んでいて長方形に見えることがわかります。ウィンドウの幅を 600 ピクセルから 900 ピクセルに変更することもでき、歪みがより明確になります。ここで何が起こっているのですか?

頂点シェーダー コードを再確認すると、座標を直接渡しているだけです。つまり、頂点の座標 x の値が 0.5 であると言うとき、OpenGL に対して、画面上の x 位置 0.5 に描画するように指示していることになります。次の図は、OpenGL 座標を示しています (x 軸と y 軸のみ)。

これらの座標は、ウィンドウ サイズを考慮して、ウィンドウ座標 (前の図の左上隅に原点がある) にマップされます。したがって、ウィンドウのサイズが 900x580 の場合、OpenGL 座標 (1,0) は座標 (900, 0) にマップされ、四角形ではなく四角形が作成されます。

つまりは、正四角形と長方形がウィンドウサイズに依存して描画されるということが起こっています。

しかし、問題はそれよりも深刻です。クワッドの z 座標を 0.0 から 1.0 および -1.0 に変更します。何が見えますか?四角形は、z 軸に沿って変位しても、正確に同じ場所に描画されます。なぜこうなった?遠くにあるオブジェクトは、近くにあるオブジェクトよりも小さく描画する必要があります。ただし、同じ x 座標と y 座標で描画しています。

ちょっと待って。これは z 座標で処理すべきではありませんか? 答えはイエスとノーです。z 座標は OpenGL にオブジェクトが近づいているか離れているかを伝えますが、OpenGL はオブジェクトのサイズについては何も知りません。サイズの異なる 2 つのオブジェクト (1 つは近くて小さく、もう 1 つは大きくて遠い) を同じサイズで画面に正しく投影できます (これらの x 座標と y 座標は同じですが、z は異なります)。OpenGL は渡された座標を使用するだけなので、これに注意する必要があります。座標を正しく投影する必要があります。

問題を診断したので、どのように修正しますか? 答えは、透視投影マトリックスを使用することです。透視投影マトリックスは、オブジェクトが歪まないように、描画領域の縦横比 (サイズと高さの関係) を処理します。また、距離も処理するため、遠くにあるオブジェクトは小さく描画されます。射影行列は、視野と表示される最大距離も考慮します。

行列に慣れていない方のために説明すると、行列は、列と行に配置された数値の 2 次元配列です。行列内の各数値は要素と呼ばれます。行列の順序は、行と列の数です。たとえば、ここでは 2x2 マトリックス (2 行 2 列) を確認できます。

上記に「答えは、透視投影マトリックスを使用することです。透視投影マトリックスは、オブジェクトが歪まないように、描画領域の縦横比 (サイズと高さの関係) を処理します。」とあるので、ここを考えてみました。つまりは縦横比が歪まないようにしてやればよいという理解をしました。

行列には、数学の本で参照できる、行列に適用できる基本演算 (加算、乗算など) がいくつかあります。3D グラフィックスに関連する行列の主な特徴は、空間内の点を変換するのに非常に役立つことです。

射影行列は、視野と最小距離と最大距離を持つカメラと考えることができます。そのカメラの視野領域は、角錐台から取得されます。次の図は、その領域を上から見た図です。

射影行列は 3D 座標を正しくマッピングするため、2D 画面で正しく表現できます。その行列の数学的表現は次のとおりです (怖がらないでください)。

アスペクト比は、画面の幅と画面の高さの関係です ($$a=幅/高さ$$)。特定の点の投影座標を取得するには、投影行列に元の座標を掛けるだけです。結果は、投影されたバージョンを含む別のベクトルになります。

そのため、ベクトルや行列などの一連の数学的エンティティを処理し、それらに対して実行できる操作を含める必要があります。すべてのコードをゼロから独自に作成するか、既存のライブラリを使用するかを選択できます。簡単なパスを選択し、JOML (Java OpenGL Math Library) と呼ばれる LWJGL で数学演算を処理するための特定のライブラリを使用します。そのライブラリを使用するには、別の依存関係をpom.xmlファイルに追加するだけです。

JOMOLの追加

        <dependency>
            <groupId>org.joml</groupId>
            <artifactId>joml</artifactId>
            <version>${joml.version}</version>
        </dependency>

すべてが設定されたので、射影行列を定義しましょう。Projection次のように定義された名前の新しいクラスを作成します。

つまり、ライブラリ「JOMOL」を使用して細かい計算用のクラス群を使いましょうという理解です。
そして、その実装は「Projection」クラスに集約(担当)させる形で実装します。

package org.lwjglb.engine.scene;

import org.joml.Matrix4f;

public class Projection {

    private static final float FOV = (float) Math.toRadians(60.0f);
    private static final float Z_FAR = 1000.f;
    private static final float Z_NEAR = 0.01f;

    private Matrix4f projMatrix;

    public Projection(int width, int height) {
        projMatrix = new Matrix4f();
        updateProjMatrix(width, height);
    }

    public Matrix4f getProjMatrix() {
        return projMatrix;
    }

    public void updateProjMatrix(int width, int height) {
        projMatrix.setPerspective(FOV, (float) width / height, Z_NEAR, Z_FAR);
    }
}

ご覧のとおり、これMatrix4fは、という名前の透視投影行列を設定するメソッドを提供するクラス (JOML ライブラリによって提供される)に依存していますsetPerspective。このメソッドには、次のパラメーターが必要です。

視野: ラジアン単位の視野角。FOVそのために定数を使用するだけです
アスペクト比: つまり、レンダリングの幅と高さの関係です。
近平面までの距離 (z-near)
遠方平面までの距離 (z-far)。
Projectionクラスインスタンスをクラスに格納Sceneし、コンストラクターで初期化します。それに加えて、ウィンドウのサイズが変更された場合に注意する必要があるため、ウィンドウのサイズが変更されたときに透視投影行列を再計算するSceneという名前の新しいメソッドをそのクラスに提供します。resize

ここは、resize()というメソッドをSceneクラスに追加します。という意味でした。細かいところはそのまま修正します。

public class Scene {
    ...
    private Projection projection;

    public Scene(int width, int height) {
        ...
        projection = new Projection(width, height);
    }
    ...
    public Projection getProjection() {
        return projection;
    }

    public void resize(int width, int height) {
        projection.updateProjMatrix(width, height);
    }    
}

Engineクラスを更新して、コンストラクターのパラメーターに適応させ、メソッドSceneを呼び出す必要もあります。

public class Engine {
    ...
    public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
        ...
        scene = new Scene(window.getWidth(), window.getHeight());
        ...
    }
    ...
    private void resize() {
        scene.resize(window.getWidth(), window.getHeight());
    }
    ...
}

Projectionクラスのまとめ

ここまでの内容をまとめると、自分の解釈ですが、次のようになりました。
射影行列を扱うために作成したProjectionクラスは、Sceneクラスに依存する(フィールド変数にセットしている)ので

  1. リサイズ処理を行うときにJOMOLの部品である「Matrix4fクラス」の値を更新します。
  2. 次に作成するユニフォームクラスに「Matrix4fクラス」をセット、ユニフォームが射影行列を計算できるようにします。

ユニフォーム

透視投影行列を計算するためのインフラストラクチャができたので、それをどのように使用するのでしょうか? これをシェーダーで使用する必要があり、すべての頂点に適用する必要があります。最初は、頂点入力 (座標や色など) にバンドルすることを考えることができます。この場合、射影行列はすべての頂点に共通であるため、多くのスペースが無駄になります。Java コードで頂点に行列を掛けることも考えられます。しかし、そうなると、VBO は役に立たなくなり、グラフィックス カードで利用可能な処理能力を使用できなくなります。

答えは「ユニフォーム」を使うことです。Uniforms は、シェーダーが使用できるグローバル GLSL 変数であり、すべての要素またはモデルに共通のデータを渡すために使用します。それでは、ユニフォームが din シェーダー プログラムを使用する方法から始めましょう。頂点シェーダー コードを変更し、呼び出された新しいユニフォームを宣言し、projectionMatrixそれを使用して投影位置を計算する必要があります。

#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec3 color;

out vec3 outColor;

uniform mat4 projectionMatrix;

void main()
{
    gl_Position = projectionMatrix * vec4(position, 1.0);
    outColor = color;
}

ご覧のとおりprojectionMatrix、4x4 行列として定義し、元の座標を乗算して位置を取得します。次に、射影行列の値をシェーダーに渡す必要があります。という名前の新しいクラスUniformMapを作成します。これにより、ユニフォームへの参照を作成し、その値を設定できます。次のように始まります。

package org.lwjglb.engine.graph;

import org.joml.Matrix4f;
import org.lwjgl.system.MemoryStack;

import java.util.*;

import static org.lwjgl.opengl.GL20.*;

public class UniformsMap {

    private int programId;
    private Map<String, Integer> uniforms;

    public UniformsMap(int programId) {
        this.programId = programId;
        uniforms = new HashMap<>();
    }

    public void createUniform(String uniformName) {
        int uniformLocation = glGetUniformLocation(programId, uniformName);
        if (uniformLocation < 0) {
            throw new RuntimeException("Could not find uniform [" + uniformName + "] in shader program [" +
                    programId + "]");
        }
        uniforms.put(uniformName, uniformLocation);
    }
    ...
}

ご覧のとおり、コンストラクターはシェーダー プログラムの識別子を受け取り、メソッドで作成されるユニフォームへMapの参照 (インスタンス) を格納する を定義します。Uniforms 参照は、次の2 つのパラメーターを受け取る関数を呼び出すことによって取得されます。IntegercreateUniformglGetUniformLocation

シェーダー プログラムの識別子。
ユニフォームの名前 (シェーダー コードで定義されているものと一致する必要があります)。
ご覧のとおり、ユニフォームの作成はそれに関連付けられているデータ型に依存しません。そのユニフォームのデータを設定したい場合、さまざまなタイプに個別のメソッドが必要になります。ここまでで、必要なのは 4x4 マトリックスをロードするメソッドだけです。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, Matrix4f value) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            Integer location = uniforms.get(uniformName);
            if (location == null) {
                throw new RuntimeException("Could not find uniform [" + uniformName + "]");
            }
            glUniformMatrix4fv(location.intValue(), false, value.get(stack.mallocFloat(16)));
        }
    }
}

SceneRenderこれで、クラスで上記のコードを使用できます。

public class SceneRender {
    ...
    private UniformsMap uniformsMap;

    public SceneRender() {
        ...
        createUniforms();
    }
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("projectionMatrix");
    }
    ...
    public void render(Scene scene) {
        ...
        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        ...
    }
}

ユニフォームのまとめ

射影行列、4 x 4マトリックスを計算するためシェーダープログラムと連携させるためのクラスがUniformsMapクラスでそれを
SceneRenderクラスで使用している。全体的には

  1. Projectionクラスが射影行列を保持する役目
  2. UniformsMapクラスがシェーダープログラムと連携する役目

メインメソッド

ほぼ完了です。正しくレンダリングされたクワッドを表示できるようになったので、プログラムを起動すると、色付きのクワッドのない黒い背景が得られます。何が起こっていますか?私たちは何かを壊しましたか?まあ、実際にはありません。シーンを見ているカメラの効果をシミュレートしていることを思い出してください。1 つは最も遠い平面 (1000f に等しい) で、もう 1 つは最も近い平面 (0.01f に等しい) です。座標は次のとおりです。

float[] positions = new float[]{
    -0.5f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.5f,  0.5f, 0.0f,
};

つまり、z 座標は可視ゾーンの外にあります。の値を割り当てましょう-0.05f。次のような巨大な正方形が表示されます。

現在起こっていることは、クワッドをカメラに近づけすぎていることです。実際にズームインしています。ここで z 座標にの値を割り当てると-1.0f、色付きのクワッドが表示されます。

public class Main implements IAppLogic {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-05", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        float[] positions = new float[]{
                -0.5f, 0.5f, -1.0f,
                -0.5f, -0.5f, -1.0f,
                0.5f, -0.5f, -1.0f,
                0.5f, 0.5f, -1.0f,
        };
        ...
    }
    ...
}

まとめ

  1. 追加したクラスは「Projectionクラス」「UniformsMapクラス」の二つ
  2. 透視投影行列を計算するためのProjectingクラス、JOMOLのMatrix4fクラス(Projectionクラスで使用している)
    ※この処理で、画面サイズに左右されずにちゃんと描画できる
  3. 「透視投影行列を計算するためのインフラ」=> Projectionクラスを使用するのはシェーダーで、4x4 行列を定義、UniformMapでシェーダーに追加したユニフォームの値を処理する

クラス図

<<<前回 次回 >>>

投稿者:

takunoji

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

コメントを残す