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でシェーダーに追加したユニフォームの値を処理する

クラス図

<<<前回 次回 >>>

Java 3D LWJGL GitBook 〜レンダリング詳細 Chapter04〜

レンダリングの詳細

前回は単純な三角形の描画を行いました。そこで、レンダリングの大まかな流れを理解し、プログラムで使用しているクラスの関連も見ました。

この章では、OpenGL がどのように物事をレンダリングするかについて話し続けます。
三角形の代わりに四角形を描画し、各頂点の色などの追加データをメッシュに設定します。

プログラムの構成

Chapter-04の処理をクラス図にして、解析したいと思います。ここでクラスの関係を理解し、処理の流れを理解しておくとよいと判断しました。
クラス図に説明を説明を加えてみました。

ポイントとしては、SceneクラスはMeshクラスを保持していて、SceneRenderクラスで描画するときにSceneクラスからMeshを取得しているところです。
<SceneRender#render()>

    public void render(Scene scene) {
        shaderProgram.bind();
        // ここがポイントの処理
        scene.getMeshMap().values().forEach(mesh -> {
                    glBindVertexArray(mesh.getVaoId());
                    glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
                }
        );

        glBindVertexArray(0);

        shaderProgram.unbind();
    }

レンダリングの確認

前回説明があった描画するのに、OpenGLでは三角形を複数描く事で、いろんな描画を実現しています。
四角形であれば、三角形を2回描けばよいです。

単純にこれを考えると、複雑な、例えばイルカのようなモデルを描画するとなると、重複する三角形が出てきます。
その例が下のコードです。

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,
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
}

四角を描くのであれば、二つの三角形の頂点データが必要になりますが、各頂点を描く時には、重複する部分が出てきてしまいます。
そこで、頂点と順序(Index)を示したデータがあれば、重複するのを防げます。

ドキュメントの記述では下のようになっています。

最終的には、重複した情報のためにさらに多くのメモリが必要になります。
しかし、大きな問題はこれではありません。最大の問題は、恥の頂点のためにシェーダーでプロセスを繰り返すことです。
ここで、インデックス バッファーが役に立ちます。四角形を描画するには、この方法で各頂点を一度だけ指定する必要があります: V1、V2、V3、V4)。
各頂点には、配列内の位置があります。V1 の位置は 0、V2 の位置は 1 などです。

プログラムの修正

chapter03のコードとchapter04のコードを比較します。変更点に注意しながら行います。
もともとの処理としては次の通りです。
Chapter03

  1. VAOのIDを生成して、作成するMeshクラス(インスタンス)で保持 ※なのでコンストラクタで処理を行っています。
  2. 位置VBOのIDを生成、VBOリストに追加 ※細かい処理は割愛します。
  3. SceneクラスでMeshクラスをマップで管理、この時にVAOをOpenGLにバインドして描画 ※呼び出し元での処理

Chapter04

  1. VAOのIDを生成して、作成するMeshクラス(インスタンス)で保持 ※なのでコンストラクタで処理を行っています。
  2. 位置VBOのIDを生成、VBOリストに追加 ※細かい処理は割愛します。
  3. 色VBOのIDを生成、VBOリストに追加、色バッファーをバッファーデータに追加 ※細かい処理は割愛します。
  4. インデックスVBOのIDを生成、VBOリストに追加、インデックスバッファーをバッファーデータに追加 ※細かい処理は割愛します。
  5. SceneクラスでMeshクラスをマップで管理、この時にVAOをOpenGLにバインドして描画 ※呼び出し元での処理

Meshクラス

コンストラクタ部分を抜き出します。見た目にコードの量が増えています。
単純にコードを追加した形となっています。具体的にはコメントの「// Color VBO」「// Index VBO」の部分です。

つまりは、位置VBOのみの登録しか行っていなかったが、他に色とインデックスVBOを追加したということです。
※コンストラクタの引数も変わっています。

Chapter03のコード

public class Mesh {
    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public Mesh(float[] positions, int numVertices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.numVertices = numVertices;
            vboIdList = new ArrayList<>();

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }
}

Chapter04のコード

public class Mesh {
    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public Mesh(float[] positions, float[] colors, int[] indices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            numVertices = indices.length;
            vboIdList = new ArrayList<>();

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            // Color VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer colorsBuffer = stack.callocFloat(colors.length);
            colorsBuffer.put(0, colors);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, colorsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

            // Index VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            IntBuffer indicesBuffer = stack.callocInt(indices.length);
            indicesBuffer.put(0, indices);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }
}

変更点の抜粋

以下のコードをchapter03に加えて、追加しただけです。
Chapter04

// Color VBO
vboId = glGenBuffers();
vboIdList.add(vboId);
FloatBuffer colorsBuffer = stack.callocFloat(colors.length);
colorsBuffer.put(0, colors);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, colorsBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

// Index VBO
vboId = glGenBuffers();
vboIdList.add(vboId);
IntBuffer indicesBuffer = stack.callocInt(indices.length);
indicesBuffer.put(0, indices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);

IAppLogicクラス(Mainクラス)

IAppLogicインターフェースを実装したクラス、つまりはMainクラスですが、このクラスの変更点を見てみましょう。

<Chapter03>

@Override
public void init(Window window, Scene scene, Render render) {
    float[] positions = new float[]{
            0.0f, 0.5f, 0.0f,
            -0.5f, -0.5f, 0.0f,
            0.5f, -0.5f, 0.0f
    };
    Mesh mesh = new Mesh(positions, 3);
    scene.addMesh("triangle", mesh);
}

<Chapter04>

public void init(Window window, Scene scene, Render render) {
    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,
    };
    float[] colors = new float[]{
            0.5f, 0.0f, 0.0f,
            0.0f, 0.5f, 0.0f,
            0.0f, 0.0f, 0.5f,
            0.0f, 0.5f, 0.5f,
    };
    int[] indices = new int[]{
            0, 1, 3, 3, 1, 2,
    };
    Mesh mesh = new Mesh(positions, colors, indices);
    scene.addMesh("quad", mesh);
}

具体的には、コードが追加されている形での変更(更新)なのでその部分を抜粋すると次のようになります。
ズバリ、色VBOのデータ(配列)とインデックスVBOのデータ(配列)が追加されています。

float[] colors = new float[]{
        0.5f, 0.0f, 0.0f,
        0.0f, 0.5f, 0.0f,
        0.0f, 0.0f, 0.5f,
        0.0f, 0.5f, 0.5f,
};
int[] indices = new int[]{
        0, 1, 3, 3, 1, 2,
};

頂点シェーダー (scene.vert)

シェーダーも同様に比較します。
<Chapter03>

#version 330

layout (location=0) in vec3 inPosition;

void main()
{
    gl_Position = vec4(inPosition, 3.0);
}

<Chapter04>

#version 330

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

out vec3 outColor;

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

フラグメントシェーダー(scene.flag)

<Chapter03>

#version 330

out vec4 fragColor;

void main()
{
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}

<Chapter04>

#version 330

in  vec3 outColor;
out vec4 fragColor;

void main()
{
fragColor = vec4(outColor, 1.0);
}

中身を理解する

概要は大体理解できたので、実際にプロおグラムを実行して理解を深めます。

疑問点

  • colors, indeciesの配列値の意味がわからない
  • シェーダーと実行結果の関連が理解できてない

colors, indeciesの配列値の意味がわからない

問題のコードは次の部分です。positionsに関しては、描画する際の頂点を指定していて、順番にX,Y,Z座標になっています。

理解していること

理解していることを確認します。

positionの指定に関して

これは、次のような形で頂点を指定しています。
資格を描画するので点は4つ指定します。 ※V = 頂点

float[] positions = new float[]{
        -0.5f, 0.5f, 0.0f, // V1
        -0.5f, -0.5f, 0.0f,// V2
        0.5f, -0.5f, 0.0f, // V3
        0.5f, 0.5f, 0.0f,  // V4
};
inndecieの指定に関して

四角形なので、頂点が4つ、つまり順番は0から3までになり、左回りに番号が振られているので
V1 = 0, V2 = 1, V3 = 2, V4 = 3となります。そして、三角形を2つ指定するので次のような形になります

  • 三角形1 = V1, V2, V4
  • 三角形2 = V4, V2, V3

三角形2に関しては、左回転で順番を読み込むので、v4がはじめに来ます。

インデックス番号で表現すると下のようになります。

  • 0, 1, 3, 3, 1, 2
    int[] indices = new int[]{
        0, 1, 3, 3, 1, 2,
    };

問題の部分

ズバリ、色の部分です。これは値を変更して実行してみたらわかりました。
どこかでグラデーションの設定がしてあると思われます。
各頂点の色を指定しています。なので下のような出力結果が得られます。

ポイントとしては、シェーダー(scene.flag)の中身を固定値ではなく入力値を返すように修正したため、colorsの配列もデータとして渡すようにコードを変更しているところです。
<scene.flag>

#version 330

in  vec3 outColor; # 入力値
out vec4 fragColor;# 出力値

void main()
{
    fragColor = vec4(outColor, 1.0);
}

色のデータ

float[] colors = new float[]{
        1.5f, 0.0f, 0.0f, // 赤
        0.0f, 1.0f, 0.0f, // 緑
        0.0f, 0.0f, 1.0f, // 蒼
        1.0f, 1.0f, 1.0f, // 白
}

これを色VBOとしてColorBufferに登録する

FloatBuffer colorsBuffer = stack.callocFloat(colors.length);
colorsBuffer.put(0, colors);

最終的なコード

public class Main implements IAppLogic {
    // 省略
    @Override
    public void init(Window window, Scene scene, Render render) {
        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,
        };
        float[] colors = new float[]{
                0.5f, 0.0f, 0.0f,
                0.0f, 0.5f, 0.0f,
                0.0f, 0.0f, 0.5f,
                0.0f, 0.5f, 0.5f,
        };
        int[] indices = new int[]{
                0, 1, 3, 3, 1, 2,
        };
        Mesh mesh = new Mesh(positions, colors, indices);
        scene.addMesh("quad", mesh);
    }
    // 省略
}

まとめ

Meshを作成するときに、頂点と、色、インデックスの指定を行い描画を行う処理順序が理解できた。
前回と比べて次の部分がキーポイントだった。

  1. 頂点VBO以外に、色VBOを追加している部分
  2. 頂点シェーダ(scene.vert)、フラグメントシェーダ(scene.frag)の出力を追加する
  3. MeshクラスがGPUに渡されて描画される(3章より)

<<< 前回 次回 >>>

Java 3D LWJGL GitBook 〜三角形の描画 Chapter03〜

あなたの最初の三角形

この章では、最初の三角形を画面にレンダリングし、プログラム可能なグラフィックス パイプラインの基礎を紹介します。しかし、その前に、まず座標系の基礎について説明します。後続の章で扱うテクニックとトピックをサポートするために、いくつかの基本的な数学的概念を簡単な方法で紹介しようとしています。読みやすさのために正確さを犠牲にする可能性のある単純化を想定しています。

アプリケーションを起動したときに、作成したプロジェクトとビルドパスの関係で、下のようなエラーが出るかもしれません。

Exception in thread "main" java.lang.RuntimeException: Error reading file [resources/shaders/scene.vert]

そんな時は、参照するパス指定が間違っているので、それを修正します。

自分の場合は、プロジェクトの構成が、lwjglbook/chapter-03/resources/...となっていたので、
「resources/shaders/scene.vert」の部分を「chapter-03/resources/shaders/scene.vert」に修正しました。

実行結果は、赤い三角形が表示されました。

では、これを実現するためにどのような実装を行っているのかを見ていきましょう。

座標についての簡単な説明

座標を指定することで、空間内のオブジェクトを見つけます。地図を考えてみてください。緯度または経度を指定して、地図上のポイントを指定します。数字のペアだけで、ポイントが正確に識別されます。その数値のペアはポイント座標です (マップは完全ではない楕円体である地球の投影であるため、実際にはもう少し複雑です。そのため、より多くのデータが必要ですが、それは良い例えです)。

2次元座標、(X,Y)

座標系は、1 つまたは複数の数値、つまり 1 つまたは複数のコンポーネントを使用して点の位置を一意に指定するシステムです。さまざまな座標系 (デカルト、極座標など) があり、座標をある座標系から別の座標系に変換できます。デカルト座標系を使用します。

デカルト座標系では、2 次元の座標は、2 つの直交軸 x と y までの符号付き距離を測定する 2 つの数値によって定義されます。

