Java 3D LWJGL GitBook: 第 20 章 – 間接描画 (静的モデル)

第 20 章 - 間接描画 (静的モデル)

この章まで、マテリアル ユニフォーム、テクスチャ、頂点、およびインデックス バッファをバインドし、構成されているメッシュごとに 1 つの描画コマンドを送信することによって、モデルをレンダリングしてきました。この章では、より効率的なレンダリングへの道を歩み始めます。バインドレス レンダリング (少なくともほとんどバインドレス) の実装を開始します。このタイプのレンダリングでは、一連の描画コマンドを呼び出してシーンを描画するのではなく、GPU がそれらをレンダリングできるようにする命令をバッファーに入力します。これは間接レンダリングと呼ばれ、次の理由により、より効率的な描画方法です。

・各メッシュを描画する前に、いくつかのバインド操作を実行する必要がなくなりました。
・単一の描画呼び出しを呼び出すだけで済みます。
・CPU 側の負荷を軽減するフラスタム カリングなどの GPU 内操作を実行できます。
ご覧のとおり、最終的な目標は、CPU 側で発生する可能性のある潜在的なボトルネックと、CPU から GPU への通信によるレイテンシを取り除きながら、GPU の使用率を最大化することです。この章では、レンダリングを変換して、静的モデルのみから始まる間接描画を使用します。アニメ化されたモデルは、次の章で扱います。

サンプルコードの実行

サンプルコードの実行を行ったときに、エラーがありました。

Exception in thread "main" java.lang.RuntimeException: Error linking Shader code: Type mismatch: Type of outMaterialIdx different between shaders.
Out of resource error.

これを解消するのに、以下の部分を修正しました。
<scene.vert>

out uint outMaterialIdx; -> flat out uint outMaterialIdx;

コンセプト

コードを説明する前に、間接描画の背後にある概念を説明しましょう。要するに、頂点のレンダリングに使用される描画パラメータを格納するバッファを作成する必要があります。これは、描画を実行するように指示する GPU によって読み取られる命令ブロックまたは描画コマンドと考えることができます。バッファーにデータが取り込まれたら、 を呼び出してglMultiDrawElementsIndirectそのプロセスをトリガーします。DrawElementsIndirectCommandバッファーに格納された各描画コマンドは、次のパラメーターによって定義されます (C を使用している場合、これは構造体によってモデル化されます)。
・count: 描画する頂点の数 (頂点とは、位置、法線情報、テクスチャ座標などをグループ化する構造と理解します)。glDrawElementsこれには、メッシュのレンダリング時に を呼び出したときに使用した頂点の数と同じ値が含まれている必要があります。
・instanceCount: 描画されるインスタンスの数。同じモデルを共有する複数のエンティティが存在する場合があります。エンティティごとに描画命令を保存する代わりに、単一の描画命令を送信するだけで、描画するエンティティの数を設定できます。これはインスタンス レンダリングと呼ばれ、計算時間を大幅に節約します。間接的な描画がなくても、VAO ごとに特定の属性を設定することで同じ結果を得ることができます。このテクニックだとさらに簡単だと思います。
・firstIndex: この描画命令に使用されるインデックス値を保持するバッファーへのオフセット (オフセットは、バイト オフセットではなく、インデックスの数で測定されます)。
・baseVertex: 頂点データを保持するバッファーへのオフセット (オフセットは、バイト オフセットではなく、頂点の数で測定されます)。
・baseInstance: このパラメーターを使用して、描画されるすべてのインスタンスで共有される値を設定できます。この値を描画するインスタンスの数と組み合わせると、インスタンスごとのデータにアクセスできます (これについては後で説明します)。

パラメータを説明するときにすでにコメントされていますが、間接描画には、頂点データを保持するバッファと、インデックス用に別のバッファが必要です。違いは、シーンのモデルを単一のバッファに適合させる複数のメッシュからすべてのデータを結合する必要があることです。メッシュごとの特定のデータにアクセスする方法は、描画パラメータのオフセット値を再生することです。

解決すべきもう 1 つの側面は、マテリアル情報またはエンティティごとのデータ (モデル マトリックスなど) を渡す方法です。前の章では、そのためにユニフォームを使用し、メッシュまたは描画するエンティティを変更したときに適切な値を設定しました。間接的な描画では、大量の描画命令を一度に送信するため、レンダリング プロセス中にデータを変更することはできません。これに対する解決策は、追加のバッファーを使用することです。エンティティごとのデータをバッファーに格納し、baseInstanceパラメーター (インスタンス ID と組み合わせて) を使用して、そのバッファー内の適切なデータ (エンティティごと) にアクセスできます (後で説明します。バッファの代わりにユニフォームの配列を使用しますが、より単純なバッファを使用することもできます)。そのバッファ内では、2 つの追加バッファにアクセスするためのインデックスを保持します。
・モデル行列データを保持するもの。
・マテリアル データ (アルベド カラーなど) を保持するもの。
テクスチャの場合、配列テクスチャと混同しないように、テクスチャの配列を使用します。配列テクスチャは、テクスチャ情報を持つ値の配列を含むテクスチャで、同じサイズの複数の画像があります。テクスチャの配列は、通常のテクスチャにマップされるサンプルのリストであるため、異なるサイズを持つことができます。テクスチャの配列には制限があり、その長さを任意の長さにすることはできず、例では最大 16 のテクスチャを設定するという制限があります (ただし、制限を設定する前に GPU の機能を確認することをお勧めします)。複数のモデルを使用している場合、16 テクスチャは高い値ではありません。この制限を回避するには、次の 2 つのオプションがあります。
・テクスチャ アトラス (個々のテクスチャを組み合わせた巨大なテクスチャ ファイル) を使用します。間接描画を使用していない場合でも、バインド呼び出しが制限されるため、テクスチャ アトラスをできるだけ使用するようにしてください。
・バインドレス テクスチャを使用します。このアプローチでは基本的に、ハンドル (64 ビット整数値) を渡してテクスチャを識別し、その識別子を使用してシェーダー プログラムでサンプラーを取得できます。可能であれば、これは間違いなく間接レンダリングを使用する方法です (これはコア機能ではなく、バージョン 4.4 以降の拡張機能です)。RenderDoc は現在これをサポートしていないため、このアプローチは使用しません (RenderDoc なしでデバッグする機能を失うことは、私にとって致命的です)。

