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章より)

<<< 前回 次回 >>>

投稿者:

takunoji

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

コメントを残す