まずは、座標系の理解が必要らしいです。ポイントとしては、次の一言に尽きます。

数字のペアだけで、ポイントが正確に識別されます

地図の類推を続けると、座標系は原点を定義します。地理座標の場合、原点は赤道とゼロ子午線が交差するポイントに設定されます。原点をどこに設定するかによって、特定の点の座標が異なります。座標系は、軸の向きを定義することもできます。前の図では、右に移動すると x 座標が増加し、上に移動すると y 座標が増加します。しかし、別の座標を取得する別の軸方向を持つ別のデカルト座標系を定義することもできます。

なので、座標系の理解をします。単純に点(Vertex)を指定するのに2二次元であれば、下図のように

ご覧のとおり、座標を構成する数値のペアに適切な意味を与えるために、原点や軸方向などの任意のパラメーターを定義する必要があります。座標空間として、任意のパラメーターのセットを持つその座標系を参照します。一連の座標を操作するには、同じ座標空間を使用する必要があります。良いニュースは、平行移動と回転を実行するだけで、座標をある空間から別の空間に変換できることです。

3次元座標、(X,Y,Z)

3D 座標を扱う場合は、追加の軸である z 軸が必要です。3D 座標は、3 つの数値 (x、y、z) のセットによって形成されます。

3次元であれば、下図のようになります。

  • 2次元の場合:x, yの2軸を使って座標を表現 (0, 1)
  • 3次元の場合:x, y, zの3軸を使って座標を表現 (0, 1, 2)

そして、3次元の座標の取り方は2種類あります。

3D 座標は、左手と右手の 2 種類に分類できます。それがどのタイプかどうやってわかりますか?手を取り、親指と人差し指の間で「L」の字を作り、中指は他の 2 本と垂直な方向を向くようにします。親指は x 軸が増加する方向を指し、人差し指は y 軸が増加する方向を指し、中指は z 軸が増加する方向を指す必要があります。左手でそれができる場合は左利き、右手を使う必要がある場合は右利きです。

右手 左手

回転を適用する場合

2D 座標空間は、回転を適用することである空間から別の空間に変換できるため、すべて同等です。反対に、3D 座標空間はすべて等しいわけではありません。両方が同じ利き手である場合、つまり、両方が左利きまたは右利きである場合にのみ、回転を適用して一方から他方に変換できます。

いくつかの基本的なトピックを定義したので、3D グラフィックスを扱う際に一般的に使用される用語について説明しましょう。後の章で 3D モデルをレンダリングする方法を説明すると、異なる 3D 座標空間を使用することがわかります。これは、これらの座標空間のそれぞれにコンテキスト、目的があるためです。座標のセットは、何かを参照しない限り意味がありません。この座標 (40.438031, -3.676626) を調べると、彼らはあなたに何かを言うかもしれません。しかし、それらが幾何学的座標 (緯度と経度) であると言うと、マドリッドのある場所の座標であることがわかります。

3Dシーンについて

3D オブジェクトをロードすると、3D 座標のセットが取得されます。これらの座標は、オブジェクト座標空間と呼ばれる 3 次元座標空間で表されます。グラフィック デザイナーがこれらの 3D モデルを作成するとき、このモデルが表示される 3D シーンについて何も知らないため、モデルにのみ関連する座標空間を使用して座標を定義することしかできません。

ワールド座標について

3D シーンを描画する場合、すべての 3D オブジェクトは、いわゆるワールド空間座標空間に対して相対的になります。3D オブジェクト空間からワールド空間座標に変換する必要があります。一部のオブジェクトは、3D シーンで適切に表示するために、回転、引き伸ばし、または拡大および移動する必要があります。

また、表示される 3D 空間の範囲を制限する必要があります。これは、3D 空間でカメラを動かすようなものです。次に、ワールド空間座標をカメラまたはビュー空間座標に変換する必要があります。最後に、これらの座標を 2D の画面座標に変換する必要があるため、3D ビュー座標を 2D 画面座標空間に投影する必要があります。

次の図は、OpenGL 座標 (z 軸は画面に対して垂直) を示しており、座標は -1 から +1 の間です。

あなたの最初の三角形

これで、OpenGL を使用してシーンをレンダリングする際に行われるプロセスの学習を開始できます。古いバージョンの OpenGL、つまり固定機能のパイプラインに慣れている場合は、なぜそれほど複雑にする必要があるのか​​疑問に思ってこの章を終了するかもしれません。画面に単純な図形を描画するのに、それほど多くの概念やコード行は必要ないと考えるかもしれません。そう思っているあなたにアドバイスをさせてください。実際には、よりシンプルではるかに柔軟です。チャンスを与えるだけです。最新の OpenGL を使用すると、一度に 1 つの問題について考えることができ、コードとプロセスをより論理的な方法で整理できます。

グラフィックス パイプライン

最終的に 3D 表現を 2D 画面に描画する一連の手順は、グラフィックス パイプラインと呼ばれます。OpenGL の最初のバージョンでは、固定機能パイプラインと呼ばれるモデルが採用されていました。このモデルは、固定された一連の操作を定義するレンダリング プロセスの一連のステップを採用しました。プログラマーは、各ステップで使用できる関数のセットに制約され、いくつかのパラメーターを設定して微調整することができました。したがって、適用できる効果と操作は API 自体によって制限されていました (たとえば、「フォグの設定」または「ライトの追加」ですが、これらの機能の実装は固定されており、変更できませんでした)。

グラフィックス パイプラインは、次の手順で構成されています。

image text
頂点と点のリスト
変換とライティング
プリミティブの組み立て
ラスタライズ

ラスタライズ :ベクトル画像をビットマップ画像へと変換すること
テクスチャリング:テクスチャを壁紙のように貼り付けること

OpenGL 2.0 では、プログラム可能なパイプラインの概念が導入されました。このモデルでは、シェーダーと呼ばれる一連の特定のプログラムを使用して、グラフィックス パイプラインを構成するさまざまなステップを制御またはプログラムできます。次の図は、OpenGL プログラマブル パイプラインの簡略化されたバージョンを示しています。

レンダリング

レンダリングは、頂点バッファーの形式で頂点のリストを入力として取り始めます。しかし、頂点とは何ですか?頂点は、シーンをレンダリングするための入力として使用できる任意のデータ構造です。

頂点(Vertex)

具体的には、(X,Y) (X,Y,Z)の座標のこと。英語では「Vertex」

ここまでで、2D または 3D 空間の点を表す構造として考えることができます。また、3D 空間内の点をどのように記述しますか? x、y、z 座標を指定する。そして、頂点バッファとは何ですか? 頂点バッファーは、頂点配列を使用してレンダリングする必要があるすべての頂点をパックし、その情報をグラフィックス パイプラインのシェーダーで利用できるようにするもう 1 つのデータ構造です。

これらの頂点は、スクリーン スペースへの各頂点の投影位置を計算することを主な目的とする頂点シェーダーによって処理されます。このシェーダは、色やテクスチャに関連する他の出力も生成できますが、その主な目的は頂点をスクリーン スペースに投影すること、つまりドットを生成することです。

ジオメトリ処理

ジオメトリ:描画対象の形状や、形状を定義づける頂点の座標や線分

ジオメトリ処理段階では、頂点シェーダーによって変換された頂点を接続して三角形を形成します。これは、頂点が格納された順序を考慮し、異なるモデルを使用してそれらをグループ化することによって行われます。なぜ三角形?三角形は、グラフィック カードの基本的な作業単位のようなものです。これは、複雑な 3D シーンを構築するために組み合わせて変換できる単純な幾何学的形状です。このステージでは、特定のシェーダーを使用して頂点をグループ化することもできます。

<シェーダープログラム(scene.flag)>

#version 330

out vec4 fragColor;

void main()
{
    fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}

ラスター化(ラスタライズ)

ラスター化段階では、前の段階で生成された三角形を取得し、それらをクリップして、ピクセル サイズのフラグメントに変換します。これらのフラグメントは、フラグメント シェーダーによるフラグメント処理段階で使用され、フレームバッファーに書き込まれる最終的な色を割り当てるピクセルを生成します。フレームバッファは、グラフィックス パイプラインの最終結果です。画面に描画する必要がある各ピクセルの値を保持します。

3D カードは、上記のすべての操作を並列化するように設計されていることに注意してください。入力データは、最終的なシーンを生成するために並行して処理されます。

それでは、最初のシェーダー プログラムを書き始めましょう。シェーダーは、ANSI C に基づいた GLSL 言語 (OpenGL Shading Language) を使用して記述されます。まず、次の内容で、ディレクトリのscene.vert下に「 」(拡張子は Vertex Shader の場合)という名前のファイルを作成します。resources\shaders

頂点シェーダー

<シェーダ・プログラム(scene.vert)>

#version 330

layout (location=0) in vec3 inPosition;

void main()
{
    gl_Position = vec4(inPosition, 3.0);
}
1行目

最初の行は、使用している GLSL 言語のバージョンを示すディレクティブです。次の表は、GLSL バージョン、そのバージョンに一致する OpenGL、および使用するディレクティブを関連付けています (Wikipedia: https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions )。

GLS バージョン OpenGL バージョン シェーダープリプロセッサー
1.10.59 2.0 #バージョン 110
1.20.8 2.1 #バージョン 120
1.30.10 3.0 #バージョン 130
1.40.08 3.1 #バージョン 140
1.50.11 3.2 #バージョン150
3.30.6 3.3 #バージョン 330
4.00.9 4.0 #バージョン 400
4.10.6 4.1 #バージョン 410
4.20.11 4.2 #バージョン 420
4.30.8 4.3 #バージョン 430
4.40 4.4 #バージョン 440
4.50 4.5 #バージョン 450
2行目

2 行目は、このシェーダーの入力形式を指定します。OpenGL バッファー内のデータは、私たちが望むものであれば何でもかまいません。つまり、言語は、事前定義されたセマンティックを持つ特定のデータ構造を渡すことを強制しません。シェーダーの観点からは、データを含むバッファーを受け取ることを期待しています。それは、位置、追加情報を含む位置、またはその他必要なものです。この例では、頂点シェーダーの観点からは、float の配列を受け取っているだけです。バッファーを埋めるとき、シェーダーによって処理されるバッファー チャンクを定義します。

mainの中

vec3: 3 つの属性で構成されるベクトル
vec4: 4 つの属性のベクトル

そのため、まずそのチャンクを意味のあるものに変換する必要があります。この場合、位置 0 から開始して、3 つの属性 (x、y、z) で構成されるベクトルを受け取ることを期待していると言っています。

シェーダーには、他の C プログラムと同様に、この場合は非常に単純なメイン ブロックがあります。gl_Position変換を適用せずに、受け取った位置を出力変数に返すだけです。なぜ 3 つの属性のベクトルが 4 つの属性のベクトル (vec4) に変換されたのか疑問に思われるかもしれません。それの訳はgl_Position同次座標を使用しているため、結果は vec4 形式であると予想されます。つまり、(x, y, z, w) の形式で何かを期待しています。ここで、w は余分な次元を表します。別の次元を追加する理由 後の章では、実行する必要がある操作のほとんどがベクトルと行列に基づいていることがわかります。その余分な次元がない場合、これらの操作の一部を組み合わせることができません。たとえば、回転操作と平行移動操作を組み合わせることができませんでした。(これについて詳しく知りたい場合は、この余分な次元により、アフィン変換と線形変換を組み合わせることができます。これについては、Fletcher Dunn と Ian Parberry による優れた本「グラフィックスとゲーム開発のための 3D 数学入門」を読むことで学習できます。 )。

フラグメントシェーダー

最初のフラグメント シェーダーを見てみましょう。scene.fragresources ディレクトリの下に、次の内容の「 」(拡張子は Fragment Shader の拡張子) という名前のファイルを作成します。

#version 330

out vec4 fragColor;