次の図は、間接描画に関係するバッファーと構造を示しています (これは、静的モデルのレンダリング中にのみ有効であることに注意してください。アニメーション モデルのレンダリングに使用する必要がある新しい構造については、次の章で説明します)。

エンティティ データ、マテリアル、およびモデル マトリックスごとにユニフォームの配列を使用することに注意してください (最後に、配列はバッファーですが、ユニフォームを使用することで便利な方法でデータにアクセスできます)。

実装

間接描画を使用するには、少なくとも OpenGL バージョン 4.6 を使用する必要があります。したがって、最初のステップは、ウィンドウ作成のウィンドウ ヒントとして使用するメジャー バージョンとマイナー バージョンを更新することです。

public class Window {
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        ...
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
        ...
    }
    ...
}

次のステップは、すべてのメッシュを 1 つのバッファにロードするようにコードを変更することですが、その前に、モデル、マテリアル、およびメッシュを格納するクラス階層を変更しません。これまで、モデルには一連のメッシュを持つ一連の関連付けられたマテリアルがあります。このクラス階層は、ドロー コールを最適化するように設定されており、最初にモデルを反復し、次にマテリアルを反復し、最後にメッシュを反復しました。この構造を変更し、マテリアルの下にメッシュを保存しなくなります。代わりに、メッシュはモデルの直下に保存されます。マテリアルを一種のキャッシュに保存し、そのキャッシュ内のメッシュのキーへの参照を保持します。それに加えて、以前は、Mesh各モデル メッシュのインスタンス。本質的に、メッシュ データの VAO と関連する VBO が含まれていました。すべてのメッシュに対して単一のバッファを使用するため、シーンのメッシュのセット全体に対して単一の VAO とそれに関連付けられた VBO が必要になります。Mesh したがって、クラスの下にインスタンスのリストを保存する代わりにModel、頂点バッファーのオフセット、インデックス バッファーのオフセットなど、描画パラメーターを構築するために使用されるデータを保存します。変更を調べてみましょう。一つずつ。

MaterialCache次のように定義されているクラスから始めます。

package org.lwjglb.engine.graph;

import java.util.*;

public class MaterialCache {

    public static final int DEFAULT_MATERIAL_IDX = 0;

    private List<Material> materialsList;

    public MaterialCache() {
        materialsList = new ArrayList<>();
        Material defaultMaterial = new Material();
        materialsList.add(defaultMaterial);
    }

    public void addMaterial(Material material) {
        materialsList.add(material);
        material.setMaterialIdx(materialsList.size() - 1);
    }

    public Material getMaterial(int idx) {
        return materialsList.get(idx);
    }

    public List<Material> getMaterialsList() {
        return materialsList;
    }
}

ご覧のとおり、Materialインスタンスを に格納するだけListです。したがって、 を識別するためにMaterial必要なのは、リスト内のそのインスタンスのインデックスだけです。(このアプローチでは、動的に新しいマテリアルを追加するのが難しくなる場合がありますが、このサンプルの目的には十分単純です。それを変更して、新しいモデル、マテリアルなどをコードに追加するための堅牢なサポートを提供することもできます。 )。クラスを変更してインスタンスMaterialのリストを削除しMesh、マテリアル インデックスをマテリアル キャッシュに保存する必要があります。

public class Material {
    ...
    private Vector4f ambientColor;
    private Vector4f diffuseColor;
    private int materialIdx;
    private String normalMapPath;
    private float reflectance;
    private Vector4f specularColor;
    private String texturePath;

    public Material() {
        diffuseColor = DEFAULT_COLOR;
        ambientColor = DEFAULT_COLOR;
        specularColor = DEFAULT_COLOR;
        materialIdx = 0;
    }
    ...
    public int getMaterialIdx() {
        return materialIdx;
    }
    ...
    public void setMaterialIdx(int materialIdx) {
        this.materialIdx = materialIdx;
    }
    ...
}

前に説明したように、Modelクラスを変更してマテリアルへの参照を削除する必要があります。代わりに、2 つの主要なリファレンスを保持します。

・MeshDataAssimp を使用して読み取ったメッシュ データを保持するリストインスタンス (新しいクラス)。
・RenderBuffers.MeshDrawData間接描画に必要な情報 (主に、上記で説明したデータ バッファーに関連付けられたオフセット情報) を含むインスタンス (これも新しいクラス)のリスト。
MeshDataassimp を使用してモデルをロードするときに、最初にインスタンスのリストを生成します。その後、データを保持するグローバル バッファーを作成して、RenderBuffers.MeshDrawDataインスタンスを生成します。その後、 MeshDataインスタンスへの参照を削除できます。これはあまり洗練されたソリューションではありませんが、ロード前とロード後の階層を使用して複雑さを増すことなく、概念を説明するのに十分単純です。クラスの変更点は次のModelとおりです。

public class Model {
    ...
    private final String id;
    private List<Animation> animationList;
    private List<Entity> entitiesList;
    private List<MeshData> meshDataList;
    private List<RenderBuffers.MeshDrawData> meshDrawDataList;

    public Model(String id, List<MeshData> meshDataList, List<Animation> animationList) {
        entitiesList = new ArrayList<>();
        this.id = id;
        this.meshDataList = meshDataList;
        this.animationList = animationList;
        meshDrawDataList = new ArrayList<>();
    }
    ...
    public List<MeshData> getMeshDataList() {
        return meshDataList;
    }

    public List<RenderBuffers.MeshDrawData> getMeshDrawDataList() {
        return meshDrawDataList;
    }

    public boolean isAnimated() {
        return animationList != null && !animationList.isEmpty();
    }
    ...
}

クラスの定義MeshDataは非常に単純です。頂点の位置、テクスチャ座標などを保存するだけです。

package org.lwjglb.engine.graph;

import org.joml.Vector3f;

public class MeshData {

    private Vector3f aabbMax;
    private Vector3f aabbMin;
    private float[] bitangents;
    private int[] boneIndices;
    private int[] indices;
    private int materialIdx;
    private float[] normals;
    private float[] positions;
    private float[] tangents;
    private float[] textCoords;
    private float[] weights;