void main()
{
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

構造は頂点シェーダーとよく似ています。この場合、各フラグメントに固定色を設定します。出力変数は 2 行目に定義され、vec4 fragColor として設定されます。

シェーダーを使うクラスの作成

シェーダーを作成したので、それらをどのように使用しますか? という名前の新しいクラスを作成する必要があります。このクラスはShaderProgram基本的に、さまざまなシェーダー モジュール (頂点、フラグメント) のソース コードを受け取り、それらをコンパイルしてリンクし、シェーダー プログラムを生成します。これは、従う必要がある一連の手順です。

一連の手順

  1. OpenGL プログラムを作成します。
  2. シェーダー プログラム モジュール (頂点シェーダーまたはフラグメント シェーダー) を読み込みます。
  3. シェーダーごとに、新しいシェーダー モジュールを作成し、そのタイプ (頂点、フラグメント) を指定します。
  4. シェーダーをコンパイルします。
  5. シェーダーをプログラムにアタッチします。
  6. プログラムをリンクします。
    最後に、シェーダー プログラムが GPU に読み込まれ、プログラム識別子という識別子を参照して使用できます。

ShaderProgramクラス

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjglb.engine.Utils;

import java.util.*;

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

public class ShaderProgram {

    private final int programId;

    public ShaderProgram(List<ShaderModuleData> shaderModuleDataList) {
        programId = glCreateProgram();
        if (programId == 0) {
            throw new RuntimeException("Could not create Shader");
        }

        List<Integer> shaderModules = new ArrayList<>();
        shaderModuleDataList.forEach(s -> shaderModules.add(createShader(Utils.readFile(s.shaderFile), s.shaderType)));

        link(shaderModules);
    }

    public void bind() {
        glUseProgram(programId);
    }

    public void cleanup() {
        unbind();
        if (programId != 0) {
            glDeleteProgram(programId);
        }
    }

    protected int createShader(String shaderCode, int shaderType) {
        int shaderId = glCreateShader(shaderType);
        if (shaderId == 0) {
            throw new RuntimeException("Error creating shader. Type: " + shaderType);
        }

        glShaderSource(shaderId, shaderCode);
        glCompileShader(shaderId);

        if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) {
            throw new RuntimeException("Error compiling Shader code: " + glGetShaderInfoLog(shaderId, 1024));
        }

        glAttachShader(programId, shaderId);

        return shaderId;
    }
    public int getProgramId() {
        return programId;
    }

    private void link(List<Integer> shaderModules) {
        glLinkProgram(programId);
        if (glGetProgrami(programId, GL_LINK_STATUS) == 0) {
            throw new RuntimeException("Error linking Shader code: " + glGetProgramInfoLog(programId, 1024));
        }

        shaderModules.forEach(s -> glDetachShader(programId, s));
        shaderModules.forEach(GL30::glDeleteShader);
    }

    public void unbind() {
        glUseProgram(0);
    }

    public void validate() {
        glValidateProgram(programId);
        if (glGetProgrami(programId, GL_VALIDATE_STATUS) == 0) {
            throw new RuntimeException("Error validating Shader code: " + glGetProgramInfoLog(programId, 1024));
        }
    }

    public record ShaderModuleData(String shaderFile, int shaderType) {
    }
}

のコンストラクターは、シェーダー モジュール typ (頂点、フラグメントなど) を定義するインスタンスShaderProgramのリストと、シェーダー モジュール コードを含むソース ファイルへのパスを受け取ります。ShaderModuleDataコンストラクターは、(メソッドを呼び出して) 各シェーダー モジュールを最初にコンパイルしcreateShader、最後に (メソッドを呼び出して) すべてをリンクすることにより、新しい OpenGL シェーダー プログラムを作成することから始めますlink。シェーダ プログラムがリンクされると、コンパイルされた頂点シェーダとフラグメント シェーダを解放できます( を呼び出すことによりglDetachShader)。

メソッドはvalidate、基本的にglValidateProgram関数を呼び出します。この関数は主にデバッグ目的で使用され、ゲームが本番段階に達したときに使用しないでください。このメソッドは、現在の OpenGL の状態でシェーダーが正しいかどうかを検証しようとします。これは、シェーダーが正しくても、現在の状態がシェーダーを実行するのに十分ではないという事実 (一部のデータがまだアップロードされていない可能性がある) が原因で、場合によっては検証が失敗する可能性があることを意味します。必要なすべての入力データと出力データが適切にバインドされたときに呼び出す必要があります (描画呼び出しを実行する直前がよいでしょう)。

ShaderProgramまた、このプログラムをレンダリングに使用するメソッド、つまりバインド、バインドを解除する別のメソッド (使用しない場合)、および最後に、リソースが不要になったときにすべてのリソースを解放するクリーンアップ メソッドも提供します。
?
という名前のユーティリティ クラスを作成しますUtils。この場合、ファイルを にロードする public メソッドを定義しますString。

Utilsクラス(ファイル読み込み)

package org.lwjglb.engine;

import java.io.IOException;
import java.nio.file.*;

public class Utils {

    private Utils() {
        // Utility class
    }

    public static String readFile(String filePath) {
        String str;
        try {
            str = new String(Files.readAllBytes(Paths.get(filePath)));
        } catch (IOException excp) {
            throw new RuntimeException("Error reading file [" + filePath + "]", excp);
        }
        return str;
    }
}

Sceneクラス

Sceneモデル、ライトなどの 3D シーンの値を保持するという名前の新しいクラスも必要になります。今では、描画したいモデルのメッシュ (頂点のセット) を引き裂くだけです。これは、そのクラスのソース コードです。

package org.lwjglb.engine.scene;

import org.lwjglb.engine.graph.Mesh;

import java.util.*;

public class Scene {

    private Map<String, Mesh> meshMap;

    public Scene() {
        meshMap = new HashMap<>();
    }

    public void addMesh(String meshId, Mesh mesh) {
        meshMap.put(meshId, mesh);
    }

    public void cleanup() {
        meshMap.values().stream().forEach(Mesh::cleanup);
    }

    public Map<String, Mesh> getMeshMap() {
        return meshMap;
    }
}

Meshクラスについて

ご覧のとおりMesh、後で描画に使用されるマップにインスタンスを格納するだけです。しかし、とは何Meshですか?これは基本的に、頂点データを GPU にロードしてレンダリングに使用できるようにする方法です。クラスの詳細を説明する前に、Meshクラスでどのように使用できるかを見てみましょうMain。

Meshクラスの使い方(呼び出し方)
public class Main implements IAppLogic {

    public static void main(String[] args) {
        Main main = new Main();
        Engine gameEng = new Engine("chapter-03", new Window.WindowOptions(), main);
        gameEng.start();
    }
    ...
    @Override
    public void init(Window window, Scene scene, Render render) {
        float[] positions = new float[]{
                0.0f, 0.5f, 0.0f,
                -0.5f, -0.5f, 0.0f,
                0.5f, -0.5f, 0.0f
        };
        Mesh mesh = new Mesh(positions, 3);
        scene.addMesh("triangle", mesh);
    }
    ...
}

このinitメソッドでは、三角形の頂点の座標を含む float の配列を定義します。ご覧のとおり、その配列には構造がなく、そこにすべての座標をダンプするだけです。現状では、OpenGL は構造を認識できません。そのデータの。それは単なるフロートのシーケンスです。次の図は、座標系の三角形を示しています。

Meshクラス

そのデータの構造を定義して GPU にロードするMeshクラスは、次のように定義されるクラスです。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;

import java.nio.FloatBuffer;
import java.util.*;

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

public class Mesh {

    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public Mesh(float[] positions, int numVertices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.numVertices = numVertices;
            vboIdList = new ArrayList<>();

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }

    public void cleanup() {
        vboIdList.stream().forEach(GL30::glDeleteBuffers);
        glDeleteVertexArrays(vaoId);
    }

    public int getNumVertices() {
        return numVertices;
    }

    public final int getVaoId() {
        return vaoId;
    }
}
VAOとVBO

ここでは、頂点配列オブジェクト (VAO) と頂点バッファー オブジェクト (VBO) という 2 つの重要な概念を紹介します。上記のコードで迷子になった場合は、最後に、描画したいオブジェクトをモデル化するデータをグラフィックス カード メモリに送信していることを思い出してください。保存すると、後で描画中に参照するための識別子が取得されます。

VBO(頂点バッファ オブジェクト)とは

まず頂点バッファ オブジェクト (VBO) から始めましょう。VBO は、頂点を格納するグラフィックス カード メモリに格納される単なるメモリ バッファです。これは、三角形をモデル化するフロートの配列を転送する場所です。前に述べたように、OpenGL はデータ構造について何も知りません。実際、座標だけでなく、テクスチャや色などの他の情報も保持できます。頂点配列オブジェクト (VAO) は、通常属性リストと呼ばれる 1 つ以上の VBO を含むオブジェクトです。各アトリビュート リストには、位置、色、テクスチャなどの 1 種類のデータを保持できます。各スロットには、必要なデータを自由に格納できます。

VAO(頂点配列オブジェクト)とは

VAO は、グラフィックス カードに格納されるデータの一連の定義をグループ化するラッパーのようなものです。VAO を作成すると、識別子が取得されます。その識別子を使用して、作成時に指定した定義を使用して、それとそれに含まれる要素をレンダリングします。

Meshクラスのコンストラクタ処理

それでは、上記のコードを確認してみましょう。最初に、VAO を作成し (glGenVertexArrays関数を呼び出して)、それをバインドします (glBindVertexArray関数を呼び出して)。その後、( を呼び出してglGenBuffers) VBO を作成し、データをそこに入れる必要があります。そのために、float の配列を に格納しますFloatBuffer。これは主に、C ベースの OpenGL ライブラリとインターフェイスする必要があるためです。そのため、float の配列をライブラリで管理できるものに変換する必要があります。

まとめると次のようになります。

  1. glGenVertexArrays関数を呼び出してVAOの作成
  2. glBindVertexArray関数でVAOのバインド
  3. VBOの生成
  4. VBOのIDをリストに追加
...
private int vaoId;
...
public Mesh(float[] positions, int numVertices) {
    ...
    // VAOの生成
    vaoId = glGenVertexArrays();
    // VAOのバインド
    glBindVertexArray(vaoId);
    // VBOの生成
    int vboId = glGenBuffers();
    // VBOをリストに格納
    vboIdList.add(vboId);
    ...
}
...

このクラスを使用してMemoryUtilオフヒープ メモリにバッファを作成し、OpenGL ライブラリからアクセスできるようにします。(put メソッドを使用して) データを保存した後、flip メソッドを使用してバッファーの位置を 0 の位置にリセットする必要があります (つまり、書き込みが終了したと言います)。Java オブジェクトは、ヒープと呼ばれる領域に割り当てられることに注意してください。ヒープは、JVM のプロセス メモリに確保された大量のメモリです。ヒープに格納されたメモリには、ネイティブ コードからアクセスできません (JNI、Java からネイティブ コードを呼び出すことを可能にするメカニズムでは、アクセスできません)。Java とネイティブ コードの間でメモリ データを共有する唯一の方法は、Java でメモリを直接割り当てることです。

以前のバージョンの LWJGL を使用している場合は、いくつかのトピックを強調することが重要です。BufferUtilsバッファーの作成にユーティリティ クラスを使用していないことに気付いたかもしれません。代わりにMemoryUtilクラスを使用します。これは、BufferUtilsがあまり効率的ではなく、下位互換性のためにのみ維持されているためです。代わりに、LWJGL 3 はバッファ管理の 2 つの方法を提案しています。
・自動管理バッファー、つまり、ガベージ コレクターによって自動的に収集されるバッファー。これらのバッファーは、主に短時間の操作、または GPU に転送され、プロセス メモリに存在する必要のないデータに使用されます。これは、org.lwjgl.system.MemoryStackクラスを使用して実現されます。
・手動で管理されるバッファー。この場合、終了したら慎重に解放する必要があります。これらのバッファは、長時間の操作または大量のデータを対象としています。これは、MemoryUtilクラスを使用して実現されます。
詳細については、 を参照して ください。
その後、( を呼び出して) VBO をバインドし、(関数glBindBufferを呼び出して) データ int ut をロードします。glBufferData次に、最も重要な部分です。データの構造を定義し、VAO の属性リストの 1 つに格納する必要があります。これは、次の行で行われます。

glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

パラメータは次のとおりです。

・index: シェーダーがこのデータを予期する場所を指定します。
・size: 頂点属性ごとのコンポーネント数を指定します (1 から 4)。この場合、3D 座標を渡しているので、3 である必要があります。
・type: 配列内の各コンポーネントのタイプ (この場合は float) を指定します。
・normalized: 値を正規化するかどうかを指定します。
・stride: 連続する汎用頂点属性間のバイト オフセットを指定します。(後で説明します)。
・offset: バッファ内の最初のコンポーネントへのオフセットを指定します。

VBO と VAO が終了したら、バインドを解除できるようにします (0 にバインドします)。
glBindVertexArray(0);
自動管理バッファを使用しているため、try/catchブロックが終了すると、バッファは自動的にクリーンアップされます。

Meshクラスの概要

このMeshクラスは、cleanup基本的に VAO と VBO を解放するメソッドと、メッシュの頂点数と VAO の ID を取得するいくつかのゲッター メソッドによって完成されます。この要素をレンダリングするとき、描画操作を使用するときに VAO id を使用します。

それでは、これらすべてを機能させてみましょう。シーン内のすべてのモデルのレンダリングを実行するという名前の新しいクラスを作成し、SceneRender次のように定義します。

package org.lwjglb.engine.graph;

import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;

import java.util.*;

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

public class SceneRender {

    private ShaderProgram shaderProgram;

    public SceneRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/scene.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
    }

    public void cleanup() {
        shaderProgram.cleanup();
    }

    public void render(Scene scene) {
        shaderProgram.bind();

        scene.getMeshMap().values().forEach(mesh -> {
                    glBindVertexArray(mesh.getVaoId());
                    glDrawArrays(GL_TRIANGLES, 0, mesh.getNumVertices());
                }
        );

        glBindVertexArray(0);

        shaderProgram.unbind();
    }
}

ご覧のとおり、コンストラクターで 2 つのShaderModuleDataインスタンス (1 つは頂点シェーダー用、もう 1 つはフラグメント用) を作成し、シェーダー プログラムを作成します。cleanupリソース (この場合はシェーダー プログラム) を解放するrenderメソッドと、描画を実行するメソッドを定義します。このメソッドは、メソッドを呼び出してシェーダー プログラムを使用することから始まりbindます。Scene次に、インスタンスに保存されているメッシュを繰り返し処理し、 (関数を呼び出して) それらをバインドし、(glBindVertexArray関数を呼び出して) VAO の頂点を描画しglDrawArraysます。最後に、VAO とシェーダー プログラムのバインドを解除して、状態を復元します。

Renderクラスの修正

Render最後に、クラスを使用するようにクラスを更新するだけですSceneRender。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;

public class Render {

    private SceneRender sceneRender;

    public Render() {
        GL.createCapabilities();
        sceneRender = new SceneRender();
    }

    public void cleanup() {
        sceneRender.cleanup();
    }

    public void render(Window window, Scene scene) {
        sceneRender.render(window, scene);
    }
}

このrenderメソッドは、フレームバッファをクリアし、(glViewportメソッドを呼び出して) ビューポートをウィンドウのサイズに設定することから始めます。つまり、レンダリング領域をそれらの寸法に設定します (これはフレームごとに行う必要はありませんが、ウィンドウのサイズ変更をサポートしたい場合は、この方法で行い、各フレームの潜在的な変更に適応させることができます)。その後、インスタンスrenderに対してメソッドを呼び出すだけです。SceneRenderそして、それだけです!手順を注意深く実行すると、次のように表示されます。

理解できなかった部分

次の文章が重要そうなのに、理解できませんでした。よく読んで今後理解したいと思います。
しかし、2週目なので補足を追加します。

3D オブジェクトをロードすると、3D 座標のセットが取得されます。

Objファイルを読み込むとき「3D 座標のセットが取得されます。」Objファイルは、以下のようなものです。

これらの座標は、オブジェクト座標空間と呼ばれる 3 次元座標空間で表されます。グラフィック デザイナーがこれらの
3D モデルを作成するとき、このモデルが表示される 3D シーンについて何も知らないため、モデルにのみ関連する座標空間を使用して座標を定義することしかできません。

多分グラフィックデザイナーはシーンについて知る必要がないので、コミュニケーションを取る際には気を付けましょうということだと思います。
そして、各3Dモデルを描画するためには「シーン」というプログラム的な要素、状態があり、それをプログラムでいうところのSceneクラス。

大まかな流れで理解する

  1. Mainクラスのインスタンス化
  2. Engineクラスのインスタンス化に伴い以下の設定
      1. ロジッククラス(Mainクラス)のセット
      1. Rennderクラスのセット
      1. シーンクラスのセット
      1. Meshクラスをインスタンス化、シーンクラスにセット
  3. Engine#start()を起動、Engine#run()を起動する

Engine#run()はゲームループになっている。

三角形の描画(理論編)

ここまでで、描画処理の学習準備が整いました。3Dモデル(シーン)の描画学習を始めます。
その前に、下のような文言があります。

画面に単純な図形を描画するのに、それほど多くの概念やコード行は必要ないと考えるかもしれません。
そう思っているあなたにアドバイスをさせてください。実際には、よりシンプルではるかに柔軟です。チャンスを与えるだけです。
最新の OpenGL を使用すると、一度に 1 つの問題について考えることができ、コードとプロセスをより論理的な方法で整理できます。

これに関して、「今回のプログラム(三角形を描画するプログラム)は単純なのでWindowクラス、Engineクラスなど作成する必要がないのでは?」と疑問に思うかもしれないが
実際にはよりシンプルではるかに柔軟です。チャンスを与えるだけです。」ということです。

つまり、各処理の担当クラスを作成し今後そのクラスに処理(例えるならば、担当業務)を追加していくということです。

理論編 グラフィックスパイプライン

次のような処理フローをグラフィックスパイプラインと呼びます。
この処理フローは次のような特徴があります。

固定された一連の操作を定義するレンダリング プロセスの一連のステップを採用しました。
プログラマーは、各ステップで使用できる関数のセットに制約され、いくつかのパラメーターを設定して微調整することができました。

しかし、これは旧バージョンのOpenGLで、下のようなデメリットがありました。

適用できる効果と操作は API 自体によって制限されていました
(たとえば、「フォグの設定」または「ライトの追加」ですが、これらの機能の実装は固定されており、変更できませんでした)。

<旧バージョンの処理フロー>

  1. 点(Vertex)と点のインデックスリストの処理
  2. 変形とライトの処理
  3. プリミティブの組み立て
  4. 抽象度の高い形式で記述された画像データを、コンピュータが最終的に出力することのできる画素の集まり(ビットマップ形式/ラスター形式)に変換
  5. 3Dモデル表面の質感を表現するための手法で、3Dオブジェクトの表面にテクスチャを壁紙のように貼り付けること
  6. Frame Bufferに出力

<新バージョン処理フロー>

  1. 点(Vertex)と点のインデックスリストの処理:VertexShader処理を追加
  2. ジオメトリの処理:GeometoryShader処理を追加
  3. プリミティブの組み立て
  4. 抽象度の高い形式で記述された画像データを、コンピュータが最終的に出力することのできる画素の集まり(ビットマップ形式/ラスター形式)に変換
  5. 3Dモデル表面の質感を表現するための手法で、3Dオブジェクトの表面にテクスチャを壁紙のように貼り付けること
  6. Frame Bufferに出力

ジオメトリとは

ジオメトリとは、幾何学、形状、などの意味を持つ英単語。 コンピュータグラフィックスにおける描画対象の形状や、形状を定義づける頂点の座標や線分、
面などの図形を表す式の係数といったデータの組み合わせを意味することが多い。
Vertexシェーダとは
3DCG では様々なモデルはポリゴン(面)で表示されています。ポリゴンは頂点と線で表現されます。
シェーダはこの頂点情報から面を作り、描画していくわけです。
レンダラが頂点情報を参照するときに呼び出すシェーダを、名前の通り Vertex シェーダ と呼びます。
Frame Bufferとは
フレームバッファとは、コンピュータ内部で一画面分の表示内容を丸ごと記憶しておくことができるメモリ領域やメモリ装置のこと。
画面に何かを描画する際にはまずソフトウェアがフレームバッファの内容を書き換え、
その内容を一定のタイミングでディスプレイなどの表示装置に転送することで画面上に更新が反映される。
これにより、描画処理の過程やその途中の状態が利用者の目に触れることを防ぐことができる。
メインメモリ(RAM)の一部に専用の領域を確保してフレームバッファとして使用する場合と、専用のメモリ装置を使用する場合がある。
ビデオカードなどに内蔵されているビデオメモリ(VRAM:Video RAM)のことをフレームバッファと呼ぶ場合もある。

レンダリング処理

レンダリング処理の順序

  1. 頂点バッファーの形式で頂点のリストを入力として取り始めます。
  2. 頂点シェーダ処理
  3. ジオメトリ処理
  4. フレームバッファへ出力

3D カードは、上記のすべての操作を並列化するように設計されていることに注意してください。入力データは、最終的なシーンを生成するために並行して処理されます。

単語 意味
頂点バッファー 頂点バッファーは、頂点配列を使用してレンダリングする必要があるすべての頂点をパックし、その情報をグラフィックス パイプラインのシェーダーで利用できるようにするデータ構造です
頂点シェーダー スクリーン スペースへの各頂点の投影位置を計算することを主な目的とす

色やテクスチャに関連する他の出力も生成できますが、その主な目的は頂点をスクリーン スペースに投影すること、つまりドットを生成することです。

ジオメトリ処理 頂点シェーダーによって変換された頂点を接続して三角形を形成します。これは、頂点が格納された順序を考慮し、異なるモデルを使用してそれらをグループ化することによって行われます。
グラフィック カードの基本的な作業単位のようなものです。これは、複雑な 3D シーンを構築するために組み合わせて変換できる単純な幾何学的形状です。このステージでは、特定のシェーダーを使用して頂点をグループ化することもできます。
ラスター化(処理) ジオメトリ処理段階で生成された三角形を取得し、それらをクリップして、ピクセル サイズのフラグメントに変換します。
これらのフラグメントは、フラグメント シェーダーによるフラグメント処理段階で使用され、フレームバッファーに書き込まれる最終的な色を割り当てるピクセルを生成します。
フレームバッファ フレームバッファは、グラフィックス パイプラインの最終結果です。画面に描画する必要がある各ピクセルの値を保持します。

頂点シェーダーを書く

ドキュメントに下のような記述があります。

それでは、最初のシェーダー プログラムを書き始めましょう。シェーダーは、ANSI C に基づいた GLSL 言語 (OpenGL Shading Language) を使用して記述されます。
まず、次の内容で、ディレクトリのscene.vert下に「 」(拡張子は Vertex Shader の場合)という名前のファイルを作成します。

各行に対して、説明をコメントで追加しました。つまり、このコードは3次元ベクトルを4次元ベクトルに変換しています。

// 使用している GLSL 言語のバージョンを示すディレクティブです。
#version 330

// このシェーダーの入力形式を指定します。
layout (location=0) in vec3 inPosition;

void main()
{
    // 4次元ベクトルをgl_Positionにセット
    gl_Position = vec4(inPosition, 1.0);
}

フラグメントシェーダーを書く

ドキュメントの説明配下の通り

構造は頂点シェーダーとよく似ています。この場合、各フラグメントに固定色を設定します。出力変数は 2 行目に定義され、vec4 fragColor として設定されます。

// 使用している GLSL 言語のバージョンを示すディレクティブです。
#version 330

// このシェーダーの出力変数
out vec4 fragColor;

void main()
{
    // フラグメントに固定色を設定
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

上記コードの場合は、フラグメントの色が固定で赤になっています。

  var colors = [
    1.0,  1.0,  1.0,  1.0,    // 白
    1.0,  0.0,  0.0,  1.0,    // 赤
    0.0,  1.0,  0.0,  1.0,    // 緑
    0.0,  0.0,  1.0,  1.0     // 青
  ];

シェーダーのまとめ

頂点シェーダーは入力の型を、フラグメントシェーダーは出力の色を指定している。

作成するクラスについて

今までのChapter-01, Chapter-02では記述していませんでしたが。ここで整理したいと思います。

使用しているクラス一覧

No パッケージ名 クラス名 役割
1 org.lwhglb.game Main メインメソッドを起動する
2 org.lwhglb.engine Engine タイトル, WinoowOption, IAplogicを引数にして、画面タイトル、ウィンドウ設定、APロジックを起動する
3 org.lwhglb.engine AppLogic 画面クリア、初期化、入力、更新)処理を実装するインターフェース
4 org.lwhglb.engine Utils ユーティリティクラス、現状ではファイル読みこみ処理の未実装
5 org.lwhglb.engine Window 内部クラスにWindowOptionを持つ、画面の設定、表示など画面周りの処理を行う
6 org.lwhglb.engine.graph Mesh まだ説明がない、三角形を描いている
7 org.lwhglb.engine.graph Render Meshクラスを描画する
8 org.lwhglb.engine.graph SceneRender 各頂点のバインド、描画処理を行っている
9 org.lwhglb.engine.graph ShaderProgram さまざまなシェーダー モジュール (頂点、フラグメント) のソース コードを受け取り、それらをコンパイルしてリンクし、シェーダー プログラムを生成
10 org.lwhglb.engine.scene Scene シーン描画のためのデータを管理するクラス