    public MeshData(float[] positions, float[] normals, float[] tangents, float[] bitangents,
                    float[] textCoords, int[] indices, int[] boneIndices, float[] weights,
                    Vector3f aabbMin, Vector3f aabbMax) {
        materialIdx = 0;
        this.positions = positions;
        this.normals = normals;
        this.tangents = tangents;
        this.bitangents = bitangents;
        this.textCoords = textCoords;
        this.indices = indices;
        this.boneIndices = boneIndices;
        this.weights = weights;
        this.aabbMin = aabbMin;
        this.aabbMax = aabbMax;
    }

    public Vector3f getAabbMax() {
        return aabbMax;
    }

    public Vector3f getAabbMin() {
        return aabbMin;
    }

    public float[] getBitangents() {
        return bitangents;
    }

    public int[] getBoneIndices() {
        return boneIndices;
    }

    public int[] getIndices() {
        return indices;
    }

    public int getMaterialIdx() {
        return materialIdx;
    }

    public float[] getNormals() {
        return normals;
    }

    public float[] getPositions() {
        return positions;
    }

    public float[] getTangents() {
        return tangents;
    }

    public float[] getTextCoords() {
        return textCoords;
    }

    public float[] getWeights() {
        return weights;
    }

    public void setMaterialIdx(int materialIdx) {
        this.materialIdx = materialIdx;
    }
}

クラスの変更ModelLoaderも非常に簡単です。マテリアル キャッシュを使用し、読み取ったデータをMeshData(以前のMeshクラスではなく) 新しいクラスに保存する必要があります。また、マテリアルにはメッシュ データへの参照はありませんが、メッシュ データにはキャッシュ内のマテリアルのインデックスへの参照があります。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, MaterialCache materialCache,
                                  boolean animation) {
        return loadModel(modelId, modelPath, textureCache, materialCache, aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices |
                aiProcess_Triangulate | aiProcess_FixInfacingNormals | aiProcess_CalcTangentSpace | aiProcess_LimitBoneWeights |
                aiProcess_GenBoundingBoxes | (animation ? 0 : aiProcess_PreTransformVertices));
    }

    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache,
                                  MaterialCache materialCache, int flags) {
        ...

        for (int i = 0; i < numMaterials; i++) {
            AIMaterial aiMaterial = AIMaterial.create(aiScene.mMaterials().get(i));
            Material material = processMaterial(aiMaterial, modelDir, textureCache);
            materialCache.addMaterial(material);
            materialList.add(material);
        }

        int numMeshes = aiScene.mNumMeshes();
        PointerBuffer aiMeshes = aiScene.mMeshes();
        List<MeshData> meshDataList = new ArrayList<>();
        List<Bone> boneList = new ArrayList<>();
        for (int i = 0; i < numMeshes; i++) {
            AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
            MeshData meshData = processMesh(aiMesh, boneList);
            int materialIdx = aiMesh.mMaterialIndex();
            if (materialIdx >= 0 && materialIdx < materialList.size()) {
                meshData.setMaterialIdx(materialList.get(materialIdx).getMaterialIdx());
            } else {
                meshData.setMaterialIdx(MaterialCache.DEFAULT_MATERIAL_IDX);
            }
            meshDataList.add(meshData);
        }
        ...
        return new Model(modelId, meshDataList, animations);
    }
    ...
    private static MeshData processMesh(AIMesh aiMesh, List<Bone> boneList) {
        ...
        return new MeshData(vertices, normals, tangents, bitangents, textCoords, indices, animMeshData.boneIds,
                animMeshData.weights, aabbMin, aabbMax);
    }
}

このSceneクラスは、マテリアル キャッシュを保持するクラスになります (また、cleanupVAO と VBO がモデル マップにリンクされなくなるため、mwthod は不要になります):

public class Scene {
    ...
    private MaterialCache materialCache;
    ...
    public Scene(int width, int height) {
        ...
        materialCache = new MaterialCache();
        ...
    }
    ...
    public MaterialCache getMaterialCache() {
        return materialCache;
    }
    ...    
}

クラスの変更は、Meshクラスを導入したためMeshDataです (コンストラクターの引数とメソッドを変更するだけの問題です)。

public class Mesh {
    ...
    public Mesh(MeshData meshData) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.aabbMin = meshData.getAabbMin();
            this.aabbMax = meshData.getAabbMax();
            numVertices = meshData.getIndices().length;
            ...
            FloatBuffer positionsBuffer = stack.callocFloat(meshData.getPositions().length);
            positionsBuffer.put(0, meshData.getPositions());
            ...
            FloatBuffer normalsBuffer = stack.callocFloat(meshData.getNormals().length);
            normalsBuffer.put(0, meshData.getNormals());
            ...
            FloatBuffer tangentsBuffer = stack.callocFloat(meshData.getTangents().length);
            tangentsBuffer.put(0, meshData.getTangents());
            ...
            FloatBuffer bitangentsBuffer = stack.callocFloat(meshData.getBitangents().length);
            bitangentsBuffer.put(0, meshData.getBitangents());
            ...
            FloatBuffer textCoordsBuffer = MemoryUtil.memAllocFloat(meshData.getTextCoords().length);
            textCoordsBuffer.put(0, meshData.getTextCoords());
            ...
            FloatBuffer weightsBuffer = MemoryUtil.memAllocFloat(meshData.getWeights().length);
            weightsBuffer.put(meshData.getWeights()).flip();
            ...
            IntBuffer boneIndicesBuffer = MemoryUtil.memAllocInt(meshData.getBoneIndices().length);
            boneIndicesBuffer.put(meshData.getBoneIndices()).flip();
            ...
            IntBuffer indicesBuffer = stack.callocInt(meshData.getIndices().length);
            indicesBuffer.put(0, meshData.getIndices());
        }
    }
    ...
}

今度は、間接描画用に作成する新しい主要なクラスの 1 つであるRenderBuffersクラスの番です。このクラスは、すべてのメッシュのデータを含む VBO を保持する単一の VAO を作成します。この場合、静的モデルのみをサポートするため、単一の VAO が必要になります。クラスは次のRenderBuffersように始まります。

public class RenderBuffers {

    private int staticVaoId;
    private List<Integer> vboIdList;

    public RenderBuffers() {
        vboIdList = new ArrayList<>();
    }

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

このクラスは、モデルをロードする 2 つのメソッドを定義します。

・loadAnimatedModelsアニメモデル用。これは、この章では実装されません。
・loadStaticModelsアニメーションのないモデルの場合。
これらのメソッドは次のように定義されています。

public class RenderBuffers {
    ...
    public final int getStaticVaoId() {
        return staticVaoId;
    }