一般的なシェーダープログラムの動き

  1. OpenGL プログラムを作成します。
  2. シェーダー プログラム モジュール (頂点シェーダーまたはフラグメント シェーダー) を読み込みます。
  3. シェーダーごとに、新しいシェーダー モジュールを作成し、そのタイプ (頂点、フラグメント) を指定します。
  4. シェーダーをコンパイルします。
  5. シェーダーをプログラムにアタッチします。
  6. プログラムをリンクします。

最後に、シェーダー プログラムが GPU に読み込まれ、プログラム識別子という識別子を参照して使用できます。

ShaderProgramクラスの動き

Javaで作成されているソースコードのShaderProgramクラスの動きです。

コンストラクタ

コンストラクタの引数に、ShaderProgramのインナークラスShaderModuleDataを持っていて、ANSCのシェーダーのファイル(scene.flag, scene.vert)へのパスと
タイプ指定用のint型を指定します。つまりは下のようなコードがかけるわけです。

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

public class ShaderProgram {
    public SceneRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
    }
}

GL_VERTEX_SHADERとGL_FRAGMENT_SHADERは、org.lwjgl.opengl.GL30クラスで保持している定数です。
余談ですが、次のようなimport文は、クラスを宣言しなくても直接メソッドやフィールドにアクセスできいます。

下のコードは同じ結果を2回出力します。

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

public class Sample {
    public void test() {
        System.out.println("定数1: " + GL_VERTEX_SHADER + "定数2: " +  GL_FRAGMENT_SHADER);
        System.out.println("定数1: " + GL30.GL_VERTEX_SHADER + "定数2: " +  GL30.GL_FRAGMENT_SHADER);
    }
}

Meshクラスについて

実際に、サンプルプログラムで表示する三角形を表すクラスです。このクラスは次のプロパティ(属性、フィールド変数の事)を持っている。

  • 頂点数
  • VAOのID
  • VBOリスト(VBOのIDリスト)

VAOとVBO

ドキュメントに以下の説明があります。

頂点配列オブジェクト (VAO) と頂点バッファー オブジェクト (VBO) という 2 つの重要な概念を紹介します。上記のコードで迷子になった場合は、最後に、描画したいオブジェクトをモデル化するデータをグラフィックス カード メモリに送信していることを思い出してください。保存すると、後で描画中に参照するための識別子が取得されます。

頂点配列オブジェクト (VAO)
VAOは、通常属性リストと呼ばれる 1 つ以上の VBO を含むオブジェクトです。各アトリビュート リストには、位置、色、テクスチャなどの 1 種類のデータを保持できます。各スロットには、必要なデータを自由に格納できます。
頂点バッファー オブジェクト (VBO)
VBO は、頂点を格納するグラフィックス カード メモリに格納される単なるメモリ バッファです。これは、三角形をモデル化するフロートの配列を転送する場所です。前に述べたように、OpenGL はデータ構造について何も知りません。実際、座標だけでなく、テクスチャや色などの他の情報も保持できます。

Meshクラスの処理内容を確認

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;

import java.nio.FloatBuffer;
import java.util.*;

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

public class Mesh {

    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public Mesh(float[] positions, int numVertices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.numVertices = numVertices;
            vboIdList = new ArrayList<>();
            // VAOの生成とバインド
            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }

    public void cleanup() {
        vboIdList.stream().forEach(GL30::glDeleteBuffers);
        glDeleteVertexArrays(vaoId);
    }

    public int getNumVertices() {
        return numVertices;
    }

    public final int getVaoId() {
        return vaoId;
    }
}
  1. 最初に、glGenVertexArrays関数を呼び出してVAO を作成し 、glBindVertexArray関数を呼び出しそれをバインドします。
  2. glGenBuffers関数を呼び出して VBO を作成し、データをそこに入れる必要があります。そのために、float の配列を に格納しますFloatBuffer。
    これは主に、C ベースの OpenGL ライブラリとインターフェイスする必要があるためです。そのため、float の配列をライブラリで管理できるものに変換する必要があります。

ここら辺の処理は、SceneRenderクラスで行っています。

三角形の描画(実践編)

今まで、長々と理論部分を記述してきましたがここからは、プログラムの実行をメインに記述します。
具体的に、プログラムを実行、コードを読んで処理の内容を理解しようということです。

とりあえず描画しているコード

赤い三角形を描画している部分は次のところでした。
Render#render()

glViewport(0, 0, window.getWidth(), window.getHeight());

上記のメソッドでは次のものを指定しています。

  1. 第一引数、第二引数で表示する位置
  2. 第三引数、第四引数で三角形の底辺、高さ

これの値を変更してプログラムを実行してみましょう。自分は以下のようにコードを変更して実行しました。

  1. ファイルの参照先を変更する、SceneRenderクラスのコンストラクタの一部を修正しました。
    public SceneRender() {
    List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
    shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.vert", GL_VERTEX_SHADER));
    shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
    shaderProgram = new ShaderProgram(shaderModuleDataList);
    }
  2. 三角形の描画処理を変更薄る
    public void render(Window window, Scene scene) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //glViewport(0, 0, window.getWidth(), window.getHeight());
    glViewport(1, 2, window.getWidth() / 3, window.getHeight() / 2);
    sceneRender.render(scene);
    }

    そして実行結果外貨の通り

そして、さらにscene.flagの値を変更して下のようにします。

#version 330

out vec4 fragColor;

void main()
{
    fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}

三角形の色が青くなりました。この部分で色を指定しているようです。

つぎはsceme.vert

#version 330

layout (location=0) in vec3 inPosition;

void main()
{
    gl_Position = vec4(inPosition, 3.0);
}

三角形の大きさが小さく、位置もずれました。画面の最大サイズが(おそらく)3倍になったようです。

クラス図

次は、Chapter04を学習します。(これから作成します)
<<<前回 次回 >>>

Java 3D LWJGL GitBook 〜ゲームループの実装 Chapter02〜

第二章ゲームループ

前回に引き続き3Dモデルを表示させるために、LWJGLを使用して実現したいと思っています。LWJGLの資料には3Dモデルの詳細が説明されているので、
Gitbookで作成されているドキュメントを参考にして、学習および、実行します。

の章では、ゲーム ループを作成してゲーム エンジンの開発を開始します。ゲーム ループは、すべてのゲームのコア コンポーネントです。これは基本的に無限ループであり、定期的にユーザー入力を処理し、ゲームの状態を更新し、画面にレンダリングします。

注意

このドキュメントは英語なのでそれをブラウザで翻訳して読んでいます。
そして、このドキュメントの内容を補足する内容を記載しています。

ゲームループの基本

次のスニペットは、ゲーム ループの構造を示しています。

while (keepOnRunning) {
    input();
    update();
    render();
}

このinputメソッドは、ユーザー入力 (キーストローク、マウスの動きなど) を処理します。このupdateメソッドは、ゲームの状態 (敵の位置、AI など) を更新する役割を担っています。ゲームループは終わりましたか?まあ、まだです。上記のスニペットには多くの落とし穴があります。まず、ゲーム ループの実行速度は、実行するマシンによって異なります。マシンが十分に高速な場合、ユーザーはゲームで何が起こっているかを確認することさえできません。さらに、そのゲーム ループはすべてのマシン リソースを消費します。

まず、ゲームの状態が更新される期間と、ゲームが画面にレンダリングされる期間を別々に制御したい場合があります。なぜこれを行うのですか?ゲームの状態を一定の速度で更新することは、特に物理エンジンを使用している場合には重要です。逆に、レンダリングが間に合わない場合、ゲーム ループの処理中に古いフレームをレンダリングしても意味がありません。一部のフレームをスキップする柔軟性があります。

ゲームループの処理「loop()」
  • input(): キーボード・マウスなどの入力のの処理をする。
  • update(): ゲームの状態(敵の位置、AI など)を更新する。
  • render(): ゲーム画面の更新をする。

クラス図

クラス名 役目 概要
Main ゲームプログラム起動 ゲームを起動して、IAppLogicをimplements(実装)する
Window GLFWのすべての呼び出しを行う ウィンドウ ハンドル、その幅と高さ、およびウィンドウのサイズが変更など
Scene 3D シーンの将来の要素 (モデルなど) を保持 現状では空のプレースホルダー
Render 画面の描画、レンダリングを行う 現状では画面をクリアする別のプレースホルダー
Engine ゲームロジックを動かす ゲームループの実行を行う

ここで、「カプセル化」という言葉について補足します。

カプセル化とは
一つの機能を実現するための処理をまとめて管理するためのクラスの作り方です。

つまり、Windowクラスは、ウィンドウの表示、設定などの処理をひとまとめにして、持っているのでウィンドウの操作をしたければ
このクラスを使用すればよい。という形でプログラムを組むことができます。
同様に、Engineクラス、Scene、Renderとあります。もちろんMainクラスも同様です。

では、各クラスの詳細を見ていきます。

IAppLogicインターフェース

ゲーム ループを調べる前に、エンジンのコアを形成するサポート クラスを作成しましょう。まず、ゲーム ロジックをカプセル化するインターフェイスを作成します。これにより、ゲーム エンジンをさまざまな章で再利用できるようになります。このインターフェイスには、ゲーム アセットの初期化 ( init)、ユーザー入力の処理 ( input)、ゲーム ステートの更新 ( update)、およびリソースのクリーンアップ( ) を行うメソッドが含まれますcleanup。

このインターフェースは、このインターフェースを実装(implements)するクラスに定義したメソッドをオーバーライドする事を強制するので、
このインターフェースを実装(implements)するクラスは、必ず定義したメソッドを実装します。

つまり、MainクラスはIAppLogicを実装(implements)するので、

ゲーム アセットの初期化 ( init)、ユーザー入力の処理 ( input)、ゲーム ステートの更新 ( update)、およびリソースのクリーンアップ( ) を行う

ということです。

package org.lwjglb.engine;

import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public interface IAppLogic {

    void cleanup();

    void init(Window window, Scene scene, Render render);

    void input(Window window, Scene scene, long diffTimeMillis);

    void update(Window window, Scene scene, long diffTimeMillis);
}

Windowクラス

ご覧のとおり、まだ定義していないいくつかのクラス インスタンス ( Window、Sceneおよび) と、これらのメソッドの呼び出しの間に渡されるミリ秒を保持するRenderという名前のパラメーターがあります。diffTimeMillis

Windowクラスから始めましょう。ウィンドウを作成および管理するための GLFW ライブラリへのすべての呼び出しをこのクラスにカプセル化します。その構造は次のようになります。

主に、ウィンドウを起動するために必要な処理を行います。インナークラス(内部クラス)にて、ウィンドウサイズを保持しています。
Window#WindowOptionの重要な部分(プロパティ(フィールド変数))を抜粋しておきます。