    public void loadAnimatedModels(Scene scene) {
        // To be completed
    }

    public void loadStaticModels(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        staticVaoId = glGenVertexArrays();
        glBindVertexArray(staticVaoId);
        int positionsSize = 0;
        int normalsSize = 0;
        int textureCoordsSize = 0;
        int indicesSize = 0;
        int offset = 0;
        for (Model model : modelList) {
            List<RenderBuffers.MeshDrawData> meshDrawDataList = model.getMeshDrawDataList();
            for (MeshData meshData : model.getMeshDataList()) {
                positionsSize += meshData.getPositions().length;
                normalsSize += meshData.getNormals().length;
                textureCoordsSize += meshData.getTextCoords().length;
                indicesSize += meshData.getIndices().length;

                int meshSizeInBytes = meshData.getPositions().length * 14 * 4;
                meshDrawDataList.add(new MeshDrawData(meshSizeInBytes, meshData.getMaterialIdx(), offset,
                        meshData.getIndices().length));
                offset = positionsSize / 3;
            }
        }

        int vboId = glGenBuffers();
        vboIdList.add(vboId);
        FloatBuffer meshesBuffer = MemoryUtil.memAllocFloat(positionsSize + normalsSize * 3 + textureCoordsSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                populateMeshBuffer(meshesBuffer, meshData);
            }
        }
        meshesBuffer.flip();
        glBindBuffer(GL_ARRAY_BUFFER, vboId);
        glBufferData(GL_ARRAY_BUFFER, meshesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(meshesBuffer);

        defineVertexAttribs();
         // Index VBO
        vboId = glGenBuffers();
        vboIdList.add(vboId);
        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indicesSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                indicesBuffer.put(meshData.getIndices());
            }
        }
        indicesBuffer.flip();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(indicesBuffer);

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

まず、VAO (静的モデルに使用されます) を作成し、モデルのメッシュを反復処理します。単一のバッファを使用してすべてのデータを保持するため、これらの要素を繰り返し処理して最終的なバッファ サイズを取得します。RenderBuffers.MeshDrawData位置要素、法線などの数を計算します。最初のループを使用して、インスタンスを含むリストに保存するオフセット情報も入力します。その後、単一の VBO を作成します。MeshVAO と VBO を作成する同様のタスクを行ったクラスとの大きな違いがわかります。この場合、位置や法線などに単一の VBO を使用します。個別の VBO を使用する代わりに、すべてのデータを行ごとにロードするだけです。これは、populateMeshBuffer(これについては後で説明します)。その後、すべてのモデルのすべてのメッシュのインデックスを含むインデックス VBO を作成します。

クラスは次のMeshDrawDataように定義されます。

public class RenderBuffers {
    ...
    public record MeshDrawData(int sizeInBytes, int materialIdx, int offset, int vertices) {
    }
}

基本的に、メッシュのサイズ (バイト単位) ( sizeInBytes)、関連付けられているマテリアル インデックス、頂点情報と頂点を保持するバッファー内のオフセット、このメッシュのインデックス数を格納します。オフセットは「行」で測定されます。位置、法線、テクスチャ座標を保持するメッシュの部分を 1 つの「行」と考えることができます。この「行」は、1 つの頂点に関連付けられたすべての情報を保持し、頂点シェーダーで処理されます。これが、位置要素の数を 3 だけダイブする理由です。各「行」には 3 つの位置要素があり、位置データの「行」の数は法線データの「行」の数と一致します。 .

は次のpopulateMeshBufferように定義されます。

public class RenderBuffers {
    ...
    private void populateMeshBuffer(FloatBuffer meshesBuffer, MeshData meshData) {
        float[] positions = meshData.getPositions();
        float[] normals = meshData.getNormals();
        float[] tangents = meshData.getTangents();
        float[] bitangents = meshData.getBitangents();
        float[] textCoords = meshData.getTextCoords();

        int rows = positions.length / 3;
        for (int row = 0; row < rows; row++) {
            int startPos = row * 3;
            int startTextCoord = row * 2;
            meshesBuffer.put(positions[startPos]);
            meshesBuffer.put(positions[startPos + 1]);
            meshesBuffer.put(positions[startPos + 2]);
            meshesBuffer.put(normals[startPos]);
            meshesBuffer.put(normals[startPos + 1]);
            meshesBuffer.put(normals[startPos + 2]);
            meshesBuffer.put(tangents[startPos]);
            meshesBuffer.put(tangents[startPos + 1]);
            meshesBuffer.put(tangents[startPos + 2]);
            meshesBuffer.put(bitangents[startPos]);
            meshesBuffer.put(bitangents[startPos + 1]);
            meshesBuffer.put(bitangents[startPos + 2]);
            meshesBuffer.put(textCoords[startTextCoord]);
            meshesBuffer.put(textCoords[startTextCoord + 1]);
        }
    }
    ...
}

ご覧のとおり、データの「行」を繰り返し処理し、位置、法線、およびテクスチャ座標をバッファにパックします。は次のdefineVertexAttribsように定義されます。

public class RenderBuffers {
    ...
    private void defineVertexAttribs() {
        int stride = 3 * 4 * 4 + 2 * 4;
        int pointer = 0;
        // Positions
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Normals
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Tangents
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Bitangents
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Texture coordinates
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 2, GL_FLOAT, false, stride, pointer);
    }
    ...
}

前の例のように、VAO の頂点属性を定義するだけです。ここでの唯一の違いは、単一の VBO を使用していることです。

クラスの変更を調べる前に、次のように始まる頂点シェーダー ( ) からSceneRender始めましょう。scene.vert

#version 460

const int MAX_DRAW_ELEMENTS = 100;
const int MAX_ENTITIES = 50;

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec3 tangent;
layout (location=3) in vec3 bitangent;
layout (location=4) in vec2 texCoord;

out vec3 outNormal;
out vec3 outTangent;
out vec3 outBitangent;
out vec2 outTextCoord;
out vec4 outViewPosition;
out vec4 outWorldPosition;
out uint outMaterialIdx;

struct DrawElement
{
    int modelMatrixIdx;
    int materialIdx;
};

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform DrawElement drawElements[MAX_DRAW_ELEMENTS];
uniform mat4 modelMatrices[MAX_ENTITIES];
...

最初に気付くのは、バージョンが に増えたこと460です。また、アニメーションに関連付けられた定数 (MAX_WEIGHTSおよびMAX_BONES)、ボーン インデックスの属性、およびボーン マトリックスのユニフォームを削除しました。次の章で、アニメーションにはこの情報が必要ないことがわかります。drawElementsとmodelMatricesユニフォームのサイズを定義する 2 つの新しい定数を作成しました。drawElementsユニフォームはインスタンスを保持しますDrawElement。メッシュと関連付けられたエンティティごとに 1 つのアイテムがあります。覚えていると思いますが、メッシュに関連付けられたすべてのアイテムを描画する単一の命令を記録し、描画するインスタンスの数を設定します。ただし、モデル マトリックスなど、エンティティごとに固有のデータが必要になります。これはdrawElementsこの配列は、使用されるマテリアル インデックスも指します。modelMatrices配列は、各エンティティのモデル マトリックスのみを保持します。outMaterialIdxマテリアル情報は、出力変数を使用して渡すフラグメント シェーダーで使用されます。

mainアニメーションを扱う必要がないため、関数は大幅に単純化されています。

...
void main()
{
    vec4 initPos = vec4(position, 1.0);
    vec4 initNormal = vec4(normal, 0.0);
    vec4 initTangent = vec4(tangent, 0.0);
    vec4 initBitangent = vec4(bitangent, 0.0);

    uint idx = gl_BaseInstance + gl_InstanceID;
    DrawElement drawElement = drawElements[idx];
    outMaterialIdx = drawElement.materialIdx;
    mat4 modelMatrix =  modelMatrices[drawElement.modelMatrixIdx];
    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    outWorldPosition = modelMatrix * initPos;
    outViewPosition  = viewMatrix * outWorldPosition;
    gl_Position   = projectionMatrix * outViewPosition;
    outNormal     = normalize(modelViewMatrix * initNormal).xyz;
    outTangent    = normalize(modelViewMatrix * initTangent).xyz;
    outBitangent  = normalize(modelViewMatrix * initBitangent).xyz;
    outTextCoord  = texCoord;
}

ここで重要なのは、drawElementsサイズにアクセスするための適切なインデックスを取得することです。gl_BaseInstanceおよびgl_InstanceID組み込み変数を使用します。間接描画の指示を記録するときは、baseInstance属性を使用します。その属性の値は、gl_BaseInstance組み込み変数に関連付けられたものになります。は、メッシュから別のメッシュに変更するたびにgl_InstanceID開始さ0れ、モデルに関連付けられたエンティティのインスタンスの数だけ増加します。したがって、この 2 つの変数を組み合わせることで、drawElements配列内のエンティティごとの特定の情報にアクセスできるようになります。適切なインデックスを取得したら、以前のバージョンのシェーダーと同様に、位置と法線情報を変換するだけです。

シーン フラグメント シェーダー ( scene.frag) は次のように定義されます。

#version 400

const int MAX_MATERIALS  = 20;
const int MAX_TEXTURES = 16;

in vec3 outNormal;
in vec3 outTangent;
in vec3 outBitangent;
in vec2 outTextCoord;
in vec4 outViewPosition;
in vec4 outWorldPosition;
flat in uint outMaterialIdx;

layout (location = 0) out vec4 buffAlbedo;
layout (location = 1) out vec4 buffNormal;
layout (location = 2) out vec4 buffSpecular;

struct Material
{
    vec4 diffuse;
    vec4 specular;
    float reflectance;
    int normalMapIdx;
    int textureIdx;
};

uniform sampler2D txtSampler[MAX_TEXTURES];
uniform Material materials[MAX_MATERIALS];

vec3 calcNormal(int idx, vec3 normal, vec3 tangent, vec3 bitangent, vec2 textCoords) {
    mat3 TBN = mat3(tangent, bitangent, normal);
    vec3 newNormal = texture(txtSampler[idx], textCoords).rgb;
    newNormal = normalize(newNormal * 2.0 - 1.0);
    newNormal = normalize(TBN * newNormal);
    return newNormal;
}

void main() {
    Material material = materials[outMaterialIdx];
    vec4 text_color = texture(txtSampler[material.textureIdx], outTextCoord);
    vec4 diffuse = text_color + material.diffuse;
    if (diffuse.a < 0.5) {
        discard;
    }
    vec4 specular = text_color + material.specular;

    vec3 normal = outNormal;
    if (material.normalMapIdx > 0) {
        normal = calcNormal(material.normalMapIdx, outNormal, outTangent, outBitangent, outTextCoord);
    }

    buffAlbedo   = vec4(diffuse.xyz, material.reflectance);
    buffNormal   = vec4(0.5 * normal + 0.5, 1.0);
    buffSpecular = specular;
}

主な変更点は、マテリアル情報とテクスチャへのアクセス方法に関連しています。これで、マテリアル情報の配列が得られます。これは、現在outMaterialIdx入力変数にある頂点シェーダーで計算したインデックスによってアクセスされます (これには、flatこの値を頂点からフラグメント ステージに補間してはならないことを示す修飾子があります)。 . テクスチャの配列を使用して、通常のテクスチャまたは法線マップにアクセスします。これらのテクスチャへのインデックスは、Material構造体に格納されるようになりました。非定数式を使用してサンプラーの配列にアクセスするため、GLSL バージョンを 400 にアップグレードする必要があります (この機能は OpenGL 4.0 以降でのみ使用可能です)。

今度は、SceneRenderクラスの変化を調べる番です。コードで使用される一連の定数、間接描画命令 ( staticRenderBufferHandle) および描画コマンドの数( ) を持つバッファーの 1 つのハンドルを定義することから始めますstaticDrawCount。createUniforms前に示したシェーダーの変更に従って、メソッドを変更する必要もあります。

public class SceneRender {
    ...
    public static final int MAX_DRAW_ELEMENTS = 100;
    public static final int MAX_ENTITIES = 50;
    private static final int COMMAND_SIZE = 5 * 4;
    private static final int MAX_MATERIALS = 20;
    private static final int MAX_TEXTURES = 16;
    ...
    private Map<String, Integer> entitiesIdxMap;
    ...
    private int staticDrawCount;
    private int staticRenderBufferHandle;
    ...
    public SceneRender() {
        ...
        entitiesIdxMap = new HashMap<>();
    }

    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("projectionMatrix");
        uniformsMap.createUniform("viewMatrix");