compatibleProfile
これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps
1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します (これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
height
目的のウィンドウの高さ。
width
目的のウィンドウの幅。
ups
1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。
package org.lwjglb.engine;

import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.system.MemoryUtil;
import org.tinylog.Logger;

import java.util.concurrent.Callable;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryUtil.NULL;

public class Window {

    private final long windowHandle;
    private int height;
    private Callable<Void> resizeFunc;
    private int width;
    ...
    ...
    public static class WindowOptions {
        public boolean compatibleProfile;
        public int fps;
        public int height;
        public int ups = Engine.TARGET_UPS;
        public int width;
    }
}

ご覧のとおり、ウィンドウ ハンドル、その幅と高さ、およびウィンドウのサイズが変更されたときに呼び出されるコールバック関数を格納するためのいくつかの属性が定義されています。また、ウィンドウの作成を制御するいくつかのオプションを設定するための内部クラスも定義します。

compatibleProfile: これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps: 1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します (これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
・height: 目的のウィンドウの高さ。
・width: 希望のウィンドウ幅:
・ups: 1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。
`Window+ クラスのコンストラクターを調べてみましょう。

public class Window {
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        this.resizeFunc = resizeFunc;
        if (!glfwInit()) {
            throw new IllegalStateException("Unable to initialize GLFW");
        }
        glfwDefaultWindowHints();
        glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
        if (opts.compatibleProfile) {
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
        } else {
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
            glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        }

        if (opts.width > 0 && opts.height > 0) {
            this.width = opts.width;
            this.height = opts.height;
        } else {
            glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
            GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
            width = vidMode.width();
            height = vidMode.height();
        }
       windowHandle = glfwCreateWindow(width, height, title, NULL, NULL);
        if (windowHandle == NULL) {
            throw new RuntimeException("Failed to create the GLFW window");
        }

        glfwSetFramebufferSizeCallback(windowHandle, (window, w, h) -> resized(w, h));

        glfwSetErrorCallback((int errorCode, long msgPtr) ->
                Logger.error("Error code [{}], msg [{]]", errorCode, MemoryUtil.memUTF8(msgPtr))
        );
        glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
            }
        });

        glfwMakeContextCurrent(windowHandle);

        if (opts.fps > 0) {
            glfwSwapInterval(0);
        } else {
            glfwSwapInterval(1);
        }

        glfwShowWindow(windowHandle);

        int[] arrWidth = new int[1];
        int[] arrHeight = new int[1];
        glfwGetFramebufferSize(windowHandle, arrWidth, arrHeight);
        width = arrWidth[0];
        height = arrHeight[0];
    }
    ...

ウィンドウヒントを設定してウィンドウを非表示にし、サイズ変更可能に設定することから始めます。その後、OpenGL のバージョンを設定し、ウィンドウ オプションに応じてコアまたは互換プロファイルを設定します。次に、適切な幅と高さを設定していない場合は、プライマリ モニターのサイズを取得してウィンドウ サイズを設定します。次に、 を呼び出してウィンドウを作成し、ウィンドウのglfwCreateWindowサイズが変更されたとき、またはウィンドウの終了 (ESCキーが押されたとき) を検出するためにいくつかのコールバックを設定します。ターゲット FPS を手動で設定する場合は、呼び出しglfwSwapInterval(0)て v-sync を無効にし、最後にウィンドウを表示してフレーム バッファー サイズ (render() に使用されるウィンドウの部分) を取得します。

クラスの残りのメソッドは、Windowリソースのクリーンアップ、サイズ変更コールバック、ウィンドウ サイズのいくつかのゲッター、およびイベントをポーリングし、ウィンドウを閉じる必要があるかどうかを確認するメソッドです。

public class Window {
    ...
    public void cleanup() {
        glfwFreeCallbacks(windowHandle);
        glfwDestroyWindow(windowHandle);
        glfwTerminate();
        GLFWErrorCallback callback = glfwSetErrorCallback(null);
        if (callback != null) {
            callback.free();
        }
    }

    public int getHeight() {
        return height;
    }

    public int getWidth() {
        return width;
    }

    public boolean isKeyPressed(int keyCode) {
        return glfwGetKey(windowHandle, keyCode) == GLFW_PRESS;
    }

    public void pollEvents() {
        glfwPollEvents();
    }

    protected void resized(int width, int height) {
        this.width = width;
        this.height = height;
        try {
            resizeFunc.call();
        } catch (Exception excp) {
            Logger.error("Error calling resize callback", excp);
        }
    }

    public void update() {
        glfwSwapBuffers(windowHandle);
    }

    public boolean windowShouldClose() {
        return glfwWindowShouldClose(windowHandle);
    }
    ...
}

このSceneクラスは、3D シーンの将来の要素 (モデルなど) を保持します。今では空のプレースホルダーです。

package org.lwjglb.engine.scene;

public class Scene {

    public Scene() {
    }

    public void cleanup() {
        // Nothing to be done here yet
    }
}

Renderクラスは、画面をクリアする別のプレースホルダーになりました。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;

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

public class Render {

    public Render() {
        GL.createCapabilities();
    }

    public void cleanup() {
        // Nothing to be done here yet
    }

    public void render(Window window, Scene scene) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }
}

Engineこれで、次のように始まる名前の新しいクラスにゲーム ループを実装できます。

package org.lwjglb.engine;

import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public class Engine {

    public static final int TARGET_UPS = 30;
    private final IAppLogic appLogic;
    private final Window window;
    private Render render;
    private boolean running;
    private Scene scene;
    private int targetFps;
    private int targetUps;

    public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
        window = new Window(windowTitle, opts, () -> {
            resize();
            return null;
        });
        targetFps = opts.fps;
        targetUps = opts.ups;
        this.appLogic = appLogic;
        render = new Render();
        scene = new Scene();
        appLogic.init(window, scene, render);
        running = true;
    }

    private void cleanup() {
        appLogic.cleanup();
        render.cleanup();
        scene.cleanup();
        window.cleanup();
    }

    private void resize() {
        // Nothing to be done yet
    }
    ...
}

このEngineクラスは、コンストラクターでウィンドウのタイトル、ウィンドウ オプション、およびIAppLogicインターフェイスの実装への参照を受け取ります。Windowコンストラクターでは、、RenderおよびSceneクラスのインスタンスを作成します。このcleanupメソッドは、他のクラスcleanupリソースを呼び出すだけです。ゲーム ループは、次のrunように定義されるメソッドで定義されます。

public class Engine {
    ...
    private void run() {
        long initialTime = System.currentTimeMillis();
        float timeU = 1000.0f / targetUps;
        float timeR = targetFps > 0 ? 1000.0f / targetFps : 0;
        float deltaUpdate = 0;
        float deltaFps = 0;

        long updateTime = initialTime;
        while (running && !window.windowShouldClose()) {
            window.pollEvents();

            long now = System.currentTimeMillis();
            deltaUpdate += (now - initialTime) / timeU;
            deltaFps += (now - initialTime) / timeR;

            if (targetFps <= 0 || deltaFps >= 1) {
                appLogic.input(window, scene, now - initialTime);
            }

            if (deltaUpdate >= 1) {
                long diffTimeMillis = now - updateTime;
                appLogic.update(window, scene, diffTimeMillis);
                updateTime = now;
                deltaUpdate--;
            }

            if (targetFps <= 0 || deltaFps >= 1) {
                render.render(window, scene);
                deltaFps--;
                window.update();
            }
            initialTime = now;
        }

        cleanup();
    }
    ...
}

ループは、更新 ( ) とレンダリング呼び出し ( )の間の最大経過時間をミリ秒単位で制御するtimeUとの2 つのパラメーターを計算することから始まります。これらの期間が消費された場合、ゲームの状態を更新するかレンダリングする必要があります。後者の場合、ターゲット FPS が 0 に設定されている場合、v-sync リフレッシュ レートに依存するため、値を に設定するだけです。ループは、ウィンドウを介してイベントをポーリングすることから始まります。その後、現在の時間をミリ秒単位で取得します。その後、更新呼び出しとレンダリング呼び出しの間の経過時間を取得します。レンダリング (または v-sync のリレー) の最大経過時間を過ぎた場合、 を呼び出してユーザー入力を処理します。最大更新経過時間を超えた場合は、呼び出してゲームの状態を更新しますtimeRtimeUtimeR0appLogic.inputappLogic.update. レンダリング (または v-sync のリレー) の最大経過時間を過ぎた場合、 を呼び出してレンダリング呼び出しをトリガーしますrender.render。

ループの最後で、cleanupメソッドを呼び出してリソースを解放します。

最後に、次のEngineように完了します。

public class Engine {
    ...
    public void start() {
        running = true;
        run();
    }

    public void stop() {
        running = false;
    }
}

スレッドについて少し注意してください。GLFW はメインスレッドから初期化する必要があります。イベントのポーリングもそのスレッドで行う必要があります。したがって、ゲームでよく見られるゲーム ループ用の別のスレッドを作成する代わりに、メイン スレッドからすべてを実行します。Threadこれが、startメソッドでnew を作成しない理由です。

Main最後に、クラスを単純化して次のようにします。

package org.lwjglb.game;

import org.lwjglb.engine.*;
import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public class Main implements IAppLogic {

    public static void main(String[] args) {
        Main main = new Main();
        Engine gameEng = new Engine("chapter-02", new Window.WindowOptions(), main);
        gameEng.start();
    }

    @Override
    public void cleanup() {
        // Nothing to be done yet
    }

    @Override
    public void init(Window window, Scene scene, Render render) {
        // Nothing to be done yet
    }

    @Override
    public void input(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done yet
    }

    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done yet
    }
}

インスタンスを作成し、メソッドEngineで起動するだけです。mainこのMainクラスはIAppLogic、今では空になっているインターフェースも実装しています。

ゲームループについて

以下のような説明がありました。

これは基本的に無限ループであり、定期的にユーザー入力を処理し、ゲームの状態を更新し、画面にレンダリングします。

この通りなのですが、 少しかみ砕いて記載したいと思います。やることは以下の通り

  1. 無限ループ内で実行する
  2. 定期的にユーザー入力を処理
  3. ゲームの状態(データの状態)を更新
  4. 画面を再描画(レンダリング)

これをコードに落とすと次のようになります。

// 無限ループ
while () {
    // ユーザー入力
    input();
    // データの更新
    update();
    // 画面の更新
    rendaer();
}

input(), update(), render()の各メソッドは別途実装します。ただ、実装する内容に関しては作成するものによって変わるのでインターフェースにしておきます。
それが説明にある「IAppLogic」です。
このようにインターフェースを作成し、implementsしてやれば必ず上記の3メソッドを実装することになるので、クラス構成がわかりやすくなります。

処理の流れ

Githubにアップされているコードをどのような順序で処理しているのか一通り調べました。  
次のような順序で処理していました。

メインクラス:メインメソッドのあるクラス
インターフェースIAppLogicを実装し、自身(Mainクラス)をEngineクラスのコンストラクタに渡してEngine#start()メソッドを実行しています。
Engineクラス
WindowOptionの値、IAppLogicをフィールド変数にセットしていつでも呼び出せるようにしています。
またstart()メソッドからrun()メソッドを実行し、run()メソッドでは、ゲームループを実行しています。

プログラムの実行結果としては、何も表示されない(画面が黒い)状態になります。

Windowクラス

オブジェクト指向プログラミングの基本「役割分担」を行った結果「画面関連の処理を担当するクラス」をWindowクラスとして作成します。
ドキュメントに以下の記載があるように、クラスを作成します。※コードはGithubにアップされていますが。。。

ウィンドウを作成および管理するための GLFW ライブラリへのすべての呼び出しをこのクラスにカプセル化

WindowOption

ウィンドウを作成するのに必要な情報をインナークラス(内部クラス)で管理します。

public class Window {

    private final long windowHandle;
    private int height;
    private Callable<Void> resizeFunc;
    private int width;

    /** インナークラス */
    public static class WindowOptions {
        public boolean compatibleProfile;
        public int fps;
        public int height;
        public int ups = Engine.TARGET_UPS;
        public int width;
    }
}

各フィールド(オプションの値)は次のようになっています。

compatibleProfile: これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps: 1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、
モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します
(これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
height: 目的のウィンドウの高さ。
width: 希望のウィンドウ幅:
ups: 1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。

Windowクラスのコンストラクタ

まずは、コンストラクタの定義を見てみます。

 public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
    // 省略
 }

第一引数にタイトル、第二引数にWindowOption(Windowクラスのインナークラス)、第三引数にCallBackがあります。
第三引数のクラスがよくわからないので、呼び出し元を調べます。

window = new Window(windowTitle, opts, () -> {
    resize();
    return null;
});

Engineクラスで呼び出されています。変数「window」はWindowクラス型のフィールド変数です。
つまりは、Windowクラスをインスタンス化してwindow変数にセットしているというわけです。
注目するのは、第三引数です。渡しているのは「() -> { ... };」の部分です。これは、いわゆる関数型という書き方で
Functionalインターフェースの理解が必要になります。
なので、今回は、「引数に関数(メソッド)を渡しているのだな。」と理解してください。

まとめると、第三引数ひは、resize()メソッドを呼び出してから、nullを返す処理を行っているメソッドを渡しています。

スレッドについて少し注意してください。GLFW はメインスレッドから初期化する必要があります。イベントのポーリングもそのスレッドで行う必要があります。したがって、ゲームでよく見られるゲーム ループ用の別のスレッドを作成する代わりに、メイン スレッドからすべてを実行します。Threadこれが、startメソッドでnew を作成しない理由です。

いろいろ書いていますが、メインメソッドが動いているスレッドでGLFWの初期化を行いましょう。ということです。

まとめ

IAppLogicクラスにゲームループの中身、cleanup(), init(), input(), update()を作成しています。
Window, EngineクラスはIAppLogicを動かすためにゲームループを作成したり、ウィンドウの処理を行ったりとそれぞれに役割が分担されています。
ここから、ゲームの実装方法、OpenGLの操作方法法を学習していくと思われます。

WindowOptionについて

画面のサイズを保持するクラスですが、このクラスの高さ(height)と幅(width)は、Windowクラスのコンストラクタ、以下の処理部分で値がセットされているようです。ちなみに、int型のデフォルト値(宣言しただけの時にセットされる値は0です。

        if (opts.width > 0 && opts.height > 0) {
            System.out.println("*** In True ***");
            this.width = opts.width;
            this.height = opts.height;
        } else {
            System.out.println("*** In False ***");
            glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
            GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
            width = vidMode.width();
            height = vidMode.height();
        }

<<< 前回 次回 >>>

Java 3D LWJGL GitBook: Chapter01〜環境構築 ハローワールド 〜

イントロダクション

Javaで3Dモデルを操作することができ、ゲーム作成のためのAPI(ライブラリ)、LEJGL(Light Weight Java Game Library)を
使用して、3Dモデルを表示させようと思います。
<参照するもの>

注意

このドキュメントは英語なのでそれをブラウザで翻訳して読んでいます。

早い話が。。。

OBJファイルを読み込んで3DモデルをJavaで表示するためにLWJGLを使用することにしました。

LWJGLは下のような特徴があります。

  1. OBJファイルは、3Dモデルを作成したときにできるファイルです。
  2. OpenGLというライブラリも使用しているので、3Dモデルや通常の2Dイメージも表示できるのがLWJGLです。
  3. やはり、プログラムなのでゲームを作成するのでなくても、部分的に、例えば3Dモデルの表示のみでも使える

Blenderなどで作成した3Dモデル(Objファイル)を表示するのが目的です。
JOGL(他のライブラリ)でやろうと思ったけど、チュートリアルや、ドキュメントが少ないので、やはり今回も使わない方向でいきます。

参考

LWJGLのGitbookです、合わせてプログラムソースもGithubにアップされているので、そちらも参考にします。

LWJGL GitBook Chapter-01

今回は、LWJGLを起動することを確認するプログラムコードを動かしましょう。というものです。
はじめのプログラム「ハローワールド」のLWJGL版です。基本的にハローワルドはDB接続、フレームワークの使用をするときにそれらがちゃんと動かせるかどうか確認するために実行するプログラムです。

今回の学習内容

学習を始めるのに参考にするもののリンクを張っておきます。

IntelliJ IDEAを使用して、環境構築(プログラムを動かす設定)をおこない、ハローワールドを実行するということが今回の目的になります。
早い話が、LWJGLが動く確認をします

環境構築

参考サイトにあるように、環境構築を行います。下の文言を参考にします。

。IntelliJ は無​​料のオープン ソース バージョンであるコミュニティ バージョンを提供しており、 https ://www.jetbrains.com/idea/download/ からダウンロードできます。

このリンクから、IntelliJをダウンロードして、インストールしたら、早速必要なライブラリ(依存関係)をダウンロードします。
Mavenで実行しているようなので、Maven(メイベン)を使用します。

Mavenの使用

用途としては、必要なライブラリをダウンロードしてくることです。そのためにビルドツールとしてMavenを使用します。他にもGradleが有名です。
まずは、IntelliJ起動してください。そしたらGithubからGitbookのプロジェクトをチェックアウトします。

プロジェクトのチェックアウト(クローン)

起動した、IntelliJのプロジェクトを未選択の状態で開きます。初回の軌道であればそのままで大丈夫です。

ここで、赤枠の部分Get from VCSをクリックします。「VCS」は バージョンコントロールシステムのことです。つまり、バージョン管理を行うアプリケーションのことです。

そして、下の項目を入力します。

URLに関しては、Githubのページを開いたら、したのようなボタンがあるのでそれをクリックします。

必要なライブラリのダウンロード

クローンしてきたプロジェクトにはpom.xmlというファイルがあると思います。これを右クリックして次のようにします。

  1. Mavenを選択
  2. Reload Projectをクリック

これで必要なライブラリがダウンロードされます。
早速、ハローワールドを実行してみましょう。下のような実行結果が見れると思います。

Gitbookの内容

まずは、環境構築のために必要なリポジトリ、依存関係のダウンロードを行うためのPOMファイルへの記述内容の説明があります。

POMファイル(pom.xml)の概要

参考にしたページには書いていませんが、pom.xmlでは、次のようなものを定義しています。

  • project: 作成するプロジェクト
  • parent: 親プロジェクト
  • artifactId: このファイルの定義するプロジェクトのID
  • dependencies: 依存関係
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.lwjglb</groupId>
        <artifactId>book</artifactId>
        <version>1.0</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>chapter-01</artifactId>
    <version>1.0</version>

    <dependencies>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>

        <!-- Natives -->
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

この本では、 OpenGLを使用して 3D ゲームを開発する際の主なテクニックを学びます。Java でサンプルを開発し、Lightweight Java Game Library LWJGLを使用します。LWJGL ライブラリを使用すると、Java から OpenGL などの低レベル API (アプリケーション プログラミング インターフェイス) にアクセスできます。

LWJGL は、OpenGL のラッパーのように機能する低レベル API です。したがって、短期間で 3D ゲームの作成を開始することを考えている場合は、jMonkeyEngineやUnityなどの他の代替手段を検討する必要があります。この低レベルの API を使用すると、結果を確認する前に、多くの概念を調べ、多くのコード行を記述する必要があります。このようにすることの利点は、3D グラフィックスをよりよく理解できるようになり、常にコントロールできるようになることです。

Java に関しては、少なくとも Java 18 が必要です。したがって、そのバージョンがインストールされていない場合の最初のステップは、Java SDK をダウンロードすることです。ここからOpenJDK バイナリをダウンロードするか、Windows および macOS 用のインストーラーがあるAdoptiumからダウンロードできます。Adoptium ビルドをダウンロードするときは、「JRE」ではなく「JDK」をダウンロードしてください。お使いのオペレーティング システムに適したインストーラーを選択してインストールするだけです。いずれにせよ、この本は Java 言語についてある程度理解していることを前提としています。そうでない場合は、まずその言語について適切な知識を得る必要があります。

サンプルを実行するために必要な Java IDE を使用できます。Java を適切にサポートする IntelliJ IDEA をダウンロードできます。IntelliJ は無​​料のオープン ソース バージョンであるコミュニティ バージョンを提供しており、 https ://www.jetbrains.com/idea/download/ からダウンロードできます。

IDE でソース コードを開くと、すべての章 (親プロジェクト) を含むルート フォルダーを開くか、各章を個別に開くことができます。最初のケースでは、各章の作業ディレクトリを章のルート フォルダに適切に設定することを忘れないでください。サンプルは、ルート フォルダーがチャプター ベース フォルダーであると想定して、相対パスを使用してファイルにアクセスしようとします。

サンプルのビルドにはMavenを使用します。Maven はほとんどの IDE に既に統合されており、それらのさまざまなサンプルを直接開くことができます。章のサンプルを含むフォルダーを開くだけで、IntelliJ はそれが Maven プロジェクトであることを検出します。

Maven は、(プロジェクト オブジェクト モデル) という名前の XML ファイルに基づいてプロジェクトをビルドします。このファイルは、pom.xmlプロジェクトの依存関係 (使用する必要があるライブラリ) と、ビルド プロセス中に実行する手順を管理します。Maven は、構成よりも規則の原則に従います。つまり、標準のプロジェクト構造と命名規則に固執する場合、構成ファイルは、ソース ファイルの場所やコンパイルされたクラスの場所を明示的に指定する必要はありません。

この本は Maven のチュートリアルを意図したものではないので、必要に応じて Web で情報を見つけてください。ソース コード フォルダーは、使用するプラグインを定義し、使用するライブラリのバージョンを収集する親プロジェクトを定義します。

LWJGL 3.1 では、プロジェクトのビルド方法にいくつかの変更が導入されました。現在、ベース コードはよりモジュール化されており、巨大なモノリシック jar ファイルを使用する代わりに、使用するパッケージをより選択することができます。これには代償が伴います。依存関係を 1 つずつ慎重に指定する必要があります。ただし、ダウンロードページには、pom ファイルを生成する優れたツールが含まれています。この場合、最初に GLFW と OpenGL バインディングを使用します。ソースコードでpomファイルがどのように見えるかを確認できます。

LWJGL プラットフォームの依存関係は、プラットフォームのネイティブ ライブラリの展開を既に処理しているため、他のプラグイン ( などmavennatives) を使用する必要はありません。LWJGL プラットフォームを構成するプロパティを設定するために、3 つのプロファイルを設定するだけです。プロファイルは、Windows、Linux、および Mac OS ファミリのそのプロパティの正しい値を設定します。

    <profiles>
        <profile>
            <id>windows-profile</id>
            <activation>
                <os>
                    <family>Windows</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-windows</native.target>
            </properties>                
        </profile>
        <profile>
            <id>linux-profile</id>
            <activation>
                <os>
                    <family>Linux</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-linux</native.target>
            </properties>                
        </profile>
        <profile>
            <id>OSX-profile</id>
            <activation>
                <os>
                    <family>mac</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-osx</native.target>
            </properties>
        </profile>
    </profiles>

各プロジェクト内で、LWJGL プラットフォームの依存関係は、現在のプラットフォームのプロファイルで確立された正しいプロパティを使用します。

        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-platform</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
        </dependency>

それに加えて、すべてのプロジェクトは実行可能な jar (java -jar name_of_the_jar.jar と入力して実行できるもの) を生成します。MANIFEST.MFこれは、正しい値を持つファイルで jar を作成する maven-jar-plugin を使用して実現されます。そのファイルの最も重要な属性はMain-Class、プログラムのエントリ ポイントを設定する です。さらに、すべての依存関係は、Class-Pathそのファイルの属性のエントリとして設定されます。別のコンピューターで実行するには、ターゲット ディレクトリの下にあるメインの jar ファイルと lib ディレクトリ (そこに含まれるすべての jar ファイルを含む) をコピーするだけです。

LWJGL クラスを含む jar には、ネイティブ ライブラリも含まれています。また、LWJGL はそれらを抽出し、JVM がライブラリを探すパスに追加します。

この章のソース コードは、LWJGL サイトhttp://www.lwjgl.org/guideの入門用サンプルから直接取得したものです。非常によく文書化されていますが、ソース コードを見て、最も関連性の高い部分を説明しましょう。クラスごとにソースコードを貼り付けると読めなくなってしまうので、フラグメントを含めます。特定の各フラグメントが属するクラスをよりよく理解するために、各フラグメントには常にクラス ヘッダーが含まれます。3 つのドット ( ...) を使用して、フラグメントの前後にさらにコードがあることを示します。HelloWorldサンプルは、次のように始まる名前の単一のクラスに含まれています。

ソースコード解析

プログラムの内容を確認します。初回のLWJGL学習なので市登場するクラスは1つです。
ポイントは、OpenGLを使用している処理部分です。
具体的には、以下の通りです。

  1. 赤い四角を描画している処理
  2. 描画の準備をする処理
  3. プログラムの構成

プログラムの構成

  • main: runメソッドを起動。
  • run: init, loopを呼び出す。
  • init: アプリケーションを初期化するメソッド。
  • loop: 基本的にウィンドウにレンダリングする無限ループであるメソッド。

メインメソッド

以下のサンプルコードにあるように、メインメソッドの処理内容が書いてあります。
単純に「HelloWorld#run()」を呼び出しています。
とてもシンプルです。言葉に直すのであれば「『ハローワールド』を起動する」という言い方になります。

package org.lwjglb;

import org.lwjgl.Version;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;

import java.nio.IntBuffer;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;

public class HelloWorld {

    // The window handle
    private long window;

    public static void main(String[] args) {
        new HelloWorld().run();
    }
    ...
}

run()メソッド

mainこのクラスは、メソッドを呼び出すメソッド内に、ウィンドウ ハンドルへの参照を格納するだけです (これが何を意味するかは後で説明します) run。そのメソッドの分析を始めましょう。

このメソッドがプログラムの処理内容を具体的に呼び出している部分です。
具体的に、以下の処理を行っています。正確には「呼び出しています」といったほうが良いかもしれません。

  1. LWJGLのバージョン情報表示。
  2. プログラムの初期処理「init()」を呼び出す。
  3. プログラムの主要部分「loop()」を呼び出す。
  4. プログラムの終了処理。

1~4の各処理に対して、細かい部分は各メソッド(init(), loop())の中身を見ることにして、
この「run()メソッド」では、上記の処理(呼び出し)を行っています。

public class HelloWorld {
    ...
    public void run() {
        System.out.println("Hello LWJGL " + Version.getVersion() + "!");

        init();
        loop();

        // Free the window callbacks and destroy the window
        glfwFreeCallbacks(window);
        glfwDestroyWindow(window);

        // Terminate GLFW and free the error callback
        glfwTerminate();
        glfwSetErrorCallback(null).free();
    }
    ...
}

init()メソッド

このメソッドはinit、アプリケーションを初期化するメソッドを呼び出してから、loop基本的にウィンドウにレンダリングする無限ループであるメソッドを呼び出します。メソッドが終了したら、loop初期化中に作成されたいくつかのリソースを解放する必要があります (GLFW ウィンドウ)。メソッドから始めましょうinit。

public class HelloWorld {
    ...
    private void init() {
        // Setup an error callback. The default implementation
        // will print the error message in System.err.
        GLFWErrorCallback.createPrint(System.err).set();

        // Initialize GLFW. Most GLFW functions will not work before doing this.
        if (!glfwInit())
            throw new IllegalStateException("Unable to initialize GLFW");

        // Configure GLFW
        glfwDefaultWindowHints(); // optional, the current window hints are already the default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

        // Create the window
        window = glfwCreateWindow(300, 300, "Hello World!", NULL, NULL);
        if (window == NULL)
            throw new RuntimeException("Failed to create the GLFW window");

        // Setup a key callback. It will be called every time a key is pressed, repeated or released.
        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
        });
        ...
    }
    ...
}

GLFWを呼び出すことから始めます。これは、GUI コンポーネント (Windows など) とイベント (キーの押下、マウスの動きなど) を処理するライブラリであり、OpenGL コンテキストが簡単な方法でアタッチされています。現在、Swing または AWT を直接使用して OpenGL をレンダリングすることはできません。AWT を使用したい場合はlwjgl3-awt を確認できますが、この本では GLFW に固執します。最初に、GLFW ライブラリを初期化し、ウィンドウの初期化のためのいくつかのパラメーターを設定することから始めます (サイズ変更可能かどうかなど)。ウィンドウは、glfwCreateWindowウィンドウの幅と高さ、およびウィンドウのタイトルを受け取ります。この関数は、他の GLFW 関連関数で使用できるように保存する必要があるハンドルを返します。その後、キーが押されたときに呼び出される関数であるキーボード コールバックを設定します。ESCこの場合、ウィンドウを閉じるためにキーが押されたかどうかを検出したいだけです。メソッドを続けましょうinit:

public class HelloWorld {
    ...
    private void init() {
        ...
        // Get the thread stack and push a new frame
        try (MemoryStack stack = stackPush()) {
            IntBuffer pWidth = stack.mallocInt(1); // int*
            IntBuffer pHeight = stack.mallocInt(1); // int*

            // Get the window size passed to glfwCreateWindow
            glfwGetWindowSize(window, pWidth, pHeight);

            // Get the resolution of the primary monitor
            GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

            // Center the window
            glfwSetWindowPos(
                    window,
                    (vidmode.width() - pWidth.get(0)) / 2,
                    (vidmode.height() - pHeight.get(0)) / 2
            );
        } // the stack frame is popped automatically

        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        // Enable v-sync
        glfwSwapInterval(1);

        // Make the window visible
        glfwShowWindow(window);
    }
    ...
}

次の章で説明しますが、ここでは LWJGL の主要なクラスであるMemoryStack. 前に述べたように、LJWGL はネイティブ ライブラリ (C ベースの関数) のラッパーを提供します。Java にはポインターの概念がないため (少なくとも C の用語ではタイヒンク)、C 関数に構造体を渡すのは単純な作業ではありません。これらの構造を共有し、上記の例のように参照パラメーターを渡すには、ネイティブ コードからアクセスできるメモリを割り当てる必要があります。LWJGL は、MemoryStackネイティブ アクセス可能なメモリ/構造を割り当てることができるクラスを提供します。これにより、自動的にクリーンアップされます (実際には、再利用できるように strcuture のようなプールに返されます)。stackPushメソッドが呼び出されます。ネイティブにアクセス可能なすべてのメモリ/構造は、このスタック クラスを通じてインスタンス化されます。上記のサンプルでは、glfwGetWindowSize​​ウィンドウの寸法を取得するために を呼び出す必要があります。値は参照渡しのアプローチを使用して返されるため、2 つの int を (2 の形式でIntBuffer) 割り当てる必要があります。その情報とモニターの寸法を使用して、ウィンドウを中央に配置し、OpenGL をセットアップし、v-sync を有効にして (これについては次の章で詳しく説明します)、最後にウィンドウを表示します。

ここで、何かを継続的にレンダリングするための無限ループが必要です。

まとめると...

次のようになります。前提としてLWJGLはOpenGLを使用しています。

  • 既存のJava Swing(GUI作成をするAPI)、AWT(これもGUIを作成するAPI)などを直接使用してOpenGLをレンダリングはできない。
  • GLFW ライブラリを初期化(glfwInit())ウィンドウの初期化のためのいくつかのパラメーターを設定する「(コメントの)Configure GLFW部分」
  • glfwGetWindowSize()はウィンドウの幅と高さ、およびウィンドウのタイトルを受け取ります。そして、他の GLFW 関連関数で使用できるように保存する必要があるハンドルを返します。「」
  • キーが押されたときに呼び出される関数であるキーボード コールバックを設定します。「glfwSetKeyCallback()」

loop()メソッド

<行っている処理>

  • OpenGL コンテキストを作成
  • クリア カラーを設定
  • 各ループで (カラーおよび深度バッファに対して) クリア操作
  • ウィンドウを閉じる必要があるかどうかを検出するためにキーボード イベントをポーリング
public class HelloWorld {
    ...
    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the GLCapabilities instance and makes the OpenGL
        // bindings available for use.
        GL.createCapabilities();

        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        while (!glfwWindowShouldClose(window)) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer

            glfwSwapBuffers(window); // swap the color buffers

            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }
    ...
}

最初に OpenGL コンテキストを作成し、クリア カラーを設定して、各ループで (カラーおよび深度バッファに対して) クリア操作を実行し、ウィンドウを閉じる必要があるかどうかを検出するためにキーボード イベントをポーリングします。これらの概念については、次の章で詳しく説明します。ただし、完全を期すために、レンダリングはターゲットに対して行われます。この場合は、色情報と深度値 (3D の場合) を含むバッファーに対して行われます。これらのバッファーに対するレンダリングが終了したら、GLFW に通知する必要があります。このバッファは、 を呼び出して表示する準備ができていglfwSwapBuffersます。GLFW はいくつかのバッファーを維持するため、1 つのバッファーでレンダリング操作を実行しながら、もう 1 つのバッファーがウィンドウに表示されます (そうでない場合、ちらつきアーティファクトが発生します)。

環境が正しくセットアップされていれば、それを実行して赤い背景のウィンドウを見ることができるはずです。

クラス図

わかりやすいようにクラス図を作成しました。

今回のコードは、クラスが一つですべての処理を行っているので、上記のようになります。

プログラムの実行内容

実行しているコードを説明しています。自分の理解では次のようになります。詳細は次の章(Chapter)で説明するそうです。

  1. メインメソッドの説明
    フィールド変数にwindowというint型の変数があり、runというスタティックメソッドを実行している
  2. run()メソッドの実行している内容の説明
    init()メソッドとloop()メソッドを呼び出している、loop()メソッドは無限ループになっている
  3. init()メソッドの説明
    GLFWを呼び出すことから始めています。GLFWは”GUI コンポーネント (Windows など) とイベント (キーの押下、マウスの動きなど) を処理するライブラリであり、OpenGL コンテキストが簡単な方法でアタッチされています”とあるように、画面コントロールのライブラリのようです。実行内容に関しては本文を参照ください。
  4. loop()メソッドの説明
    本文にありますが、何かを継続的にレンダリングするための無限ループということでした

loop()の処理内容

  1. glClearColor()で画面をクリア
  2. glfwSwapBuffers()でGLFWに通知

上記の処理を行っているようですが、細かいところは、次の章で明らかになると思います。。。

まとめ

環境構築編

Mavenの依存関係をPOMファイルで設定、Mevenによる依存関係の解決(ライブラリのダウンロード)を行い。
LWJGLが実行できるように環境を構築する使用しているIDEはIntelliJ IDEA

実装編

プログラムの実行、処理の内容を解説。

  1. init()で画面コントロール、表示するサイズなどの設定を行う
  2. loop()メソッドで、”何かを継続的にレンダリングするための無限ループ”

を実装するというところを理解した。

今回のハローワールドでは、赤い画面を表示するだけだったが、LWJGLの動作の基本が、おおまかに理解できる。

次回 >>>

Tutorial#1(別途行ったものです)

今回からLWJGLのチュートリアルを進めてきます。
このチュートリアルには、セットアップ(環境構築)手順も載っていました。

まずは、写経から入ります。
この動画で作成しているコードを写経していきます。
作成したコードは、下のようなコードになりました。

2クラスあります。
画面を起動するためのMainクラス自分が作成したものは「GameMainクラス」にしています。

public class GameMain {
    public static void main(String[] args) {
        Window win = new Window(800, 500, "LWJGL Tutorial");
        win.create();
        while (!win.closed()) {
            win.update();
            win.swapBuffers();
        }
    }
}

「Windowクラス」

public class Window {
    private int width;
    private int height;
    private String title;
    private long window;

    public Window(int width, int height, String title) {
        this.width = width;
        this.height = height;
        this.title = title;
    }

    public void create() {
        if (!GLFW.glfwInit()) {
            System.err.println("Error: Coudn not GLFW");
            System.exit(-1);
        }
        window = GLFW.glfwCreateWindow(width, height, title, 0, 0);
        if (window == 0) {
            System.err.println("Error: window coud not be created");
            System.exit(-1);
        }
        GLFWVidMode videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
        GLFW.glfwSetWindowPos(window, (videoMode.width() - width) / 2, (videoMode.height() - height) / 2);
    }

    public boolean closed() {
        return GLFW.glfwWindowShouldClose(window);
    }

    public void update() {
        GLFW.glfwPollEvents();
    }

    public void swapBuffers() {
        GLFW.glfwSwapBuffers(window);
    }
}

今回の作成した2つのクラスは、以下のような役割分担になっています。
GameMain: Windowクラスを起動する
Window: LWJGLで使用しているGLFWクラスを使用して画面を作成する処理を書きます。

今回はGitにあるソースをコピーとか参照することができなかったので、動画を見て写経しました。それで、下のコードを間違えていました、すると表示した時に画面が端っこに行ってしまいました。
GLFW.glfwSetWindowPos(window, (videoMode.width() + width) / 2, (videoMode.height() + height) / 2);

そして、このコードがよくわかりませんでした。
どの値を計算しているか?これがわからなかったという次第です。

なのでちょいと、いじって見ました、
タイトルの部分と、上の計算している部分のコードをチョチョっといじりました。

結局は、上のよくわからなかった部分は画面表示の初期位置を指定するものでした。

次回は、Chapter02をやります。

関連ページ一覧

http://zenryokuservice.com/wp/2018/10/27/java-game-lwjgl-gitbook-chapter5-2%E3%80%9C%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E8%A9%B3%E7%B4%B0%E3%80%9C/

<開発準備>

http://zenryokuservice.com/wp/2018/05/02/set-up-1-eclipse-java/

<LWJGLのGitBokに関して>

  1. Chapter1[外枠の表示のみ]
  2. Chapter2-1〜クラスの構成〜
  3. Chapter2-2〜インターフェースの使い方と詳細〜
  4. Chapter2-3〜GameEngineクラス(サンプルクラス)〜/li>
  5. Chapter2-4〜Windowクラス(サンプルクラス)〜
  6. Chapter3〜描画処理を読む〜
  7. Chapter4〜シェーダについて〜
  8. Chapter5-1〜レンダリングについて〜

<Java Basic>

  1. Java Basic Level 1 〜Hello Java〜
  2. Java Basic Level2 〜Arithmetic Calculate〜
  3. Java Basic Level3 〜About String class〜
  4. Java Basic Level 4〜Boolean〜
  5. Java Basic Level 5〜If Statement〜
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 〜Traning of If statement〜
  8. Java Basic Level8 〜How to use for statement〜
  9. Java Basic Level 8.5 〜Array〜
  10. Java Basic Level 8.5 〜Array〜
  11. Java Basic Level 10 〜While statement 〜
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2〜