        for (int i = 0; i < MAX_TEXTURES; i++) {
            uniformsMap.createUniform("txtSampler[" + i + "]");
        }

        for (int i = 0; i < MAX_MATERIALS; i++) {
            String name = "materials[" + i + "]";
            uniformsMap.createUniform(name + ".diffuse");
            uniformsMap.createUniform(name + ".specular");
            uniformsMap.createUniform(name + ".reflectance");
            uniformsMap.createUniform(name + ".normalMapIdx");
            uniformsMap.createUniform(name + ".textureIdx");
        }

        for (int i = 0; i < MAX_DRAW_ELEMENTS; i++) {
            String name = "drawElements[" + i + "]";
            uniformsMap.createUniform(name + ".modelMatrixIdx");
            uniformsMap.createUniform(name + ".materialIdx");
        }

        for (int i = 0; i < MAX_ENTITIES; i++) {
            uniformsMap.createUniform("modelMatrices[" + i + "]");
        }
    }
    ...
}

は、各エンティティが配置されているモデルに関連付けられたエンティティのリスト内のentitiesIdxMap位置を保存します。Mapその情報をエンティティ識別子をキーとして使用して保存します。間接描画コマンドは、各モデルに関連付けられたメッシュを反復して記録されるため、後でこの情報が必要になります。主な変更点はrenderメソッドにあり、次のように定義されています。

public class SceneRender {
    ...
    public void render(Scene scene, RenderBuffers renderBuffers, GBuffer gBuffer) {
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId());
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, gBuffer.getWidth(), gBuffer.getHeight());
        glDisable(GL_BLEND);

        shaderProgram.bind();

        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        uniformsMap.setUniform("viewMatrix", scene.getCamera().getViewMatrix());

        TextureCache textureCache = scene.getTextureCache();
        List<Texture> textures = textureCache.getAll().stream().toList();
        int numTextures = textures.size();
        if (numTextures > MAX_TEXTURES) {
            Logger.warn("Only " + MAX_TEXTURES + " textures can be used");
        }
        for (int i = 0; i < Math.min(MAX_TEXTURES, numTextures); i++) {
            uniformsMap.setUniform("txtSampler[" + i + "]", i);
            Texture texture = textures.get(i);
            glActiveTexture(GL_TEXTURE0 + i);
            texture.bind();
        }

        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                uniformsMap.setUniform("modelMatrices[" + entityIdx + "]", entity.getModelMatrix());
                entityIdx++;
            }
        }

        // Static meshes
        int drawElement = 0;
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    uniformsMap.setUniform(name + ".materialIdx", meshDrawData.materialIdx());
                    drawElement++;
                }
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBindVertexArray(renderBuffers.getStaticVaoId());
        glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, staticDrawCount, 0);
        glBindVertexArray(0);

        glEnable(GL_BLEND);
        shaderProgram.unbind();
    }
    ...
}

テクスチャ サンプラーの配列をバインドし、すべてのテクスチャ ユニットをアクティブにする必要があることがわかります。それに加えて、エンティティを繰り返し処理し、モデル マトリックスに均一な値を設定します。drawElements次のステップは、モデル マトリックスのインデックスとマテリアル インデックスを指す各エンティティの適切な値を使用して、アレイ ユニフォームをセットアップすることです。その後、glMultiDrawElementsIndirect間接描画を行う機能。その前に、描画命令 (描画コマンド) を保持するバッファーと、メッシュとインデックス データを保持する VAO をバインドする必要があります。しかし、いつ間接描画用のバッファを設定するのでしょうか? 答えは、レンダリング コールごとにこれを実行する必要はないということです。エンティティの数に変化がない場合は、そのバッファを 1 回記録して、各レンダリング コールで使用できます。この特定の例では、起動時にそのバッファにデータを入力するだけです。つまり、エンティティの数を変更したい場合は、そのバッファを再度作成する必要があります (独自のエンジンに対して行う必要があります)。

間接描画バッファーを実際に構築するメソッドが呼び出さsetupStaticCommandBufferれ、次のように定義されます。

public class SceneRender {
    ...
    private void setupStaticCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        int numMeshes = 0;
        for (Model model : modelList) {
            numMeshes += model.getMeshDrawDataList().size();
        }

        int firstIndex = 0;
        int baseInstance = 0;
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            int numEntities = entities.size();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(numEntities);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance += entities.size();
            }
        }
        commandBuffer.flip();

        staticDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        staticRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }
    ...
}

最初にメッシュの総数を計算します。その後、間接描画命令を保持するバッファを作成して入力します。ご覧のとおり、最初に を割り当てますByteBuffer。このバッファは、メッシュと同じ数の命令セットを保持します。描画命令の各セットは 5 つの属性で構成され、それぞれの長さは 4 バイトです (パラメーターの各セットの合計の長さがCOMMAND_SIZE定数を定義します)。すぐにスペースが不足するため、このバッファーを使用して割り当てることはできませんMemoryStack(LWJGL がこれに使用するスタックのサイズは制限されています)。したがって、使用して割り当てる必要がありますMemoryUtil完了したら、手動で割り当てを解除することを忘れないでください。バッファを取得したら、モデルに関連付けられたメッシュの反復処理を開始します。この章の冒頭を見て、間接描画に必要な構造体を確認してください。それに加えて、各エンティティのモデル マトリックス インデックスを適切に取得するために、以前に計算した をdrawElements使用してユニフォームを設定します。Map最後に、GPU バッファーを作成し、データをそこにダンプします。

メソッドを更新しcleanupて間接描画バッファを解放する必要があります。

public class SceneRender {
    ...
    public void cleanup() {
        shaderProgram.cleanup();
        glDeleteBuffers(staticRenderBufferHandle);
    }
    ...
}

マテリアル ユニフォームの値を設定するには、新しいメソッドが必要になります。

public class SceneRender {
   private void setupMaterialsUniform(TextureCache textureCache, MaterialCache materialCache) {
        List<Texture> textures = textureCache.getAll().stream().toList();
        int numTextures = textures.size();
        if (numTextures > MAX_TEXTURES) {
            Logger.warn("Only " + MAX_TEXTURES + " textures can be used");
        }
        Map<String, Integer> texturePosMap = new HashMap<>();
        for (int i = 0; i < Math.min(MAX_TEXTURES, numTextures); i++) {
            texturePosMap.put(textures.get(i).getTexturePath(), i);
        }

        shaderProgram.bind();
        List<Material> materialList = materialCache.getMaterialsList();
        int numMaterials = materialList.size();
        for (int i = 0; i < numMaterials; i++) {
            Material material = materialCache.getMaterial(i);
            String name = "materials[" + i + "]";
            uniformsMap.setUniform(name + ".diffuse", material.getDiffuseColor());
            uniformsMap.setUniform(name + ".specular", material.getSpecularColor());
            uniformsMap.setUniform(name + ".reflectance", material.getReflectance());
            String normalMapPath = material.getNormalMapPath();
            int idx = 0;
            if (normalMapPath != null) {
                idx = texturePosMap.computeIfAbsent(normalMapPath, k -> 0);
            }
            uniformsMap.setUniform(name + ".normalMapIdx", idx);
            Texture texture = textureCache.getTexture(material.getTexturePath());
            idx = texturePosMap.computeIfAbsent(texture.getTexturePath(), k -> 0);
            uniformsMap.setUniform(name + ".textureIdx", idx);
        }
        shaderProgram.unbind();
    }
}

サポートされているテクスチャの最大数 ( MAX_TEXTURES) を超えていないことを確認し、前の章で使用した情報を使用してマテリアル情報の配列を作成するだけです。唯一の変更点は、関連するテクスチャと法線マップのインデックスをマテリアル情報に保存する必要があることです。

エンティティ インデックス マップを更新するには、別のメソッドが必要です。

public class SceneRender {
    ...
    private void setupEntitiesData(Scene scene) {
        entitiesIdxMap.clear();
        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }
    }
    ...
}

クラスの変更を完了するには、クラスから呼び出せるようにSceneRenderをラップするメソッドを作成します。setupXXRender

public class SceneRender {
    ...
    public void setupData(Scene scene) {
        setupEntitiesData(scene);
        setupStaticCommandBuffer(scene);
        setupMaterialsUniform(scene.getTextureCache(), scene.getMaterialCache());
    }
    ...
}

影のレンダリング プロセスも間接描画を使用するように変更します。頂点シェーダー ( ) の変更は非常に似ています。アニメーション情報は使用せず、組み込み変数とshadow.vertの組み合わせを使用して適切なモデル マトリックスにアクセスする必要があります。この場合、マテリアル情報は必要ないため、フラグメント シェーダー ( ) は変更されません。gl_BaseInstancegl_InstanceIDshadow.frag

#version 460

const int MAX_DRAW_ELEMENTS = 100;
const int MAX_ENTITIES = 50;

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec3 tangent;
layout (location=3) in vec3 bitangent;
layout (location=4) in vec2 texCoord;

struct DrawElement
{
    int modelMatrixIdx;
};

uniform mat4 modelMatrix;
uniform mat4 projViewMatrix;
uniform DrawElement drawElements[MAX_DRAW_ELEMENTS];
uniform mat4 modelMatrices[MAX_ENTITIES];

void main()
{
    vec4 initPos = vec4(position, 1.0);
    uint idx = gl_BaseInstance + gl_InstanceID;
    int modelMatrixIdx = drawElements[idx].modelMatrixIdx;
    mat4 modelMatrix = modelMatrices[modelMatrixIdx];
    gl_Position = projViewMatrix * modelMatrix * initPos;
}

の変更ShadowRenderも、SceneRenderクラスの変更と非常によく似ています。

public class ShadowRender {

    private static final int COMMAND_SIZE = 5 * 4;
    ...
    private Map<String, Integer> entitiesIdxMap;
    ...
    private int staticRenderBufferHandle;
    ...
    public ShadowRender() {
        ...
        entitiesIdxMap = new HashMap<>();
    }

    public void cleanup() {
        shaderProgram.cleanup();
        shadowBuffer.cleanup();
        glDeleteBuffers(staticRenderBufferHandle);
    }

    private void createUniforms() {
        ...
        for (int i = 0; i < SceneRender.MAX_DRAW_ELEMENTS; i++) {
            String name = "drawElements[" + i + "]";
            uniformsMap.createUniform(name + ".modelMatrixIdx");
        }

        for (int i = 0; i < SceneRender.MAX_ENTITIES; i++) {
            uniformsMap.createUniform("modelMatrices[" + i + "]");
        }
    }
    ...
}

新しいユニフォームを使用するには、createUniformsメソッドを更新する必要がありcleanup、間接描画バッファーを解放する必要があります。renderメソッドは、メッシュとエンティティに対して個別の描画コマンドを送信する代わりに、 を使用するようになりましたglMultiDrawElementsIndirect。

public class ShadowRender {
    ...
    public void render(Scene scene, RenderBuffers renderBuffers) {
        CascadeShadow.updateCascadeShadows(cascadeShadows, scene);

        glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer.getDepthMapFBO());
        glViewport(0, 0, ShadowBuffer.SHADOW_MAP_WIDTH, ShadowBuffer.SHADOW_MAP_HEIGHT);

        shaderProgram.bind();

        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                uniformsMap.setUniform("modelMatrices[" + entityIdx + "]", entity.getModelMatrix());
                entityIdx++;
            }
        }

        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
            glClear(GL_DEPTH_BUFFER_BIT);
        }

        // Static meshes
        int drawElement = 0;
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    drawElement++;
                }
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBindVertexArray(renderBuffers.getStaticVaoId());
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);

            CascadeShadow shadowCascade = cascadeShadows.get(i);
            uniformsMap.setUniform("projViewMatrix", shadowCascade.getProjViewMatrix());

            glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, staticDrawCount, 0);
        }
        glBindVertexArray(0);

        shaderProgram.unbind();
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
    ...
}

最後に、間接描画バッファとエンティティ マップをセットアップする同様のメソッドが必要です。

public class ShadowRender {
    ...
    public void setupData(Scene scene) {
        setupEntitiesData(scene);
        setupStaticCommandBuffer(scene);
    }

    private void setupEntitiesData(Scene scene) {
        entitiesIdxMap.clear();
        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }
    }

    private void setupStaticCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        Map<String, Integer> entitiesIdxMap = new HashMap<>();
        int entityIdx = 0;
        int numMeshes = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            numMeshes += model.getMeshDrawDataList().size();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }

        int firstIndex = 0;
        int baseInstance = 0;
        int drawElement = 0;
        shaderProgram.bind();
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            int numEntities = entities.size();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(numEntities);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance += entities.size();
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    drawElement++;
                }
            }
        }
        commandBuffer.flip();
        shaderProgram.unbind();

        staticDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        staticRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }
}

クラスでは、Renderクラスをインスタンス化し、間接描画バッファと関連データを作成するためにすべてのモデルとエンティティが作成されたときに呼び出すことができるRenderBuffers新しいメソッドを提供するだけです。setupData

public class Render {
    ...
    private RenderBuffers renderBuffers;
    ...
    public Render(Window window) {
        ...
        renderBuffers = new RenderBuffers();
    }

    public void cleanup() {
        ...
        renderBuffers.cleanup();
    }
    ...
    public void render(Window window, Scene scene) {
        shadowRender.render(scene, renderBuffers);
        sceneRender.render(scene, renderBuffers, gBuffer);
        ...
    }
    ...
    public void setupData(Scene scene) {
        renderBuffers.loadStaticModels(scene);
        renderBuffers.loadAnimatedModels(scene);
        sceneRender.setupData(scene);
        shadowRender.setupData(scene);
        List<Model> modelList = new ArrayList<>(scene.getModelMap().values());
        modelList.forEach(m -> m.getMeshDataList().clear());
    }
}

クラスを更新して、TextureCacheすべてのテクスチャを返すメソッドを提供する必要があります。

public class TextureCache {
    ...
    public Collection<Texture> getAll() {
        return textureMap.values();
    }
    ...
}

モデルとマテリアルを扱うクラス階層を変更したため、クラスを更新する必要がありますSkyBox(個々のモデルをロードするには、追加の手順が必要になります)。

public class SkyBox {

    private Material material;
    private Mesh mesh;
    ...
    public SkyBox(String skyBoxModelPath, TextureCache textureCache, MaterialCache materialCache) {
        skyBoxModel = ModelLoader.loadModel("skybox-model", skyBoxModelPath, textureCache, materialCache, false);
        MeshData meshData = skyBoxModel.getMeshDataList().get(0);
        material = materialCache.getMaterial(meshData.getMaterialIdx());
        mesh = new Mesh(meshData);
        skyBoxModel.getMeshDataList().clear();
        skyBoxEntity = new Entity("skyBoxEntity-entity", skyBoxModel.getId());
    }

    public void cleanuo() {
        mesh.cleanup();
    }

    public Material getMaterial() {
        return material;
    }

    public Mesh getMesh() {
        return mesh;
    }
    ...
}

これらの変更はクラスにも影響しSkyBoxRenderます。sky bos render では、間接描画は使用しません (メッシュを 1 つだけレンダリングするため、使用する価値はありません)。

public class SkyBoxRender {
    ...
    public void render(Scene scene) {
        SkyBox skyBox = scene.getSkyBox();
        if (skyBox == null) {
            return;
        }
        shaderProgram.bind();

        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        viewMatrix.set(scene.getCamera().getViewMatrix());
        viewMatrix.m30(0);
        viewMatrix.m31(0);
        viewMatrix.m32(0);
        uniformsMap.setUniform("viewMatrix", viewMatrix);
        uniformsMap.setUniform("txtSampler", 0);

        Entity skyBoxEntity = skyBox.getSkyBoxEntity();
        TextureCache textureCache = scene.getTextureCache();
        Material material = skyBox.getMaterial();
        Mesh mesh = skyBox.getMesh();
        Texture texture = textureCache.getTexture(material.getTexturePath());
        glActiveTexture(GL_TEXTURE0);
        texture.bind();

        uniformsMap.setUniform("diffuse", material.getDiffuseColor());
        uniformsMap.setUniform("hasTexture", texture.getTexturePath().equals(TextureCache.DEFAULT_TEXTURE) ? 0 : 1);

        glBindVertexArray(mesh.getVaoId());

        uniformsMap.setUniform("modelMatrix", skyBoxEntity.getModelMatrix());
        glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);

        glBindVertexArray(0);

        shaderProgram.unbind();
    }
    ...
}

クラスではScene、メソッドを呼び出す必要はありませんScene cleanup(バッファに関連付けられたデータがRenderBuffersクラスにあるため)。

public class Engine {
    ...
    private void cleanup() {
        appLogic.cleanup();
        render.cleanup();
        window.cleanup();
    }
    ...
}

最後に、Mainクラスで、キューブ モデルに関連付けられた 2 つのエンティティを読み込みます。それらを個別にローテーションして、コードが正常に機能することを確認します。最も重要な部分は、すべてがロードされたときにRenderクラスメソッドを呼び出すことです。setupData

public class Main implements IAppLogic {
    ...
    private Entity cubeEntity1;
    private Entity cubeEntity2;
    ...
    private float rotation;

    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-20", opts, main);
        ...
    }

    public void init(Window window, Scene scene, Render render) {
        ...
        Model terrainModel = ModelLoader.loadModel(terrainModelId, "resources/models/terrain/terrain.obj",
                scene.getTextureCache(), scene.getMaterialCache(), false);
        ...
        Model cubeModel = ModelLoader.loadModel("cube-model", "resources/models/cube/cube.obj",
                scene.getTextureCache(), scene.getMaterialCache(), false);
        scene.addModel(cubeModel);
        cubeEntity1 = new Entity("cube-entity-1", cubeModel.getId());
        cubeEntity1.setPosition(0, 2, -1);
        cubeEntity1.updateModelMatrix();
        scene.addEntity(cubeEntity1);

        cubeEntity2 = new Entity("cube-entity-2", cubeModel.getId());
        cubeEntity2.setPosition(-2, 2, -1);
        cubeEntity2.updateModelMatrix();
        scene.addEntity(cubeEntity2);

        render.setupData(scene);
        ...
        SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache(),
                scene.getMaterialCache());
        ...
    }
    ...
    public void update(Window window, Scene scene, long diffTimeMillis) {
        rotation += 1.5;
        if (rotation > 360) {
            rotation = 0;
        }
        cubeEntity1.setRotation(1, 1, 1, (float) Math.toRadians(rotation));
        cubeEntity1.updateModelMatrix();

        cubeEntity2.setRotation(1, 1, 1, (float) Math.toRadians(360 - rotation));
        cubeEntity2.updateModelMatrix();
    }
}

すべての変更を実装すると、これに似たものが表示されるはずです。

投稿者:

takunoji

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

コメントを残す