Java 3D LWJGL GitBook: 第 21 章 – 間接描画 (アニメーション モデル) と計算シェーダー

第 21 章 - 間接描画 (アニメーション モデル) と計算シェーダー

この章では、間接描画を使用する場合のアニメーション モデルのサポートを追加します。そのために、コンピューティング シェーダーという新しいトピックを導入します。計算シェーダーを使用して、モデルの頂点をバインディング ポーズから最終的な位置に変換します (現在のアニメーションに従って)。これが完了したら、通常のシェーダーを使用してそれらをレンダリングできます。レンダリング中にアニメートされたモデルとアニメートされていないモデルを区別する必要はありません。それに加えて、レンダリング プロセスからアニメーション変換を切り離すことができます。そうすることで、アニメーション モデルをレンダリング レートとは異なるレートで更新できるようになります (アニメーション化された頂点が変更されていなければ、各フレームで変換する必要はありません)。

サンプルコードの実行結果

実行するときに、以下の修正を行いました。
<scene.vert>

out uint outMaterialIdx; -> flat out uint outMaterialIdx;

コンセプト

コードを説明する前に、アニメーション モデルの間接描画の背後にある概念を説明しましょう。私たちが従うアプローチは、前の章で使用したものと多かれ少なかれ同じです。頂点データを含むグローバル バッファを作成します。主な違いは、最初に計算シェーダーを使用して頂点をバインディング ポーズから最終ポーズに変換することです。それに加えて、モデルに複数のインスタンスを使用しません。その理由は、同じアニメーション化されたモデルを共有する複数のエンティティがある場合でも、それらは異なるアニメーション状態になる可能性があります (アニメーションが後で開始されたり、更新率が低くなったり、モデルの特定の選択されたアニメーションでさえある可能性があります)異なる場合があります)。したがって、アニメーション化された頂点を含むグローバル バッファ内に、エンティティごとに 1 つのデータ チャンクが必要になります。

データをバインドし続ける必要があります。シーンのすべてのメッシュ用に別のグローバル バッファを作成します。この場合、エンティティごとに別々のチャンクを持つ必要はなく、メッシュごとに 1 つだけです。コンピューティング シェーダーは、バインディング ポーズ データ バッファーにアクセスし、エンティティごとにそれを処理し、静的モデルに使用されるものと同様の構造を持つ別のグローバル バッファーに結果を格納します。

モデルの読み込み

Modelこのクラスにはボーン マトリックス データを保存しないため、クラスを更新する必要があります。代わりに、その情報は共通のバッファに格納されます。したがって、内部クラスAnimatedFrameはもはやレコードになることはできません (レコードは不変です)。

public class Model {
    ...
    public static class AnimatedFrame {
        private Matrix4f[] bonesMatrices;
        private int offset;

        public AnimatedFrame(Matrix4f[] bonesMatrices) {
            this.bonesMatrices = bonesMatrices;
        }

        public void clearData() {
            bonesMatrices = null;
        }

        public Matrix4f[] getBonesMatrices() {
            return bonesMatrices;
        }

        public int getOffset() {
            return offset;
        }

        public void setOffset(int offset) {
            this.offset = offset;
        }
    }
    ...
}

レコードから通常の内部クラスに渡すという事実、クラス属性へのアクセス方法を変更するには、クラスModelをわずかに変更する必要があります。ModelLoader

public class ModelLoader {
    ...
    private static void buildFrameMatrices(AIAnimation aiAnimation, List<Bone> boneList, Model.AnimatedFrame animatedFrame,
                                           int frame, Node node, Matrix4f parentTransformation, Matrix4f globalInverseTransform) {
        ...
        for (Bone bone : affectedBones) {
            ...
            animatedFrame.getBonesMatrices()[bone.boneId()] = boneTransform;
        }
        ...
    }
    ...
}

RenderBuffersクラスで管理される、必要な新しいグローバル バッファを確認しましょう。

public class RenderBuffers {

    private int animVaoId;
    private int bindingPosesBuffer;
    private int bonesIndicesWeightsBuffer;
    private int bonesMatricesBuffer;
    private int destAnimationBuffer;
    ...
    public void cleanup() {
        ...
        glDeleteVertexArrays(animVaoId);
    }
    ...
    public int getAnimVaoId() {
        return animVaoId;
    }

    public int getBindingPosesBuffer() {
        return bindingPosesBuffer;
    }

    public int getBonesIndicesWeightsBuffer() {
        return bonesIndicesWeightsBuffer;
    }

    public int getBonesMatricesBuffer() {
        return bonesMatricesBuffer;
    }

    public int getDestAnimationBuffer() {
        return destAnimationBuffer;
    }
    ...
}

はanimVaoId、変換されたアニメーション頂点を含むデータを定義する VAO を格納します。つまり、計算シェーダーによって処理された後のデータです (メッシュとエンティティごとに 1 つのチャンクを覚えておいてください)。データ自体はバッファに格納され、そのハンドルは に格納されdestAnimationBufferます。VAO を理解しない計算シェーダーでそのバッファーにアクセスする必要があります。バッファーだけです。bonesMatricesBufferまた、ボーン マトリックスとインデックスとウェイトを、それぞれ と で表される 2 つのバッファに格納する必要がありbonesIndicesWeightsBufferます。このcleanup方法では、新しい VAO をきれいにすることを忘れてはなりません。新しい属性のゲッターも追加する必要があります。

loadAnimatedModelsこれで、次のように始まる を実装できます。

public class RenderBuffers {
    ...
    public void loadAnimatedModels(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(Model::isAnimated).toList();
        loadBindingPoses(modelList);
        loadBonesMatricesBuffer(modelList);
        loadBonesIndicesWeights(modelList);

        animVaoId = glGenVertexArrays();
        glBindVertexArray(animVaoId);
        int positionsSize = 0;
        int normalsSize = 0;
        int textureCoordsSize = 0;
        int indicesSize = 0;
        int offset = 0;
        int chunkBindingPoseOffset = 0;
        int bindingPoseOffset = 0;
        int chunkWeightsOffset = 0;
        int weightsOffset = 0;
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                List<RenderBuffers.MeshDrawData> meshDrawDataList = model.getMeshDrawDataList();
                bindingPoseOffset = chunkBindingPoseOffset;
                weightsOffset = chunkWeightsOffset;
                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 + meshData.getNormals().length * 3 + meshData.getTextCoords().length) * 4;
                    meshDrawDataList.add(new MeshDrawData(meshSizeInBytes, meshData.getMaterialIdx(), offset,
                            meshData.getIndices().length, new AnimMeshDrawData(entity, bindingPoseOffset, weightsOffset)));
                    bindingPoseOffset += meshSizeInBytes / 4;
                    int groupSize = (int) Math.ceil((float) meshSizeInBytes / (14 * 4));
                    weightsOffset += groupSize * 2 * 4;
                    offset = positionsSize / 3;
                }
            }
            chunkBindingPoseOffset += bindingPoseOffset;
            chunkWeightsOffset += weightsOffset;
        }

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

        defineVertexAttribs();

        // Index VBO
        int vboId = glGenBuffers();
        vboIdList.add(vboId);
        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indicesSize);
        for (Model model : modelList) {
            model.getEntitiesList().forEach(e -> {
                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);
    }
    ...
}

以下のメソッドがどのように定義されているかについては後で説明しますが、今では:

・loadBindingPoses: アニメートされたモデルに関連付けられたすべてのメッシュのバインド ポーズ情報を格納します。
・loadBonesMatricesBuffer: アニメートされたモデルの各アニメーションのボーン マトリックスを格納します。
・loadBonesIndicesWeights: アニメートされたモデルのボーン インデックスとウェイト情報を格納します。
コードは と非常によく似ていloadStaticModelsます。アニメーション化されたモデルの VAO を作成することから始めて、モデルのメッシュを反復処理します。単一のバッファを使用してすべてのデータを保持するため、これらの要素を繰り返し処理して最終的なバッファ サイズを取得します。最初のループは、静的バージョンとは少し異なることに注意してください。モデルに関連付けられたエンティティを反復処理する必要があり、それらのそれぞれについて、関連付けられたすべてのメッシュのサイズを計算します。

loadBindingPosesメソッドを調べてみましょう。

public class RenderBuffers {
    ...
    private void loadBindingPoses(List<Model> modelList) {
        int meshSize = 0;
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                meshSize += meshData.getPositions().length + meshData.getNormals().length * 3 +
                        meshData.getTextCoords().length + meshData.getIndices().length;
            }
        }

        bindingPosesBuffer = glGenBuffers();
        vboIdList.add(bindingPosesBuffer);
        FloatBuffer meshesBuffer = MemoryUtil.memAllocFloat(meshSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                populateMeshBuffer(meshesBuffer, meshData);
            }
        }
        meshesBuffer.flip();
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, bindingPosesBuffer);
        glBufferData(GL_SHADER_STORAGE_BUFFER, meshesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(meshesBuffer);

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

モデルごとにアニメーション データの反復処理を開始し、すべての情報を保持するバッファを計算するために、アニメーション化されたフレームごとに (すべてのボーンの) 関連する変換行列を取得します。サイズを取得したら、バッファを作成し、(2 番目のループで) それらの行列を入力し始めます。前のバッファーと同様に、計算シェーダーでこのバッファーにアクセスするため、GL_SHADER_STORAGE_BUFFERフラグを使用する必要があります。

メソッドは次のloadBonesIndicesWeightsように定義されます。

public class RenderBuffers {
    ...
    private void loadBonesIndicesWeights(List<Model> modelList) {
        int bufferSize = 0;
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                bufferSize += meshData.getBoneIndices().length * 4 + meshData.getWeights().length * 4;
            }
        }
        ByteBuffer dataBuffer = MemoryUtil.memAlloc(bufferSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                int[] bonesIndices = meshData.getBoneIndices();
                float[] weights = meshData.getWeights();
                int rows = bonesIndices.length / 4;
                for (int row = 0; row < rows; row++) {
                    int startPos = row * 4;
                    dataBuffer.putFloat(weights[startPos]);
                    dataBuffer.putFloat(weights[startPos + 1]);
                    dataBuffer.putFloat(weights[startPos + 2]);
                    dataBuffer.putFloat(weights[startPos + 3]);
                    dataBuffer.putFloat(bonesIndices[startPos]);
                    dataBuffer.putFloat(bonesIndices[startPos + 1]);
                    dataBuffer.putFloat(bonesIndices[startPos + 2]);
                    dataBuffer.putFloat(bonesIndices[startPos + 3]);
                }
            }
        }
        dataBuffer.flip();

        bonesIndicesWeightsBuffer = glGenBuffers();
        vboIdList.add(bonesIndicesWeightsBuffer);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, bonesIndicesWeightsBuffer);
        glBufferData(GL_SHADER_STORAGE_BUFFER, dataBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(dataBuffer);

        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    }
    ...
}

前の方法と同様に、重みとボーン インデックスの情報を 1 つのバッファーに格納するため、最初にそのサイズを計算し、後でデータを入力する必要があります。前のバッファーと同様に、計算シェーダーでこのバッファーにアクセスするため、GL_SHADER_STORAGE_BUFFERフラグを使用する必要があります。

計算シェーダー

今度は、計算シェーダーを介してアニメーション変換を実装する番です。前に述べたように、シェーダーは他のシェーダーと似ていますが、入力と出力に制限はありません。それらを使用してデータを変換し、バインディング ポーズとアニメーション変換マトリックスに関する情報を保持するグローバル バッファーにアクセスし、結果を別のバッファーにダンプします。アニメーション ( ) のシェーダー コードanim.compは次のように定義されます。

#version 460

layout (std430, binding=0) readonly buffer srcBuf {
    float data[];
} srcVector;

layout (std430, binding=1) readonly buffer weightsBuf {
    float data[];
} weightsVector;

layout (std430, binding=2) readonly buffer bonesBuf {
    mat4 data[];
} bonesMatrices;

layout (std430, binding=3) buffer dstBuf {
    float data[];
} dstVector;

struct DrawParameters
{
    int srcOffset;
    int srcSize;
    int weightsOffset;
    int bonesMatricesOffset;
    int dstOffset;
};
uniform DrawParameters drawParameters;

layout (local_size_x=1, local_size_y=1, local_size_z=1) in;

void main()
{
    int baseIdx = int(gl_GlobalInvocationID.x) * 14;
    uint baseIdxWeightsBuf  = drawParameters.weightsOffset + int(gl_GlobalInvocationID.x) * 8;
    uint baseIdxSrcBuf = drawParameters.srcOffset + baseIdx;
    uint baseIdxDstBuf = drawParameters.dstOffset + baseIdx;
    if (baseIdx >= drawParameters.srcSize) {
        return;
    }

    vec4 weights = vec4(weightsVector.data[baseIdxWeightsBuf], weightsVector.data[baseIdxWeightsBuf + 1], weightsVector.data[baseIdxWeightsBuf + 2], weightsVector.data[baseIdxWeightsBuf + 3]);
    ivec4 bonesIndices = ivec4(weightsVector.data[baseIdxWeightsBuf + 4], weightsVector.data[baseIdxWeightsBuf + 5], weightsVector.data[baseIdxWeightsBuf + 6], weightsVector.data[baseIdxWeightsBuf + 7]);

    vec4 position = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 1);
    position =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * position +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * position +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * position +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * position;
    dstVector.data[baseIdxDstBuf] = position.x / position.w;
    dstVector.data[baseIdxDstBuf + 1] = position.y / position.w;
    dstVector.data[baseIdxDstBuf + 2] = position.z / position.w;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 normal = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    normal =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * normal +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * normal +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * normal +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * normal;
    dstVector.data[baseIdxDstBuf] = normal.x;
    dstVector.data[baseIdxDstBuf + 1] = normal.y;
    dstVector.data[baseIdxDstBuf + 2] = normal.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 tangent = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    tangent =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * tangent +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * tangent +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * tangent +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * tangent;
    dstVector.data[baseIdxDstBuf] = tangent.x;
    dstVector.data[baseIdxDstBuf + 1] = tangent.y;
    dstVector.data[baseIdxDstBuf + 2] = tangent.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 bitangent = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    bitangent =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * bitangent +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * bitangent +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * bitangent +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * bitangent;
    dstVector.data[baseIdxDstBuf] = bitangent.x;
    dstVector.data[baseIdxDstBuf + 1] = bitangent.y;
    dstVector.data[baseIdxDstBuf + 2] = bitangent.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec2 textCoords = vec2(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1]);
    dstVector.data[baseIdxDstBuf] = textCoords.x;
    dstVector.data[baseIdxDstBuf + 1] = textCoords.y;
}

ご覧のとおり、コードは前の章でアニメーション (ループの展開) に使用したものと非常によく似ています。データが共通のバッファに格納されるようになったため、メッシュごとにオフセットを適用する必要があることに気付くでしょう。計算シェーダーでプッシュ定数をサポートするため。入力/出力データは、一連のバッファーとして定義されます。

・srcVector: このバッファには、頂点情報 (位置、法線など) が含まれます。
・weightsVector: このバッファには、特定のメッシュとエンティティの現在のアニメーション状態の重みが含まれます。
・bonesMatrices: 同じですが、ボーン マトリックス情報があります。
・dstVector: このバッファは、アニメーション変換を適用した結果を保持します。
興味深いのは、そのオフセットを計算する方法です。このgl_GlobalInvocationID変数には、計算シェーダーで現在実行中の作業項目のインデックスが含まれます。この場合、グローバル バッファにある「チャンク」と同じ数の作業項目を作成します。チャンクは、その位置、法線、テクスチャ座標などの頂点データをモデル化します。したがって、ポート頂点データは、ワークアイテムが増加するたびに、バッファー内を 14 位置 (14 float: 位置の場合は 3) 移動する必要があります。法線の場合は 3、従接線の場合は 3、接線の場合は 3、テクスチャ座標の場合は 2)。同じことが、各頂点に関連付けられたウェイト (4 つの float) とボーン インデックス (4 つの float) のデータを保持するウェイト バッファーにも当てはまります。また、頂点オフセットを使用して、バインディング ポーズ バッファーと宛先バッファーを長く移動します。drawParameters各メッシュとエンティティの ebase オフセットを指すデータ。

このシェーダーは、AnimationRender次のように定義された名前の新しいクラスで使用します。

package org.lwjglb.engine.graph;

import org.lwjglb.engine.scene.*;

import java.util.*;

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

public class AnimationRender {

    private ShaderProgram shaderProgram;
    private UniformsMap uniformsMap;

    public AnimationRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/anim.comp", GL_COMPUTE_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        createUniforms();
    }

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

    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("drawParameters.srcOffset");
        uniformsMap.createUniform("drawParameters.srcSize");
        uniformsMap.createUniform("drawParameters.weightsOffset");
        uniformsMap.createUniform("drawParameters.bonesMatricesOffset");
        uniformsMap.createUniform("drawParameters.dstOffset");
    }

    public void render(Scene scene, RenderBuffers globalBuffer) {
        shaderProgram.bind();
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, globalBuffer.getBindingPosesBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, globalBuffer.getBonesIndicesWeightsBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, globalBuffer.getBonesMatricesBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, globalBuffer.getDestAnimationBuffer());

        int dstOffset = 0;
        for (Model model : scene.getModelMap().values()) {
            if (model.isAnimated()) {
                for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                    RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                    Entity entity = animMeshDrawData.entity();
                    Model.AnimatedFrame frame = entity.getAnimationData().getCurrentFrame();
                    int groupSize = (int) Math.ceil((float) meshDrawData.sizeInBytes() / (14 * 4));
                    uniformsMap.setUniform("drawParameters.srcOffset", animMeshDrawData.bindingPoseOffset());
                    uniformsMap.setUniform("drawParameters.srcSize", meshDrawData.sizeInBytes() / 4);
                    uniformsMap.setUniform("drawParameters.weightsOffset", animMeshDrawData.weightsOffset());
                    uniformsMap.setUniform("drawParameters.bonesMatricesOffset", frame.getOffset());
                    uniformsMap.setUniform("drawParameters.dstOffset", dstOffset);
                    glDispatchCompute(groupSize, 1, 1);
                    dstOffset += meshDrawData.sizeInBytes() / 4;
                }
            }
        }

        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
        shaderProgram.unbind();
    }
}

ご覧のとおり、定義は非常に単純です。シェーダーを作成するときに、GL_COMPUTE_SHADERこれが計算シェーダーであることを示すように を設定する必要があります。不適切に使用されるユニフォームには、バインディング ポーズ バッファ、ウェイトおよびマトリックス バッファ、およびデスティネーション バッファにオフセットが含まれます。このrenderメソッドでは、モデルを繰り返し処理し、各エンティティのメッシュ描画データを取得して、glDispatchCompute. キーは、再びgroupSize変数をトスします。ご覧のとおり、メッシュ内にある頂点チャンクと同じ回数だけシェーダーを呼び出す必要があります。

その他の変更

SceneRenderアニメーション化されたモデルに関連付けられたエンティティをレンダリングするには、クラスを更新する必要があります。変更点を以下に示します。

public class SceneRender {
    ...
    private int animDrawCount;
    private int animRenderBufferHandle;
    ...
    public void cleanup() {
        ...
        glDeleteBuffers(animRenderBufferHandle);
    }
    ...
    public void render(Scene scene, RenderBuffers renderBuffers, GBuffer gBuffer) {
        ...
        // Animated meshes
        drawElement = 0;
        modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                String name = "drawElements[" + drawElement + "]";
                uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                uniformsMap.setUniform(name + ".materialIdx", meshDrawData.materialIdx());
                drawElement++;
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBindVertexArray(renderBuffers.getAnimVaoId());
        glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, animDrawCount, 0);

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

    private void setupAnimCommandBuffer(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) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(1);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance++;
            }
        }
        commandBuffer.flip();
        animDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

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

        MemoryUtil.memFree(commandBuffer);
    }

    public void setupData(Scene scene) {
        ...
        setupAnimCommandBuffer(scene);
        ...
    }
    ...
}

アニメーション モデルをレンダリングするコードは、静的エンティティに使用されるものと非常によく似ています。違いは、同じモデルを共有するエンティティをグループ化していないことです。各エンティティと関連するメッシュの描画命令を記録する必要があります。

ShadowRenderまた、アニメーション モデルをレンダリングするようにクラスを更新する必要があります。

public class ShadowRender {
    ...
    private int animDrawCount;
    private int animRenderBufferHandle;
    ...
    public void cleanup() {
        ...
        glDeleteBuffers(animRenderBufferHandle);
    }
    ...
    public void render(Scene scene, RenderBuffers renderBuffers) {
        ...
        // Anim meshes
        drawElement = 0;
        modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                String name = "drawElements[" + drawElement + "]";
                uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                drawElement++;
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBindVertexArray(renderBuffers.getAnimVaoId());
        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, animDrawCount, 0);
        }

        glBindVertexArray(0);
    }
    private void setupAnimCommandBuffer(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) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(1);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

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

        animDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

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

        MemoryUtil.memFree(commandBuffer);
    }
}

クラスでは、Renderクラスをインスタンス化し、それをループとメソッドAnimationRenderで使用するだけです。ループでは、最初にクラス メソッドを呼び出すため、シーンをレンダリングする前にアニメーション変換が適用されます。rendercleanuprenderAnimationRenderrender

public class Render {

    private AnimationRender animationRender;
    ...
    public Render(Window window) {
        ...
        animationRender = new AnimationRender();
        ...
    }

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

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

最後に、このMainクラスでは、アニメーションの更新レートが異なる 2 つのアニメーション化されたエンティティを作成して、エンティティ情報ごとに正しく分離されていることを確認します。

public class Main implements IAppLogic {
    ...
    private AnimationData animationData1;
    private AnimationData animationData2;
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-21", opts, main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        ...
        String bobModelId = "bobModel";
        Model bobModel = ModelLoader.loadModel(bobModelId, "resources/models/bob/boblamp.md5mesh",
                scene.getTextureCache(), scene.getMaterialCache(), true);
        scene.addModel(bobModel);
        Entity bobEntity = new Entity("bobEntity-1", bobModelId);
        bobEntity.setScale(0.05f);
        bobEntity.updateModelMatrix();
        animationData1 = new AnimationData(bobModel.getAnimationList().get(0));
        bobEntity.setAnimationData(animationData1);
        scene.addEntity(bobEntity);

        Entity bobEntity2 = new Entity("bobEntity-2", bobModelId);
        bobEntity2.setPosition(2, 0, 0);
        bobEntity2.setScale(0.025f);
        bobEntity2.updateModelMatrix();
        animationData2 = new AnimationData(bobModel.getAnimationList().get(0));
        bobEntity2.setAnimationData(animationData2);
        scene.addEntity(bobEntity2);
        ...
    }
    ...
    public void update(Window window, Scene scene, long diffTimeMillis) {
        animationData1.nextFrame();
        if (diffTimeMillis % 2 == 0) {
            animationData2.nextFrame();
        }
        ...
    }
}

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

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();
    }
}

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

Java 3D LWJGL GitBook:第17章 影

第17章 影

現在、光が 3D シーン内のオブジェクトにどのように影響するかを表すことができます。より多くの光を受け取るオブジェクトは、光を受けないオブジェクトよりも明るく表示されます。ただし、まだ影を落とすことはできません。影は、3D シーンのリアリズムの度合いを高めます。これが、この章で行うことです。

シャドウ マッピング

ゲームで広く使用されており、エンジンのパフォーマンスに深刻な影響を与えないシャドウ マッピングという手法を使用します。シャドウ マッピングは簡単に理解できるように見えるかもしれませんが、正しく実装するのはやや困難です。または、より正確に言えば、すべての潜在的なケースをカバーし、一貫した結果を生成する一般的な方法で実装することは非常に困難です。

それでは、特定の領域 (実際にはフラグメント) が影にあるかどうかを確認する方法を考えることから始めましょう。その領域を描画しているときに、光線を光源に投射し、衝突せずに光源に到達できる場合、そのピクセルは光の中にあります。そうでない場合、ピクセルは影になっています。

次の図は、ポイント ライトの場合を示しています。ポイント PA はソース ライトに到達できますが、ポイント PB と PC は到達できないため、影になっています。

そのレイを衝突なしでキャストできるかどうかを効率的に確認するにはどうすればよいでしょうか? 光源は理論的に無限の光線を放つことができますが、光線がブロックされているかどうかを確認するにはどうすればよいでしょうか? レイ ライトをキャストする代わりにできることは、ライトの視点から 3D シーンを見て、その場所からシーンをレンダリングすることです。カメラをライトの位置に設定してシーンをレンダリングし、各フラグメントの深度を保存できるようにします。これは、光源までの各フラグメントの距離を計算することと同じです。最後に、光源から見た最小距離をシャドウ マップとして保存します。

次の図は、平面上に浮かんでいる立方体と垂直なライトを示しています。

光の視点から見たシーンは、このようなものになります (色が濃いほど、光源に近くなります)。

その情報を使用して、通常どおり 3D シーンをレンダリングし、保存されている最小距離で光源までの各フラグメントの距離を確認できます。距離がシャドウ マップに格納されている値よりも小さい場合、オブジェクトは明るくなり、それ以外の場合は影になります。同じレイ ライトが当たる可能性のあるオブジェクトを複数持つことができますが、最小距離を保存します。

したがって、シャドウ マッピングは 2 段階のプロセスです。

・最初に、シーンをライト スペースからシャドウ マップにレンダリングして、最小距離を取得します。
・次に、カメラの視点からシーンをレンダリングし、その深度マップを使用して、オブジェクトが影にあるかどうかを計算します。
深度マップをレンダリングするには、深度バッファについて説明する必要があります。シーンをレンダリングすると、すべての深度情報は、明らかに深度バッファ (または z バッファ) という名前のバッファに格納されます。その深度情報は、
レンダリングされる各フラグメントの値。最初の章で、シーンのレンダリング中に行っていたことを思い出すと、ワールド座標からスクリーン座標に変換されます。範囲の座標空間に描画しています0に1にとってxとy軸。オブジェクトが別のオブジェクトよりも離れている場合、これがオブジェクトにどのように影響するかを計算する必要がありますzとy透視投影行列による座標。によっては自動計算されません。z
価値はありますが、当社で行う必要があります。実際に z 座標に格納されるのは、そのフラグメントの深さであり、それ以下でもそれ以上でもありません。

カスケード シャドウ マップ

上記の解決策は、そのままでは、オープン スペースの質の高い結果を生み出しません。その理由は、影の解像度がテクスチャ サイズによって制限されるためです。現在、潜在的に巨大な領域をカバーしており、深度情報を保存するために使用しているテクスチャは、良い結果を得るには十分な解像度がありません。テクスチャの解像度を上げるだけで解決できると思われるかもしれませんが、これだけでは問題を完全に解決するには不十分です。そのためには、巨大なテクスチャが必要になります。したがって、基本を説明したら、単純なシャドウ マップを改良したカスケード シャドウ マップ (CSM) と呼ばれる手法について説明します。

重要な概念は、カメラに近いオブジェクトの影は、遠くのオブジェクトの影よりも高品質である必要があるということです。1 つの方法として、カメラの近くにあるオブジェクトの影をレンダリングすることもできますが、これではシーン内を移動している限り、影が現れたり消えたりします。

カスケード シャドウ マップ (CSM) が使用するアプローチは、視錐台をいくつかの分割に分割することです。カメラに近い分割はより少ない空間をカバーしますが、遠い領域はより広い領域をカバーします。次の図は、3 つの分割に分割された視錐台を示しています。

これらの分割ごとに深度マップがレンダリングされ、各分割に適合するようにライト ビューと投影マトリックスが調整されます。したがって、深度マップを格納するテクスチャは、視錐台の縮小された領域をカバーします。また、カメラに最も近いスプリットがカバーするスペースが少ないため、深度解像度が向上します。

上記の説明から推測できるように、分割と同じ数の深度テクスチャが必要になり、それぞれのライト ビューと投影マトリックスも変更します。したがって、CSM を適用するために実行する手順は次のとおりです。

  • 視錐台を n 個の分割に分割します。
  • 深度マップのレンダリング中、分割ごとに:
    • ライト ビューと投影行列を計算します。
    • シーンをライトの視点から別の深度マップにレンダリングします
  • シーンのレンダリング中:
    • 上記で計算された深度マップを使用します。
    • 描画するフラグメントが属する分割を決定します。
    • シャドウ マップと同様にシャドウ ファクターを計算します。

のとおり、CSM の主な欠点は、分割ごとにライトの視点からシーンをレンダリングする必要があることです。これが、オープン スペースにのみ使用されることが多い理由です (もちろん、キャッシュをシャドウ計算に適用してオーバーヘッドを削減できます)。

実装

最初に作成するクラスは、ライト パースペクティブからシャドウ マップをレンダリングするために必要なマトリックスを計算します。このクラスには名前が付けられCascadeShadow、特定のカスケード シャドウ スプリット (projViewMatrixアトリビュート) の投影ビュー マトリックス (ライト パースペクティブから) と、その正射影マトリックスのファー プレーン距離(アトリビュート) が格納されsplitDistanceます。

public class CascadeShadow {

    public static final int SHADOW_MAP_CASCADE_COUNT = 3;

    private Matrix4f projViewMatrix;
    private float splitDistance;

    public CascadeShadow() {
        projViewMatrix = new Matrix4f();
    }
    ...
    public Matrix4f getProjViewMatrix() {
        return projViewMatrix;
    }

    public float getSplitDistance() {
        return splitDistance;
    }
    ...
}

このCascadeShadowクラスは、カスケード シャドウ インスタンスのリストを という名前の適切な値で初期化する静的メソッドを定義しますupdateCascadeShadows。このメソッドは次のように始まります。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        Matrix4f viewMatrix = scene.getCamera().getViewMatrix();
        Matrix4f projMatrix = scene.getProjection().getProjMatrix();
        Vector4f lightPos = new Vector4f(scene.getSceneLights().getDirLight().getDirection(), 0);

        float cascadeSplitLambda = 0.95f;

        float[] cascadeSplits = new float[SHADOW_MAP_CASCADE_COUNT];

        float nearClip = projMatrix.perspectiveNear();
        float farClip = projMatrix.perspectiveFar();
        float clipRange = farClip - nearClip;

        float minZ = nearClip;
        float maxZ = nearClip + clipRange;

        float range = maxZ - minZ;
        float ratio = maxZ / minZ;
        ...
    }
    ...
}

まず、シーンのレンダリングに使用する透視投影の分割データ、ビューと投影の行列、ライトの位置、近距離と遠距離のクリップを計算するために必要な行列を取得します。その情報を使用して、各シャドウ カスケードの分割距離を計算できます。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        ...
        // Calculate split depths based on view camera frustum
        // Based on method presented in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html
        for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
            float p = (i + 1) / (float) (SHADOW_MAP_CASCADE_COUNT);
            float log = (float) (minZ * java.lang.Math.pow(ratio, p));
            float uniform = minZ + range * p;
            float d = cascadeSplitLambda * (log - uniform) + uniform;
            cascadeSplits[i] = (d - nearClip) / clipRange;
        }
        ...
    }
    ...
}

分割位置の計算に使用されるアルゴリズムは、対数スキーマを使用して距離をより適切に分散します。カスケードを均等に分割する、または事前に設定された比率に従って分割するなど、他のさまざまなアプローチを使用することもできます。対数スキーマの利点は、近くのビューの分割に使用するスペースが少なく、カメラに近い要素の解像度が高くなることです。数学の詳細については、NVIDIA の記事を参照してください。配列には [0, 1] の範囲の値のセットが含まれます。cascadeSplitsこれを後で使用して、必要な計算を実行し、各カスケードの分割距離と射影行列を取得します。

カスケード分割のすべてのデータを計算するループを定義します。そのループでは、最初に NDC (Normalized Device Coordinates) 空間に錐台コーナーを作成します。その後、ビュー マトリックスとパースペクティブ マトリックスの逆数を使用して、これらの座標をワールド空間に投影します。ディレクショナル ライトを使用しているため、シャドウ マップのレンダリングには正投影行列を使用します。これが、NDC 座標として、可視ボリュームを含む立方体の境界のみを設定する理由です (遠くのオブジェクトはレンダリングされません)。透視投影のように小さくなります)。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        ...
        // Calculate orthographic projection matrix for each cascade
        float lastSplitDist = 0.0f;
        for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
            float splitDist = cascadeSplits[i];

            Vector3f[] frustumCorners = new Vector3f[]{
                    new Vector3f(-1.0f, 1.0f, -1.0f),
                    new Vector3f(1.0f, 1.0f, -1.0f),
                    new Vector3f(1.0f, -1.0f, -1.0f),
                    new Vector3f(-1.0f, -1.0f, -1.0f),
                    new Vector3f(-1.0f, 1.0f, 1.0f),
                    new Vector3f(1.0f, 1.0f, 1.0f),
                    new Vector3f(1.0f, -1.0f, 1.0f),
                    new Vector3f(-1.0f, -1.0f, 1.0f),
            };

            // Project frustum corners into world space
            Matrix4f invCam = (new Matrix4f(projMatrix).mul(viewMatrix)).invert();
            for (int j = 0; j < 8; j++) {
                Vector4f invCorner = new Vector4f(frustumCorners[j], 1.0f).mul(invCam);
                frustumCorners[j] = new Vector3f(invCorner.x / invCorner.w, invCorner.y / invCorner.w, invCorner.z / invCorner.w);
            }
            ...
        }
        ...
    }
    ...
}

この時点で、frustumCorners変数は可視空間を含む立方体の座標を持っていますが、この特定のカスケード分割にはワールド座標が必要です。したがって、次のステップは、それらの方法の最初に計算されたカスケード距離を機能させることです。事前に計算された距離に従って、この特定の分割の近平面と遠平面の座標を調整します。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        ...
        for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
            ...
            for (int j = 0; j < 4; j++) {
                Vector3f dist = new Vector3f(frustumCorners[j + 4]).sub(frustumCorners[j]);
                frustumCorners[j + 4] = new Vector3f(frustumCorners[j]).add(new Vector3f(dist).mul(splitDist));
                frustumCorners[j] = new Vector3f(frustumCorners[j]).add(new Vector3f(dist).mul(lastSplitDist));
            }
            ...
        }
        ...
    }
    ...
}

その後、その分割の中心の座標 (引き続きワールド座標で機能します) と、その分割の半径を計算します。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        ...
        for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
            ...
            // Get frustum center
            Vector3f frustumCenter = new Vector3f(0.0f);
            for (int j = 0; j < 8; j++) {
                frustumCenter.add(frustumCorners[j]);
            }
            frustumCenter.div(8.0f);

            float radius = 0.0f;
            for (int j = 0; j < 8; j++) {
                float distance = (new Vector3f(frustumCorners[j]).sub(frustumCenter)).length();
                radius = java.lang.Math.max(radius, distance);
            }
            radius = (float) java.lang.Math.ceil(radius * 16.0f) / 16.0f;
            ...
        }
        ...
    }
    ...
}

その情報を使用して、ライトの視点と正射投影マトリックス、および分割距離 (カメラ ビュー座標) からビュー マトリックスを計算できるようになりました。

public class CascadeShadow {
    ...
    public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
        ...
        for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
            ...
            Vector3f maxExtents = new Vector3f(radius);
            Vector3f minExtents = new Vector3f(maxExtents).mul(-1);

            Vector3f lightDir = (new Vector3f(lightPos.x, lightPos.y, lightPos.z).mul(-1)).normalize();
            Vector3f eye = new Vector3f(frustumCenter).sub(new Vector3f(lightDir).mul(-minExtents.z));
            Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
            Matrix4f lightViewMatrix = new Matrix4f().lookAt(eye, frustumCenter, up);
            Matrix4f lightOrthoMatrix = new Matrix4f().ortho
                    (minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, 0.0f, maxExtents.z - minExtents.z, true);

            // Store split distance and matrix in cascade
            CascadeShadow cascadeShadow = cascadeShadows.get(i);
            cascadeShadow.splitDistance = (nearClip + splitDist * clipRange) * -1.0f;
            cascadeShadow.projViewMatrix = lightOrthoMatrix.mul(lightViewMatrix);

            lastSplitDist = cascadeSplits[i];
        }
        ...
    }
    ...
}

これで、シャドウ マップのレンダリングに必要な行列を計算するコードが完成しました。したがって、そのレンダリングを実行するために必要なクラスのコーディングを開始できます。この場合、別の画像 (深度画像) にレンダリングします。カスケード マップの分割ごとに 1 つのテクスチャが必要です。それを管理するために、一連のテクスチャを作成するという名前の新しいクラスArrTextureを作成します。これは次のように定義されます。

package org.lwjglb.engine.graph;

import java.nio.ByteBuffer;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL14.GL_TEXTURE_COMPARE_MODE;

public class ArrTexture {

    private final int[] ids;

    public ArrTexture(int numTextures, int width, int height, int pixelFormat) {
        ids = new int[numTextures];
        glGenTextures(ids);

        for (int i = 0; i < numTextures; i++) {
            glBindTexture(GL_TEXTURE_2D, ids[i]);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, pixelFormat, GL_FLOAT, (ByteBuffer) null);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        }
    }

    public void cleanup() {
        for (int id : ids) {
            glDeleteTextures(id);
        }
    }

    public int[] getIds() {
        return ids;
    }
}

GL_CLAMP_TO_EDGEを超えた場合にテクスチャを繰り返さないため、テクスチャ ラッピング モードを に設定します。[0, 1]範囲
空のテクスチャを作成できるようになったので、それにシーンをレンダリングできるようにする必要があります。そのためには、フレーム バッファ オブジェクト (または FBO) を使用する必要があります。フレーム バッファは、レンダリングの宛先として使用できるバッファのコレクションです。画面にレンダリングしているときは、OpenGL のデフォルト バッファを使用しています。OpenGL では、FBO を使用してユーザー定義のバッファーにレンダリングできます。という名前の新しいクラスを作成することにより、シャドウ マッピング用の FBO を作成するプロセスの残りのコードを分離しますShadowBuffer。これがそのクラスの定義です。

package org.lwjglb.engine.graph;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL13.glActiveTexture;
import static org.lwjgl.opengl.GL30.*;

public class ShadowBuffer {

    public static final int SHADOW_MAP_WIDTH = 4096;

    public static final int SHADOW_MAP_HEIGHT = SHADOW_MAP_WIDTH;
    private final ArrTexture depthMap;
    private final int depthMapFBO;

    public ShadowBuffer() {
        // Create a FBO to render the depth map
        depthMapFBO = glGenFramebuffers();

        // Create the depth map textures
        depthMap = new ArrTexture(CascadeShadow.SHADOW_MAP_CASCADE_COUNT, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, GL_DEPTH_COMPONENT);

        // Attach the the depth map texture to the FBO
        glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap.getIds()[0], 0);

        // Set only depth
        glDrawBuffer(GL_NONE);
        glReadBuffer(GL_NONE);

        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
            throw new RuntimeException("Could not create FrameBuffer");
        }

        // Unbind
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    public void bindTextures(int start) {
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glActiveTexture(start + i);
            glBindTexture(GL_TEXTURE_2D, depthMap.getIds()[i]);
        }
    }

    public void cleanup() {
        glDeleteFramebuffers(depthMapFBO);
        depthMap.cleanup();
    }

    public int getDepthMapFBO() {
        return depthMapFBO;
    }

    public ArrTexture getDepthMapTexture() {
        return depthMap;
    }
}

このShadowBufferクラスは、深度マップを保持するテクスチャのサイズを決定する 2 つの定数を定義します。また、FBO 用とテクスチャ用の 2 つの属性も定義します。コンストラクターで、新しい FBO とテクスチャーの配列を作成します。その配列の各要素は、各カスケード シャドウ スプリットのシャドウ マップをレンダリングするために使用されます。FBO では、GL_DEPTH_COMPONENT深度値の保存のみに関心があるため、ピクセル形式として定数を使用します。次に、FBO をテクスチャ インスタンスにアタッチします。

行は、FBO が色をレンダリングしないように明示的に設定します。FBO にはカラー バッファが必要ですが、必要ありません。これが、カラー バッファを として使用するように設定した理由GL_NONEです。

で、シャドウ マップをレンダリングするために、以前のすべてのクラスを機能させることができます。ShadowRender次のように始まる名前の新しいクラスでこれを行います。

package org.lwjglb.engine.graph;

import org.lwjglb.engine.scene.*;

import java.util.*;

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

public class ShadowRender {
    private ArrayList<CascadeShadow> cascadeShadows;
    private ShaderProgram shaderProgram;
    private ShadowBuffer shadowBuffer;
    private UniformsMap uniformsMap;

    public ShadowRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/shadow.vert", GL_VERTEX_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);

        shadowBuffer = new ShadowBuffer();

        cascadeShadows = new ArrayList<>();
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            CascadeShadow cascadeShadow = new CascadeShadow();
            cascadeShadows.add(cascadeShadow);
        }

        createUniforms();
    }

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

    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("modelMatrix");
        uniformsMap.createUniform("projViewMatrix");
        uniformsMap.createUniform("bonesMatrices");
    }

    public List<CascadeShadow> getCascadeShadows() {
        return cascadeShadows;
    }

    public ShadowBuffer getShadowBuffer() {
        return shadowBuffer;
    }
    ...
}

ご覧のとおり、他のレンダリング クラスと非常によく似ています。シェーダー プログラム、必要なユニフォームを作成し、cleanupメソッドを提供します。唯一の例外は次のとおりです。

  • 深さの値に関心があるだけなので、フラグメントシェーダーはまったく必要ありません。頂点シェーダーからの深さを含む頂点位置をダンプするだけです-
  • CascadeShadowカスケード シャドウ スプリット (クラス インスタンスのインスタンスによってモデル化) を作成します。それに加えて、カスケード シャドウ マップとシャドウ マップをレンダリングするバッファを取得するためのゲッターをいくつか提供します。これらのゲッターは、SceneRenderシャドウ マップ データにアクセスするためにクラスで使用されます。
    クラスのrenderメソッドShadowRenderは次のように定義されます。
public class ShadowRender {
    ...
    public void render(Scene scene) {
        CascadeShadow.updateCascadeShadows(cascadeShadows, scene);

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

        shaderProgram.bind();

        Collection<Model> models = scene.getModelMap().values();
        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);

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

            for (Model model : models) {
                List<Entity> entities = model.getEntitiesList();
                for (Material material : model.getMaterialList()) {
                    for (Mesh mesh : material.getMeshList()) {
                        glBindVertexArray(mesh.getVaoId());
                        for (Entity entity : entities) {
                            uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
                            AnimationData animationData = entity.getAnimationData();
                            if (animationData == null) {
                                uniformsMap.setUniform("bonesMatrices", AnimationData.DEFAULT_BONES_MATRICES);
                            } else {
                                uniformsMap.setUniform("bonesMatrices", animationData.getCurrentFrame().boneMatrices());
                            }
                            glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
                        }
                    }
                }
            }
        }

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

最初に行うことは、カスケード マップを更新することです。これは、カスケード マップを更新することです。これは、シャドウ マップをレンダリングできるように、各カスケード スプリットの射影行列です (シーンを更新したり、カメラを移動したり、プレーヤーやアニメーションを変更したりできます)。これは、キャッシュして、シーンが変更された場合に再計算したい場合があります。簡単にするために、フレームごとに行います。その後、glBindFramebuffer関数を呼び出してシャドウ マップをレンダリングするフレーム バッファーをバインドします。それをクリアし、さまざまなカスケード シャドウ スプリットを反復処理します。

分割ごとに、次のアクションを実行します。

  • を呼び出してカスケード シャドウ スプリットに関連付けられたテクスチャをバインドし、glFramebufferTexture2Dそれをクリアします。
  • 現在のカスケード シャドウ スプリットに従って射影行列を更新します。
  • クラスで行っていたように、各エンティティをレンダリングしますSceneRender。
    shadow.vert次のように定義された新しい頂点シェーダー ( ) が必要です。
#version 330

const int MAX_WEIGHTS = 4;
const int MAX_BONES = 150;

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;
layout (location=5) in vec4 boneWeights;
layout (location=6) in ivec4 boneIndices;

uniform mat4 modelMatrix;
uniform mat4 projViewMatrix;
uniform mat4 bonesMatrices[MAX_BONES];

void main()
{
    vec4 initPos = vec4(0, 0, 0, 0);
    int count = 0;
    for (int i = 0; i < MAX_WEIGHTS; i++) {
        float weight = boneWeights[i];
        if (weight > 0) {
            count++;
            int boneIndex = boneIndices[i];
            vec4 tmpPos = bonesMatrices[boneIndex] * vec4(position, 1.0);
            initPos += weight * tmpPos;
        }
    }
    if (count == 0) {
        initPos = vec4(position, 1.0);
    }

    gl_Position = projViewMatrix * modelMatrix * initPos;
}

設定できるように、シーンの頂点シェーダーと同じ入力属性のセットを受け取ります。位置を投影するだけで、モデル マトリックスとアニメーション データに従って以前に入力された位置を更新します。

SceneRender次に、レンダリング時にカスケード シャドウ マップを使用してシャドウを適切に表示するようにクラスを更新する必要があります。まず、フラグメント シェーダーでテクスチャとしてシャドウ マップにアクセスするため、それらのユニフォームを作成する必要があります。また、頂点の位置に応じてどの分割を使用するかを選択するために、カスケード分割の射影行列と分割距離を渡す必要があります。

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            uniformsMap.createUniform("shadowMap_" + i);
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".projViewMatrix");
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".splitDistance");
        }
    }
    ...
}

クラスのrenderメソッドではSceneRender、モデルを調整する前にこれらのユニフォームを設定する必要があります。

public class SceneRender {
    ...
    public void render(Scene scene, ShadowRender shadowRender) {
        ...
        uniformsMap.setUniform("txtSampler", 0);
        uniformsMap.setUniform("normalSampler", 1);

        int start = 2;
        List<CascadeShadow> cascadeShadows = shadowRender.getCascadeShadows();
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            uniformsMap.setUniform("shadowMap_" + i, start + i);
            CascadeShadow cascadeShadow = cascadeShadows.get(i);
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".projViewMatrix", cascadeShadow.getProjViewMatrix());
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".splitDistance", cascadeShadow.getSplitDistance());
        }

        shadowRender.getShadowBuffer().bindTextures(GL_TEXTURE2);
        ...
    }
    ...
}

それでは、シーン シェーダーの変更を見てみましょう。頂点シェーダー ( scene.vert) では、モデル座標の頂点位置も fargemnet シェーダーに渡す必要があります (ビュー マトリックスの影響を受けません)。

#version 330
...
out vec3 outNormal;
out vec3 outTangent;
out vec3 outBitangent;
out vec2 outTextCoord;
out vec3 outViewPosition;
out vec4 outWorldPosition;
...
void main()
{
    ...
    outViewPosition  = mvPosition.xyz;
    outWorldPosition = modelMatrix * initPos;
    ...
}

ほとんどの変更はフラグメント シェーダーに適用されます ( scene.frag)。

#version 330
...
const int DEBUG_SHADOWS = 0;
...
const float BIAS = 0.0005;
const float SHADOW_FACTOR = 0.25;
...
in vec3 outViewPosition;
in vec4 outWorldPosition;

最初に一連の定数を定義します。

  • DEBUG_SHADOWS: これは、割り当てられるカスケード スプリットを識別するためにフラグメントに色を適用するかどうかを制御します (これ1を有効にするには値が必要です)。
  • SHADOW_FACTOR: 影にあるときにフラグメントに適用される塗りつぶしの暗化係数。
  • BIAS: フラグメントが影の影響を受けているかどうかを推定するときに適用する深度バイアス。これは、シャドウ アクネなどのシャドウ アーティファクトを削減するために使用されます。TS シャドウ アクネは、奇妙なアーティファクトを生成する深度マップを格納するテクスチャの解像度が制限されているために生成されます。精度の問題を軽減するしきい値を設定することで、この問題を解決します。
    その後、カスケード スプリットとシャドウ マップのテクスチャを格納する新しいユニフォームを定義します。また、シェーダーに逆ビュー マトリックスを渡す必要があります。前の章では、射影行列の逆行列を使用して、ビュー座標でのフラグメントの位置を取得しました。この場合、一歩先に進み、ワールド座標でもフラグメントの位置を取得する必要があります。逆ビュー行列にビュー座標のフラグメントの位置を掛けると、ワールド座標が得られます。それに加えて、カスケード スプリットの射影ビュー マトリックスとそれらのスプリット距離が必要です。
...
struct CascadeShadow {
    mat4 projViewMatrix;
    float splitDistance;
};
...
uniform CascadeShadow cascadeshadows[NUM_CASCADES];
uniform sampler2D shadowMap_0;
uniform sampler2D shadowMap_1;
uniform sampler2D shadowMap_2;
...

という名前の新しい関数を作成します。この関数はcalcShadow、ワールド位置とカスケード分割インデックスを指定すると、最終的なフラグメント カラーに適用されるシャドウ ファクターを返します。フラグメントが影の影響を受けていない場合、結果は になり1、最終的な色には影響しません。

...
float calcShadow(vec4 worldPosition, int idx) {
    vec4 shadowMapPosition = cascadeshadows[idx].projViewMatrix * worldPosition;
    float shadow = 1.0;
    vec4 shadowCoord = (shadowMapPosition / shadowMapPosition.w) * 0.5 + 0.5;
    shadow = textureProj(shadowCoord, vec2(0, 0), idx);
    return shadow;
}
...

この関数は、正投影を使用して、特定のカスケード スプリットのために、ワールド座標空間からディレクショナル ライトの NDC 空間に変換します。つまり、ワールド空間に、指定されたカスケード スプリットの射影ビュー マトリックスを乗算します。その後、これらの座標をテクスチャ座標 (左上隅から [0, 1] の範囲) に変換する必要があります。その情報を使用textureProjして、使用する適切なシャドウ マップ テクスチャを選択する関数を使用し、結果の値に応じてシャドウ ファクターを適用します。

...
float textureProj(vec4 shadowCoord, vec2 offset, int idx) {
    float shadow = 1.0;

    if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0) {
        float dist = 0.0;
        if (idx == 0) {
            dist = texture(shadowMap_0, vec2(shadowCoord.xy + offset)).r;
        } else if (idx == 1) {
            dist = texture(shadowMap_1, vec2(shadowCoord.xy + offset)).r;
        } else {
            dist = texture(shadowMap_2, vec2(shadowCoord.xy + offset)).r;
        }
        if (shadowCoord.w > 0 && dist < shadowCoord.z - BIAS) {
            shadow = SHADOW_FACTOR;
        }
    }
    return shadow;
}
...

このmain関数では、入力としてビュー位置を取り、カスケード分割ごとに計算された分割距離を反復処理して、このフラグメントが属するカスケード インデックスを決定し、シャドウ ファクターを計算します。

...
void main() {
    ...
    ...
    vec4 diffuseSpecularComp = calcDirLight(diffuse, specular, dirLight, outViewPosition, normal);

    int cascadeIndex;
    for (int i=0; i<NUM_CASCADES - 1; i++) {
        if (outViewPosition.z < cascadeshadows[i].splitDistance) {
            cascadeIndex = i + 1;
            break;
        }
    }
    float shadowFactor = calcShadow(outWorldPosition, cascadeIndex);

    for (int i=0; i<MAX_POINT_LIGHTS; i++) {
        if (pointLights[i].intensity > 0) {
            diffuseSpecularComp += calcPointLight(diffuse, specular, pointLights[i], outViewPosition, normal);
        }
    }

    for (int i=0; i<MAX_SPOT_LIGHTS; i++) {
        if (spotLights[i].pl.intensity > 0) {
            diffuseSpecularComp += calcSpotLight(diffuse, specular, spotLights[i], outViewPosition, normal);
        }
    }
    fragColor = ambient + diffuseSpecularComp;
    fragColor.rgb = fragColor.rgb * shadowFactor;

    if (fog.activeFog == 1) {
        fragColor = calcFog(outViewPosition, fragColor, fog, ambientLight.color, dirLight);
    }

    if (DEBUG_SHADOWS == 1) {
        switch (cascadeIndex) {
            case 0:
            fragColor.rgb *= vec3(1.0f, 0.25f, 0.25f);
            break;
            case 1:
            fragColor.rgb *= vec3(0.25f, 1.0f, 0.25f);
            break;
            case 2:
            fragColor.rgb *= vec3(0.25f, 0.25f, 1.0f);
            break;
            default :
            fragColor.rgb *= vec3(1.0f, 1.0f, 0.25f);
            break;
        }
    }
}

最終的なフラグメントの色は、シャドウ ファクターによって調整されます。最後に、デバッグ モードが有効になっている場合は、そのフラグメントに色を適用して、使用しているカスケードを識別します。

最後に、Renderクラスをインスタンス化して使用するようにクラスを更新する必要がありますShadowRender。また、ブレンディング アクティベーション コードをこのクラスに移動します。

public class Render {
    ...
    private ShadowRender shadowRender;
    ...
    public Render(Window window) {
        ...
        // Support for transparencies
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        ...
        shadowRender = new ShadowRender();
    }

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

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

クラスではMain、サウンド コードを削除するだけです。最後に、次のようなものが表示されます。

DEBUG_SHADOWS定数を に設定する1と、カスケード シャドウがどのように分割されるかがわかります

Java 3D LWJGL GitBook: 第16章 – オーディオ

第16章 - オーディオ

これまではグラフィックスを扱ってきましたが、すべてのゲームのもう 1 つの重要な側面はオーディオです。この章では、サウンドのサポートを追加します。

OpenAL

この章では、OpenAL (Open Audio Library) を使用してオーディオ機能について説明します。OpenAL はオーディオの OpenGL 版であり、抽象化レイヤーを介してサウンドを再生できます。そのレイヤーは、オーディオ サブシステムの根底にある複雑さから私たちを切り離します。それに加えて、3D シーンでサウンドを「レンダリング」することができます。サウンドを特定の場所に設定し、距離に応じて減衰させ、速度に応じて変更することができます (ドップラー効果のシミュレーション) 。

コーディングを開始する前に、OpenAL を扱う際に関係する主な要素を提示する必要があります。

バッファ。
ソース。
リスナー。
バッファには、音楽や効果音などのオーディオ データが格納されます。これらは、OpenGL ドメインのテクスチャに似ています。OpenAL は、オーディオ データが PCM (Pulse Coded Modulation) 形式 (モノラルまたはステレオ) であることを想定しているため、最初に PCM に変換せずに MP3 または OGG ファイルをダンプすることはできません。

次の要素はソースで、音を発する 3D 空間内の位置 (ポイント) を表します。ソースはバッファーに関連付けられ (一度に 1 つだけ)、次の属性で定義できます。

・位置、ソースの場所 ($$x$$、YとZ座標)。ちなみに、OpenAL は OpenGL と同様に右手デカルト座標系を使用するため、(単純化するために) ワールド座標はサウンド空間座標系の座標と同等であると想定できます。
・ソースの移動速度を指定する速度。これは、ドップラー効果をシミュレートするために使用されます。
・サウンドの強さを変更するために使用されるゲイン (アンプ係数のようなもの)。

ソースには、後でソース コードを説明するときに表示される追加の属性があります。

最後になりましたが、生成されたサウンドが聞こえるはずのリスナーです。Listener は、マイクが 3D オーディオ シーンでサウンドを受信するように設定されていることを表します。リスナーは 1 人だけです。そのため、オーディオのレンダリングはリスナーの視点から行われるとよく​​言われます。リスナーはいくつかの属性を共有しますが、向きなどの追加の属性があります。方向は、リスナーが向いている方向を表します。

つまり、オーディオ 3D シーンは、音を発する一連の音源と、それらを受信するリスナーによって構成されます。最終的に知覚される音は、リスナーからさまざまなソースまでの距離、それらの相対速度、および選択した伝播モデルによって異なります。ソースはバッファを共有し、同じデータを再生できます。次の図は、さまざまな要素タイプが含まれるサンプル 3D シーンを示しています。

実装

OpenAL を使用するには、最初に maven の依存関係をプロジェクト pom.xml に追加します。コンパイル時と実行時の依存関係を追加する必要があります。

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

それでは、コーディングを始めましょう。org.lwjglb.engine.soundオーディオの処理を担当するすべてのクラスをホストする名前で新しいパッケージを作成します。まずSoundBuffer、OpenAL バッファを表すという名前のクラスから始めます。そのクラスの定義の一部を以下に示します。

package org.lwjglb.engine.sound;

import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.*;

import java.nio.*;

import static org.lwjgl.openal.AL10.*;
import static org.lwjgl.stb.STBVorbis.*;
import static org.lwjgl.system.MemoryUtil.NULL;

public class SoundBuffer {
    private final int bufferId;

    private ShortBuffer pcm;

    public SoundBuffer(String filePath) {
        this.bufferId = alGenBuffers();
        try (STBVorbisInfo info = STBVorbisInfo.malloc()) {
            pcm = readVorbis(filePath, info);

            // Copy to buffer
            alBufferData(bufferId, info.channels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, pcm, info.sample_rate());
        }
    }

    public void cleanup() {
        alDeleteBuffers(this.bufferId);
        if (pcm != null) {
            MemoryUtil.memFree(pcm);
        }
    }

    public int getBufferId() {
        return this.bufferId;
    }

    private ShortBuffer readVorbis(String filePath, STBVorbisInfo info) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            IntBuffer error = stack.mallocInt(1);
            long decoder = stb_vorbis_open_filename(filePath, error, null);
            if (decoder == NULL) {
                throw new RuntimeException("Failed to open Ogg Vorbis file. Error: " + error.get(0));
            }

            stb_vorbis_get_info(decoder, info);

            int channels = info.channels();

            int lengthSamples = stb_vorbis_stream_length_in_samples(decoder);

            ShortBuffer result = MemoryUtil.memAllocShort(lengthSamples * channels);

            result.limit(stb_vorbis_get_samples_short_interleaved(decoder, channels, result) * channels);
            stb_vorbis_close(decoder);

            return result;
        }
    }
}

クラスのコンストラクターはサウンド ファイル パスを想定し、そこから新しいバッファーを作成します。最初に行うことは、 への呼び出しで OpenAL バッファを作成することalGenBuffersです。最後に、サウンド バッファは、それが保持するデータへのポインタのような整数によって識別されます。バッファーが作成されたら、その中にオーディオ データをダンプします。コンストラクターは OGG 形式のファイルを想定しているため、PCM 形式に変換する必要があります。これはreadVorbis メソッドで行われます。

以前のバージョンの LWJGL にはWaveData、WAV 形式のオーディオ ファイルをロードするために使用される という名前のヘルパー クラスがありました。このクラスは LWJGL 3 にはもう存在しません。それでも、そのクラスからソース コードを取得して、ゲームで使用することができます (おそらく変更は必要ありません)。

このSoundBufferクラスはcleanup、リソースを使い終わったときにリソースを解放するメソッドも提供します。

という名前のクラスによって実装される OpenAL のモデル化を続けましょうSoundSource。クラスは以下に定義されています。

package org.lwjglb.engine.sound;

import org.joml.Vector3f;

import static org.lwjgl.openal.AL10.*;

public class SoundSource {

    private final int sourceId;

    public SoundSource(boolean loop, boolean relative) {
        this.sourceId = alGenSources();
        alSourcei(sourceId, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
        alSourcei(sourceId, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE);
    }

    public void cleanup() {
        stop();
        alDeleteSources(sourceId);
    }

    public boolean isPlaying() {
        return alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PLAYING;
    }

    public void pause() {
        alSourcePause(sourceId);
    }

    public void play() {
        alSourcePlay(sourceId);
    }

    public void setBuffer(int bufferId) {
        stop();
        alSourcei(sourceId, AL_BUFFER, bufferId);
    }

    public void setGain(float gain) {
        alSourcef(sourceId, AL_GAIN, gain);
    }

    public void setPosition(Vector3f position) {
        alSource3f(sourceId, AL_POSITION, position.x, position.y, position.z);
    }

    public void stop() {
        alSourceStop(sourceId);
    }
}

音源クラスは、その位置、ゲイン、および再生、停止、一時停止を制御するメソッドを設定するいくつかのメソッドを提供します。複数のソースが同じバッファを共有できることに注意してください。SoundBufferクラスと同様に、aSoundSourceは各操作で使用される識別子によって識別されます。このクラスはcleanup、予約されたリソースを解放するメソッドも提供します。しかし、コンストラクターを調べてみましょう。alGenSources最初に行うことは、呼び出しでソースを作成することです。次に、コンストラクターのパラメーターを使用していくつかの興味深いプロパティを設定します。

最初のパラメータ はloop、再生するサウンドをループ モードにするかどうかを示します。デフォルトでは、再生アクションがソースに対して呼び出されると、オーディオ データが消費されると再生が停止します。一部のサウンドではこれで問題ありませんが、バックグラウンド ミュージックなど、何度も再生する必要があるサウンドもあります。オーディオがいつ停止して再生プロセスを再開するかを手動で制御する代わりに、単純に looping プロパティを true に設定します: “ alSourcei(sourceId, AL_LOOPING, AL_TRUE);”.

もう 1 つのパラメータrelativeは、ソースの位置がリスナーに対して相対的かどうかを制御します。この場合、ソースの位置を設定するときは、基本的に、OpenAL 3D シーンの位置ではなく、世界の位置ではなく、リスナーまでの距離 (ベクトルを使用) を定義しています。これは、「alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);”呼び出しによってアクティブ化されます。しかし、これを何に使用できますか?このプロパティは、たとえば、リスナーまでの距離によって影響を受けてはならない (減衰されてはならない) バックグラウンド サウンドの場合に役立ちます。たとえば、プレーヤーのコントロールに関連するバックグラウンド ミュージックやサウンド エフェクトを考えてみましょう。これらのソースを相対として設定し、それらの位置を(0,0,0)それらは減衰されません。

今度はリスナーの番です。これは、驚いたことに、 という名前のクラスによってモデル化されていSoundListenerます。これがそのクラスの定義です。

package org.lwjglb.engine.sound;

import org.joml.Vector3f;

import static org.lwjgl.openal.AL10.*;

public class SoundListener {

    public SoundListener(Vector3f position) {
        alListener3f(AL_POSITION, position.x, position.y, position.z);
        alListener3f(AL_VELOCITY, 0, 0, 0);
    }

    public void setOrientation(Vector3f at, Vector3f up) {
        float[] data = new float[6];
        data[0] = at.x;
        data[1] = at.y;
        data[2] = at.z;
        data[3] = up.x;
        data[4] = up.y;
        data[5] = up.z;
        alListenerfv(AL_ORIENTATION, data);
    }

    public void setPosition(Vector3f position) {
        alListener3f(AL_POSITION, position.x, position.y, position.z);
    }

    public void setSpeed(Vector3f speed) {
        alListener3f(AL_VELOCITY, speed.x, speed.y, speed.z);
    }
}

前のクラスとの違いは、リスナーを作成する必要がないことです。リスナーは常に 1 つ存在するため、作成する必要はありません。既に用意されています。したがって、コンストラクターでは、単に初期位置を設定するだけです。同じ理由で、cleanupメソッドは必要ありません。クラスには、クラスのようにリスナーの位置と速度を設定するためのメソッドもありますSoundSourceが、リスナーの向きを変更するための追加のメソッドがあります。オリエンテーションとは何かを確認しましょう。リスナーの向きは、次の図に示すように、「at」ベクトルと「up」ベクトルの 2 つのベクトルによって定義されます。

「at」ベクトルは基本的にリスナーが向いている方向を指し、デフォルトではその座標は(0,0,-1). 「上」ベクトルは、リスナーにとって上向きの方向を決定し、デフォルトでは(0,1,0)したがって、これら 2 つのベクトルのそれぞれの 3 つのコンポーネントが、alListenerfvメソッド呼び出しで設定されます。このメソッドは、float のセット (可変数の float) をプロパティ (この場合は向き) に転送するために使用されます。

続行する前に、ソースとリスナーの速度に関するいくつかの概念を強調する必要があります。ソースとリスナー間の相対速度により、OpenAL はドップラー効果をシミュレートします。ご存じないかもしれませんが、ドップラー効果は、あなたに近づいている移動物体が、遠ざかるときよりも高い周波数で放射しているように見える原因です. 問題は、単にソースまたはリスナーの速度を設定するだけでは、OpenAL はそれらの位置を更新しないということです。相対速度を使用してドップラー効果を計算しますが、位置は変更されません。そのため、移動するソースまたはリスナーをシミュレートする場合は、ゲーム ループ内でそれらの位置を更新する必要があります。

主要な要素をモデル化したので、それらを機能するように設定できます。OpenAL ライブラリを初期化する必要があるため、これを処理するという名前の新しいクラスを作成し、次のSoundManagerように開始します。

package org.lwjglb.engine.sound;

import org.joml.*;
import org.lwjgl.openal.*;
import org.lwjglb.engine.scene.Camera;

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

import static org.lwjgl.openal.AL10.alDistanceModel;
import static org.lwjgl.openal.ALC10.*;
import static org.lwjgl.system.MemoryUtil.NULL;

public class SoundManager {

    private final List<SoundBuffer> soundBufferList;
    private final Map<String, SoundSource> soundSourceMap;
    private long context;
    private long device;
    private SoundListener listener;

    public SoundManager() {
        soundBufferList = new ArrayList<>();
        soundSourceMap = new HashMap<>();

        device = alcOpenDevice((ByteBuffer) null);
        if (device == NULL) {
            throw new IllegalStateException("Failed to open the default OpenAL device.");
        }
        ALCCapabilities deviceCaps = ALC.createCapabilities(device);
        this.context = alcCreateContext(device, (IntBuffer) null);
        if (context == NULL) {
            throw new IllegalStateException("Failed to create OpenAL context.");
        }
        alcMakeContextCurrent(context);
        AL.createCapabilities(deviceCaps);
    }
    ...
}

このクラスは、SoundBufferおよびSoundSourceインスタンスへの参照を保持して、それらを追跡し、後で適切にクリーンアップします。SoundBuffers は List に格納されますが、SoundSources は に格納されるMapため、名前で取得できます。コンストラクターは OpenAL サブシステムを初期化します。

・デフォルトのデバイスを開きます。
・そのデバイスの機能を作成します。
・OpenGL のようなサウンド コンテキストを作成し、それを現在のコンテキストとして設定します。
このSoundManagerクラスは、音源とバッファを追加するメソッドと、cleanupすべてのリソースを解放するメソッドを定義します。

public class SoundManager {
    ...
    public void addSoundBuffer(SoundBuffer soundBuffer) {
        this.soundBufferList.add(soundBuffer);
    }

    public void addSoundSource(String name, SoundSource soundSource) {
        this.soundSourceMap.put(name, soundSource);
    }

    public void cleanup() {
        soundSourceMap.values().forEach(SoundSource::cleanup);
        soundSourceMap.clear();
        soundBufferList.forEach(SoundBuffer::cleanup);
        soundBufferList.clear();
        if (context != NULL) {
            alcDestroyContext(context);
        }
        if (device != NULL) {
            alcCloseDevice(device);
        }
    }
    ...
}

また、リスナーとソースを管理するメソッドと、playSoundSourceその名前を使用してサウンドを有効にするメソッドも提供します。

public class SoundManager {
    ...
    public SoundListener getListener() {
        return this.listener;
    }

    public SoundSource getSoundSource(String name) {
        return this.soundSourceMap.get(name);
    }

    public void playSoundSource(String name) {
        SoundSource soundSource = this.soundSourceMap.get(name);
        if (soundSource != null && !soundSource.isPlaying()) {
            soundSource.play();
        }
    }

    public void removeSoundSource(String name) {
        this.soundSourceMap.remove(name);
    }

    public void setAttenuationModel(int model) {
        alDistanceModel(model);
    }

    public void setListener(SoundListener listener) {
        this.listener = listener;
    }
    ...
}

このSoundManagerクラスには、カメラ位置を指定してリスナーの向きを更新するメソッドもあります。私たちの場合、カメラがあるときはいつでもリスナーが配置されます。では、カメラの位置と回転の情報が与えられた場合、「at」ベクトルと「up」ベクトルをどのように計算するのでしょうか? 答えは、カメラに関連付けられたビュー マトリックスを使用することです。「で」を変換する必要があります(0,0,-1)
そして「アップ」(0,1,0)
カメラの回転を考慮したベクトル。カメラに関連付けcameraMatrixられたビュー マトリックスを とします。それを達成するためのコードは次のとおりです。

public class SoundManager {
    ...
    public void updateListenerPosition(Camera camera) {
        Matrix4f viewMatrix = camera.getViewMatrix();
        listener.setPosition(camera.getPosition());
        Vector3f at = new Vector3f();
        viewMatrix.positiveZ(at).negate();
        Vector3f up = new Vector3f();
        viewMatrix.positiveY(up);
        listener.setOrientation(at, up);
    }
    ...
}

上記のコードは、以前に説明した説明と同等であり、より効率的なアプローチです。完全な逆行列を計算する必要がないだけで同じ結果が得られる、 JOMLライブラリで利用可能な高速な方法を使用します。このメソッドはLWJGL フォーラムでJOML の作成者によって提供されたものなので、そこで詳細を確認できます。ソース コードを確認すると、SoundManagerクラスがビュー マトリックスの独自のコピーを計算することがわかります。

それだけです。サウンドを再生するために必要なインフラストラクチャはすべて揃っています。Mainバックグラウンド サウンドを設定するクラスで使用するだけでよく、特定のアニメーション フレームで特定のサウンドがリスナーの位置に相対的な強度でアクティブになります。

public class Main implements IAppLogic {
    ...
    private SoundSource playerSoundSource;
    private SoundManager soundMgr;

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

    @Override
    public void cleanup() {
        soundMgr.cleanup();
    }

    @Override
    public void init(Window window, Scene scene, Render render) {
        ...
        lightAngle = 45;
        initSounds(bobEntity.getPosition(), camera);
    }

    private void initSounds(Vector3f position, Camera camera) {
        soundMgr = new SoundManager();
        soundMgr.setAttenuationModel(AL11.AL_EXPONENT_DISTANCE);
        soundMgr.setListener(new SoundListener(camera.getPosition()));

        SoundBuffer buffer = new SoundBuffer("resources/sounds/creak1.ogg");
        soundMgr.addSoundBuffer(buffer);
        playerSoundSource = new SoundSource(false, false);
        playerSoundSource.setPosition(position);
        playerSoundSource.setBuffer(buffer.getBufferId());
        soundMgr.addSoundSource("CREAK", playerSoundSource);

        buffer = new SoundBuffer("resources/sounds/woo_scary.ogg");
        soundMgr.addSoundBuffer(buffer);
        SoundSource source = new SoundSource(true, true);
        source.setBuffer(buffer.getBufferId());
        soundMgr.addSoundSource("MUSIC", source);
        source.play();
    }

    @Override
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        ...
        soundMgr.updateListenerPosition(camera);
    }

    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        animationData.nextFrame();
        if (animationData.getCurrentFrameIdx() == 45) {
            playerSoundSource.play();
        }
    }
}

最後のメモ。AL11.AL_EXPONENT_DISTANCEOpenAL では、alDistanceModel を使用して必要なモデル ( 、AL_EXPONENT_DISTANCE_CLAMPなど)を渡すことにより、減衰モデルを変更することもできます。それらで遊んで、結果を確認できます。

Java 3D LWJGL GitBook: 第15章 – アニメーション

第15章 - アニメーション

これまでは静的な 3D モデルしかロードしていませんでしたが、この章ではそれらをアニメーション化する方法を学びます。アニメーションについて考えるとき、最初のアプローチは、モデルの位置ごとに異なるメッシュを作成し、それらを GPU にロードし、それらを順番に描画して動きの錯覚を作成することです。このアプローチは一部のゲームには最適ですが、メモリ消費に関してはあまり効率的ではありません。ここで骨格アニメーションが活躍します。assimpを使用してこれらのモデルをロードする方法を学習します。

この章の完全なソース コードは、ここにあります。

アンチエイリアシングのサポート

この章では、アンチエイリアスのサポートも追加します。この瞬間まで、モデルにのこぎりのようなエッジが見られたかもしれません。これらの影響を取り除くために、基本的にいくつかのサンプルの値を使用して各ピクセルの最終的な値を構築するアンチエイリアシングを適用します。この場合、4 つのサンプル値を使用します。イメージを作成する前に、これをウィンドウ ヒントとして設定する必要があります (それを制御する新しいウィンドウ オプションを追加します)。

public class Window {
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        ...
        if (opts.antiAliasing) {
            glfwWindowHint(GLFW_SAMPLES, 4);
        }
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
        ...
    }
    ...
    public static class WindowOptions {
        public boolean antiAliasing;
        ...
    }
}

このRenderクラスでは、マルチサンプリングを有効にする必要があります (それに加えて、サンプル モデルを適切にレンダリングするために顔のカリングを削除します)。

public class Render {
    ...
    public Render(Window window) {
        GL.createCapabilities();
        glEnable(GL_MULTISAMPLE);
        glEnable(GL_DEPTH_TEST);

        sceneRender = new SceneRender();
        guiRender = new GuiRender(window);
        skyBoxRender = new SkyBoxRender();
    }
    ...
}

序章

スケルトン アニメーションでは、モデルがアニメーション化される方法は、その下にあるスケルトンによって定義されます。スケルトンは、ボーンと呼ばれる特別な要素の階層によって定義されます。これらのボーンは、位置と回転によって定義されます。これは階層であるとも言いました。つまり、各ボーンの最終的な位置は、親の位置の影響を受けます。たとえば、手首について考えてみましょう。キャラクターが肘を動かしたり、肩を動かしたりすると、手首の位置が変更されます。

骨は、物理的な骨や関節を表す必要はありません。骨は、クリエイティブがアニメーションをモデル化できるようにするアーティファクトです。ボーンに加えて、3D モデルを構成する三角形を定義するポイントである頂点があります。しかし、スケルトン アニメーションでは、関連するボーンの位置に基づいて頂点が描画されます。

この章では、さまざまな情報源を参考にしましたが、アニメーション モデルの作成方法について非常に適切に説明している 2 つの情報源を見つけました。論文のソースは次の場所で参照できます。

現在のコードでアニメーションを含むモデルをロードすると、バインディング ポーズと呼ばれるものが得られます。(前の章のコードで) それを試すことができ、3D モデルを完全に見ることができます。バインディング ポイズは、アニメーションの影響をまったく受けずに、モデルの位置法線、テクスチャ座標を定義します。アニメーション化されたモデルは、基本的に次の追加情報を定義します。
・ 変換を構成できる階層を定義するボーンによって構成されたツリーのような構造。
・ 各メッシュには、頂点の位置、法線などに関する情報が含まれているだけでなく、この頂点がどのボーンに関連しているか (ボーン インデックスを使用) と、それらがどの程度影響を受けているか (つまり、重み係数を使用してエフェクトを調整している) に関する情報が含まれます。 .
・ 各ボーンに適用する必要がある特定の変換を定義する一連のアニメーション キー フレームは、拡張によって関連する頂点を変更します。モデルは複数のアニメーションを定義でき、それぞれが複数のアニメーション キー フレームで構成されている場合があります。アニメーションの場合、これらのキー フレーム (期間を定義する) を反復処理し、それらの間で相互運用することもできます。基本的に、特定の瞬間に、関連するボーンに関連付けられた変換を各頂点に適用します。

まず、アニメーション情報を含む assimp が扱う構造体をおさらいしましょう。まず、ボーンとウェイトの情報から始めます。それぞれについてAIMesh、頂点の位置、テクスチャ座標、およびインデックスにアクセスできます。メッシュにはボーンのリストも保存されます。各ボーンは、次の属性によって定義されます。
・名前。
・オフセット マトリックス: これは後で各ボーンで使用される最終的な変換を計算するために使用されます。
ボーンは重みのリストも指します。各重みは、次の属性によって定義されます。

・重み係数、つまり、各頂点に関連付けられたボーンの変換の影響を調整するために使用される数値です。
・頂点識別子、つまり現在のボーンに関連付けられている頂点。
次の図は、これらすべての要素間の関係を示しています。

したがって、各頂点は、位置、法線、およびテクスチャ座標を含むだけでなく、それらの頂点に影響を与えるボーンの一連のインデックス (通常は 4 つの値) ( jointIndices) と、その効果を調整する一連のウェイトを持ちます。各頂点は、最終的な位置を計算するために、各ジョイントに関連付けられた変換行列に従って変更されます。したがって、次の図に示すように、各メッシュに関連付けられた VAO を拡張してその情報を保持する必要があります。

Assimp シーン オブジェクトは、ノードの階層を定義します。各ノードは、名前と子ノードのリストによって定義されます。アニメーションはこれらのノードを使用して、適用する変換を定義します。この階層は、実際にボーンの階層として定義されます。すべてのボーンはノードであり、ルート ノードを除く親と、場合によっては子のセットを持ちます。ボーンではない特別なノードがあり、変換をグループ化するために使用され、変換を計算するときに処理する必要があります。もう 1 つの問題は、このノード階層がモデル全体から定義されていることです。メッシュごとに個別の階層はありません。

シーンは、一連のアニメーションも定義します。1 つのモデルに複数のアニメーションを設定して、キャラクターの歩き方、走り方などをモデル化できます。これらのアニメーションはそれぞれ、異なる変換を定義します。アニメーションには次の属性があります。

・名前。
・期間。つまり、アニメーションの継続時間です。アニメーションは、異なるフレームごとに各ノードに適用する必要がある変換のリストであるため、名前がわかりにくいかもしれません。
・アニメーション チャンネルのリスト。アニメーション チャネルには、特定の瞬間に、各ノードに適用する必要がある移動、回転、スケーリングの情報が含まれます。アニメーション チャネルに含まれるデータをモデル化するクラスはAINodeAnim. アニメーション チャネルは、キー フレームとして同化できます。
次の図は、上記のすべての要素間の関係を示しています。

フレームの特定の瞬間に、ボーンに適用される変換は、その瞬間のアニメーション チャネルで定義された変換に、ルート ノードまでのすべての親ノードの変換を乗算したものです。したがって、シーンに保存されている情報を抽出する必要があります。プロセスは次のとおりです。

・ノード階層を構築します。
・アニメーションごとに、(アニメーション ノードごとに) 各アニメーション チャネルを反復処理し、考えられるすべてのアニメーション フレームの各ボーンの変換行列を作成します。これらの変換行列は、骨に関連付けられたノードの変換行列と骨変換行列の組み合わせです。
・ルート ノードから開始し、フレームごとに、そのノードの変換マトリックスを作成します。これは、ノードの変換マトリックスに、そのノードの特定のフレームの移動、回転、スケール マトリックスの構成を掛けたものです。
・次に、そのノードに関連付けられたボーンを取得し、ボーンのオフセット マトリックスを乗算してその変換を補完します。結果は、その特定のフレームの関連するボーンに関連付けられた変換マトリックスになり、シェーダーで使用されます。
・その後、子ノードを繰り返し処理し、親ノードの変換行列を渡し、子ノードの変換と組み合わせて使用​​します。

実装

ModelLoaderクラスの変更を分析することから始めましょう。

public class ModelLoader {

    public static final int MAX_BONES = 150;
    private static final Matrix4f IDENTITY_MATRIX = new Matrix4f();
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, boolean animation) {
        return loadModel(modelId, modelPath, textureCache, aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices |
                aiProcess_Triangulate | aiProcess_FixInfacingNormals | aiProcess_CalcTangentSpace | aiProcess_LimitBoneWeights |
                (animation ? 0 : aiProcess_PreTransformVertices));

    }
    ...
}

アニメーション付きのモデルをロードしているかどうかを示すために、メソッドに追加の引数 ( という名前animation) が必要です。loadModelその場合、aiProcess_PreTransformVerticesフラグは使用できません。このフラグは、ロードされたデータに対して何らかの変換を実行するため、モデルは原点に配置され、座標は数学 OpenGL 座標系に修正されます。このフラグはアニメーション データ情報を削除するため、アニメーション モデルには使用できません。

メッシュを処理している間、メッシュを処理しているときに、各頂点に関連付けられているボーンとウェイトも処理します。それらを処理している間、必要な変換を後で構築できるように、ボーンのリストを保存します。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {
        ...
        List<Bone> boneList = new ArrayList<>();
        for (int i = 0; i < numMeshes; i++) {
            AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
            Mesh mesh = processMesh(aiMesh, boneList);
            ...
        }
        ...
    }
    ...
    private static Mesh processMesh(AIMesh aiMesh, List<Bone> boneList) {
        ...
        AnimMeshData animMeshData = processBones(aiMesh, boneList);
        ...
        return new Mesh(vertices, normals, tangents, bitangents, textCoords, indices, animMeshData.boneIds, animMeshData.weights);
    }
    ...
}

新しいメソッドprocessBonesは次のように定義されます。

public class ModelLoader {
    ...
    private static AnimMeshData processBones(AIMesh aiMesh, List<Bone> boneList) {
        List<Integer> boneIds = new ArrayList<>();
        List<Float> weights = new ArrayList<>();

        Map<Integer, List<VertexWeight>> weightSet = new HashMap<>();
        int numBones = aiMesh.mNumBones();
        PointerBuffer aiBones = aiMesh.mBones();
        for (int i = 0; i < numBones; i++) {
            AIBone aiBone = AIBone.create(aiBones.get(i));
            int id = boneList.size();
            Bone bone = new Bone(id, aiBone.mName().dataString(), toMatrix(aiBone.mOffsetMatrix()));
            boneList.add(bone);
            int numWeights = aiBone.mNumWeights();
            AIVertexWeight.Buffer aiWeights = aiBone.mWeights();
            for (int j = 0; j < numWeights; j++) {
                AIVertexWeight aiWeight = aiWeights.get(j);
                VertexWeight vw = new VertexWeight(bone.boneId(), aiWeight.mVertexId(),
                        aiWeight.mWeight());
                List<VertexWeight> vertexWeightList = weightSet.get(vw.vertexId());
                if (vertexWeightList == null) {
                    vertexWeightList = new ArrayList<>();
                    weightSet.put(vw.vertexId(), vertexWeightList);
                }
                vertexWeightList.add(vw);
            }
        }

        int numVertices = aiMesh.mNumVertices();
        for (int i = 0; i < numVertices; i++) {
            List<VertexWeight> vertexWeightList = weightSet.get(i);
            int size = vertexWeightList != null ? vertexWeightList.size() : 0;
            for (int j = 0; j < Mesh.MAX_WEIGHTS; j++) {
                if (j < size) {
                    VertexWeight vw = vertexWeightList.get(j);
                    weights.add(vw.weight());
                    boneIds.add(vw.boneId());
                } else {
                    weights.add(0.0f);
                    boneIds.add(0);
                }
            }
        }

        return new AnimMeshData(Utils.listFloatToArray(weights), Utils.listIntToArray(boneIds));
    }
    ...
}

このメソッドは、特定のメッシュのボーン定義をトラバースし、それらのウェイトを取得して生成し、3 つのリストを埋めます。

・boneList: オフセット マトリックスを含むボーンのリストが含まれます。後で最終的な骨の変換を計算するために使用します。Boneその情報を保持するために、という名前の新しいクラスが作成されました。このリストには、すべてのメッシュのボーンが含まれます。
・boneIds: の各頂点のボーンの識別子のみが含まれますMesh。ボーンは、レンダリング時の位置によって識別されます。このリストには、特定のメッシュのボーンのみが含まれています。
・weightsMesh:関連するボーンに適用されるの各頂点の重みが含まれています。
このメソッドで取得された情報は、AnimMeshDataレコードにカプセル化されます (クラス内で定義されますModelLoader)。newBoneとVertexWeightclass もレコードです。それらは次のように定義されます。

public class ModelLoader {
    ...
    public record AnimMeshData(float[] weights, int[] boneIds) {
    }

    private record Bone(int boneId, String boneName, Matrix4f offsetMatrix) {
    }

    private record VertexWeight(int boneId, int vertexId, float weight) {
    }
}

s とsを配列Utilsに変換するために、クラスに 2 つの新しいメソッドも作成しました。Listfloatint

public class Utils {
    ...
    public static float[] listFloatToArray(List<Float> list) {
        int size = list != null ? list.size() : 0;
        float[] floatArr = new float[size];
        for (int i = 0; i < size; i++) {
            floatArr[i] = list.get(i);
        }
        return floatArr;
    }

    public static int[] listIntToArray(List<Integer> list) {
        return list.stream().mapToInt((Integer v) -> v).toArray();
    }
    ...
}

メソッドに戻るとloadModel、メッシュとマテリアルを処理したら、アニメーション データ (各アニメーションとその変換に関連付けられたさまざまなアニメーション キー フレーム) を処理します。そのすべての情報もModelクラスに保存されます。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {
        ...
        List<Model.Animation> animations = new ArrayList<>();
        int numAnimations = aiScene.mNumAnimations();
        if (numAnimations > 0) {
            Node rootNode = buildNodesTree(aiScene.mRootNode(), null);
            Matrix4f globalInverseTransformation = toMatrix(aiScene.mRootNode().mTransformation()).invert();
            animations = processAnimations(aiScene, boneList, rootNode, globalInverseTransformation);
        }

        aiReleaseImport(aiScene);

        return new Model(modelId, materialList, animations);
    }
    ...
}

メソッドは非常に単純です。buildNodesTreeノードのツリーを構築するルート ノードから開始して、ノード階層をトラバースするだけです。

public class ModelLoader {
    ...
    private static Node buildNodesTree(AINode aiNode, Node parentNode) {
        String nodeName = aiNode.mName().dataString();
        Node node = new Node(nodeName, parentNode, toMatrix(aiNode.mTransformation()));

        int numChildren = aiNode.mNumChildren();
        PointerBuffer aiChildren = aiNode.mChildren();
        for (int i = 0; i < numChildren; i++) {
            AINode aiChildNode = AINode.create(aiChildren.get(i));
            Node childNode = buildNodesTree(aiChildNode, node);
            node.addChild(childNode);
        }
        return node;
    }
    ...
}

このtoMatrixメソッドは assimp 行列を JOML 行列に変換するだけです:

public class ModelLoader {
    ...
    private static Matrix4f toMatrix(AIMatrix4x4 aiMatrix4x4) {
        Matrix4f result = new Matrix4f();
        result.m00(aiMatrix4x4.a1());
        result.m10(aiMatrix4x4.a2());
        result.m20(aiMatrix4x4.a3());
        result.m30(aiMatrix4x4.a4());
        result.m01(aiMatrix4x4.b1());
        result.m11(aiMatrix4x4.b2());
        result.m21(aiMatrix4x4.b3());
        result.m31(aiMatrix4x4.b4());
        result.m02(aiMatrix4x4.c1());
        result.m12(aiMatrix4x4.c2());
        result.m22(aiMatrix4x4.c3());
        result.m32(aiMatrix4x4.c4());
        result.m03(aiMatrix4x4.d1());
        result.m13(aiMatrix4x4.d2());
        result.m23(aiMatrix4x4.d3());
        result.m33(aiMatrix4x4.d4());

        return result;
    }
    ...
}

メソッドは次のprocessAnimationsように定義されます。

public class ModelLoader {
    ...
    private static List<Model.Animation> processAnimations(AIScene aiScene, List<Bone> boneList,
                                                           Node rootNode, Matrix4f globalInverseTransformation) {
        List<Model.Animation> animations = new ArrayList<>();

        // Process all animations
        int numAnimations = aiScene.mNumAnimations();
        PointerBuffer aiAnimations = aiScene.mAnimations();
        for (int i = 0; i < numAnimations; i++) {
            AIAnimation aiAnimation = AIAnimation.create(aiAnimations.get(i));
            int maxFrames = calcAnimationMaxFrames(aiAnimation);

            List<Model.AnimatedFrame> frames = new ArrayList<>();
            Model.Animation animation = new Model.Animation(aiAnimation.mName().dataString(), aiAnimation.mDuration(), frames);
            animations.add(animation);

            for (int j = 0; j < maxFrames; j++) {
                Matrix4f[] boneMatrices = new Matrix4f[MAX_BONES];
                Arrays.fill(boneMatrices, IDENTITY_MATRIX);
                Model.AnimatedFrame animatedFrame = new Model.AnimatedFrame(boneMatrices);
                buildFrameMatrices(aiAnimation, boneList, animatedFrame, j, rootNode,
                        rootNode.getNodeTransformation(), globalInverseTransformation);
                frames.add(animatedFrame);
            }
        }
        return animations;
    }
    ...
}

このメソッドはインスタンスの を返しListますModel.Animation。モデルには複数のアニメーションを含めることができるため、アニメーションはインデックスごとに保存されることに注意してください。これらのアニメーションごとに、アニメーション フレーム (Model.AnimatedFrameインスタンス) のリストを作成します。これは、基本的に、モデルを構成する各ボーンに適用される変換マトリックスのリストです。アニメーションごとに、次のcalcAnimationMaxFramesように定義されているメソッドを呼び出して、最大フレーム数を計算します。

public class ModelLoader {
    ...
    private static int calcAnimationMaxFrames(AIAnimation aiAnimation) {
        int maxFrames = 0;
        int numNodeAnims = aiAnimation.mNumChannels();
        PointerBuffer aiChannels = aiAnimation.mChannels();
        for (int i = 0; i < numNodeAnims; i++) {
            AINodeAnim aiNodeAnim = AINodeAnim.create(aiChannels.get(i));
            int numFrames = Math.max(Math.max(aiNodeAnim.mNumPositionKeys(), aiNodeAnim.mNumScalingKeys()),
                    aiNodeAnim.mNumRotationKeys());
            maxFrames = Math.max(maxFrames, numFrames);
        }

        return maxFrames;
    }
    ...
}

クラスの変更を確認する前に、アニメーション情報を保持するためModelLoaderのクラスの変更を確認しましょう。Model

public class Model {
    ...
    private List<Animation> animationList;
    ...
    public Model(String id, List<Material> materialList, List<Animation> animationList) {
        entitiesList = new ArrayList<>();
        this.id = id;
        this.materialList = materialList;
        this.animationList = animationList;
    }
    ...
    public List<Animation> getAnimationList() {
        return animationList;
    }
    ...
    public record AnimatedFrame(Matrix4f[] boneMatrices) {
    }

    public record Animation(String name, double duration, List<AnimatedFrame> frames) {
    }
}

ご覧のとおり、モデルに関連付けられたアニメーションのリストを保存します。それぞれのアニメーションは、名前、持続時間、およびアニメーション フレームのリストによって定義されます。基本的には、各ボーンに適用されるボーン変換マトリックスを保存するだけです。

ModelLoaderクラスに戻ると、各AINodeAnimインスタンスは、特定のフレームのモデル内のノードに適用されるいくつかの変換を定義します。特定のノードに対するこれらの変換は、AINodeAnim実例。これらの変換は、位置の移動、回転、およびスケーリング値の形式で定義されます。ここでの秘訣は、たとえば、特定のノードの移動値は特定のフレームで停止できますが、回転とスケーリングの値は次のフレームで継続できるということです。この場合、回転やスケーリングよりも移動値が少なくなります。したがって、フレームの最大数を計算するには、最大値を使用することをお勧めします。これはノードごとに定義されるため、問題はさらに複雑になります。ノードは、最初のフレームにいくつかの変換を定義するだけで、残りのフレームにそれ以上の変更を適用することはできません。この場合、常に最後に定義された値を使用する必要があります。したがって、ノードに関連付けられたすべてのアニメーションの最大数を取得します。

メソッドに戻ると、processAnimationsその情報を使用して、さまざまなフレームを反復処理し、メソッドを呼び出してボーンの変換マトリックスを構築する準備が整いましたbuildFrameMatrices。フレームごとに、ルート ノードから開始し、ノード階層の上から下に再帰的に変換を適用します。は次のbuildFrameMatricesように定義されます。

public class ModelLoader {
    ...
    private static void buildFrameMatrices(AIAnimation aiAnimation, List<Bone> boneList, Model.AnimatedFrame animatedFrame,
                                           int frame, Node node, Matrix4f parentTransformation, Matrix4f globalInverseTransform) {
        String nodeName = node.getName();
        AINodeAnim aiNodeAnim = findAIAnimNode(aiAnimation, nodeName);
        Matrix4f nodeTransform = node.getNodeTransformation();
        if (aiNodeAnim != null) {
            nodeTransform = buildNodeTransformationMatrix(aiNodeAnim, frame);
        }
        Matrix4f nodeGlobalTransform = new Matrix4f(parentTransformation).mul(nodeTransform);

        List<Bone> affectedBones = boneList.stream().filter(b -> b.boneName().equals(nodeName)).toList();
        for (Bone bone : affectedBones) {
            Matrix4f boneTransform = new Matrix4f(globalInverseTransform).mul(nodeGlobalTransform).
                    mul(bone.offsetMatrix());
            animatedFrame.boneMatrices()[bone.boneId()] = boneTransform;
        }

        for (Node childNode : node.getChildren()) {
            buildFrameMatrices(aiAnimation, boneList, animatedFrame, frame, childNode, nodeGlobalTransform,
                    globalInverseTransform);
        }
    }
    ...
}

ノードに関連付けられた変換を取得します。次に、このノードにアニメーション ノードが関連付けられているかどうかを確認します。その場合、処理しているフレームに適用される適切な移動、回転、およびスケーリング変換を取得する必要があります。その情報を使用して、そのノードに関連付けられたボーンを取得し、その特定のフレームの各ボーンの変換行列を次のように乗算して更新します。

・モデルの逆グローバル変換行列 (ルート ノード変換行列の逆行列)。
・ノードの変換マトリックス。
・ボーン オフセット マトリックス。
その後、ノード変換マトリックスをそれらの子ノードの親マトリックスとして使用して、子ノードを反復処理します。

public class ModelLoader {
    ...
    private static Matrix4f buildNodeTransformationMatrix(AINodeAnim aiNodeAnim, int frame) {
        AIVectorKey.Buffer positionKeys = aiNodeAnim.mPositionKeys();
        AIVectorKey.Buffer scalingKeys = aiNodeAnim.mScalingKeys();
        AIQuatKey.Buffer rotationKeys = aiNodeAnim.mRotationKeys();

        AIVectorKey aiVecKey;
        AIVector3D vec;

        Matrix4f nodeTransform = new Matrix4f();
        int numPositions = aiNodeAnim.mNumPositionKeys();
        if (numPositions > 0) {
            aiVecKey = positionKeys.get(Math.min(numPositions - 1, frame));
            vec = aiVecKey.mValue();
            nodeTransform.translate(vec.x(), vec.y(), vec.z());
        }
        int numRotations = aiNodeAnim.mNumRotationKeys();
        if (numRotations > 0) {
            AIQuatKey quatKey = rotationKeys.get(Math.min(numRotations - 1, frame));
            AIQuaternion aiQuat = quatKey.mValue();
            Quaternionf quat = new Quaternionf(aiQuat.x(), aiQuat.y(), aiQuat.z(), aiQuat.w());
            nodeTransform.rotate(quat);
        }
        int numScalingKeys = aiNodeAnim.mNumScalingKeys();
        if (numScalingKeys > 0) {
            aiVecKey = scalingKeys.get(Math.min(numScalingKeys - 1, frame));
            vec = aiVecKey.mValue();
            nodeTransform.scale(vec.x(), vec.y(), vec.z());
        }

        return nodeTransform;
    }
    ...
}

インスタンスは、AINodeAnim移動、回転、およびスケーリング情報を含む一連のキーを定義します。これらのキーは、特定の瞬間を参照します。情報は時間順に並べられていると仮定し、各フレームに適用される変換を含む行列のリストを作成します。前に述べたように、これらの変換の一部は特定のフレームで「停止」する可能性があるため、最後のフレームには最後の値を使用する必要があります。

メソッドは次のfindAIAnimNodeように定義されます。

public class ModelLoader {
    ...
    private static AINodeAnim findAIAnimNode(AIAnimation aiAnimation, String nodeName) {
        AINodeAnim result = null;
        int numAnimNodes = aiAnimation.mNumChannels();
        PointerBuffer aiChannels = aiAnimation.mChannels();
        for (int i = 0; i < numAnimNodes; i++) {
            AINodeAnim aiNodeAnim = AINodeAnim.create(aiChannels.get(i));
            if (nodeName.equals(aiNodeAnim.mNodeName().dataString())) {
                result = aiNodeAnim;
                break;
            }
        }
        return result;
    }
    ...
}

Meshボーン インデックスとボーン ウェイトに新しい VBO を割り当てるには、クラスを更新する必要があります。最大 4 つのウェイト (および頂点ごとの関連付けられたボーン インデックス) を使用することがわかります。

public class Mesh {

    public static final int MAX_WEIGHTS = 4;
    ...
    public Mesh(float[] positions, float[] normals, float[] tangents, float[] bitangents, float[] textCoords, int[] indices) {
        this(positions, normals, tangents, bitangents, textCoords, indices,
                new int[Mesh.MAX_WEIGHTS * positions.length / 3], new float[Mesh.MAX_WEIGHTS * positions.length / 3]);
    }

    public Mesh(float[] positions, float[] normals, float[] tangents, float[] bitangents, float[] textCoords, int[] indices,
                int[] boneIndices, float[] weights) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            ...
            // Bone weights
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer weightsBuffer = MemoryUtil.memAllocFloat(weights.length);
            weightsBuffer.put(weights).flip();
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, weightsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(5);
            glVertexAttribPointer(5, 4, GL_FLOAT, false, 0, 0);

            // Bone indices
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            IntBuffer boneIndicesBuffer = MemoryUtil.memAllocInt(boneIndices.length);
            boneIndicesBuffer.put(boneIndices).flip();
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, boneIndicesBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(6);
            glVertexAttribPointer(6, 4, GL_FLOAT, false, 0, 0);
            ...
        }
    }
    ...
}

このNodeクラスは、に関連付けられたデータを格納するだけで、AINodeその子を管理するための特定のメソッドがあります。

package org.lwjglb.engine.scene;

import org.joml.Matrix4f;

import java.util.*;

public class Node {
    private final List<Node> children;

    private final String name;

    private final Node parent;

    private Matrix4f nodeTransformation;

    public Node(String name, Node parent, Matrix4f nodeTransformation) {
        this.name = name;
        this.parent = parent;
        this.nodeTransformation = nodeTransformation;
        this.children = new ArrayList<>();
    }

    public void addChild(Node node) {
        this.children.add(node);
    }

    public List<Node> getChildren() {
        return children;
    }

    public String getName() {
        return name;
    }

    public Matrix4f getNodeTransformation() {
        return nodeTransformation;
    }

    public Node getParent() {
        return parent;
    }
}

これで、アニメーション モデルをレンダリングする方法と、静的モデルと共存させる方法を確認できます。SceneRenderクラスから始めましょう。このクラスでは、(現在のアニメーション フレームに割り当てられた) ボーン マトリックスを渡すために新しいユニフォームをセットアップして、シェーダーで使用できるようにするだけです。それに加えて、静的およびアニメーション化されたエンティティのレンダリングは、このクラスに追加の影響を与えません。

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("bonesMatrices");
        ...
    }

    public void render(Scene scene) {
        ...
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                ...
                for (Mesh mesh : material.getMeshList()) {
                    glBindVertexArray(mesh.getVaoId());
                    for (Entity entity : entities) {
                        uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
                        AnimationData animationData = entity.getAnimationData();
                        if (animationData == null) {
                            uniformsMap.setUniform("bonesMatrices", AnimationData.DEFAULT_BONES_MATRICES);
                        } else {
                            uniformsMap.setUniform("bonesMatrices", animationData.getCurrentFrame().boneMatrices());
                        }
                        glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
                    }
                }
            }
        }
    }
    ...
}

静的モデルの場合、null に設定された行列の配列を渡します。UniformsMap行列の配列の値を設定する新しいメソッドを追加するには、も変更する必要があります。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, Matrix4f[] matrices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            int length = matrices != null ? matrices.length : 0;
            FloatBuffer fb = stack.mallocFloat(16 * length);
            for (int i = 0; i < length; i++) {
                matrices[i].get(16 * i, fb);
            }
            glUniformMatrix4fv(uniforms.get(uniformName), false, fb);
        }
    }
}

AnimationDataに設定された現在のアニメーションを制御する名前の新しいクラスも作成しましたEntity。

package org.lwjglb.engine.scene;

import org.joml.Matrix4f;
import org.lwjglb.engine.graph.Model;

public class AnimationData {

    public static final Matrix4f[] DEFAULT_BONES_MATRICES = new Matrix4f[ModelLoader.MAX_BONES];

    static {
        Matrix4f zeroMatrix = new Matrix4f().zero();
        for (int i = 0; i < DEFAULT_BONES_MATRICES.length; i++) {
            DEFAULT_BONES_MATRICES[i] = zeroMatrix;
        }
    }

    private Model.Animation currentAnimation;
    private int currentFrameIdx;

    public AnimationData(Model.Animation currentAnimation) {
        currentFrameIdx = 0;
        this.currentAnimation = currentAnimation;
    }

    public Model.Animation getCurrentAnimation() {
        return currentAnimation;
    }

    public Model.AnimatedFrame getCurrentFrame() {
        return currentAnimation.frames().get(currentFrameIdx);
    }

    public int getCurrentFrameIdx() {
        return currentFrameIdx;
    }

    public void nextFrame() {
        int nextFrame = currentFrameIdx + 1;
        if (nextFrame > currentAnimation.frames().size() - 1) {
            currentFrameIdx = 0;
        } else {
            currentFrameIdx = nextFrame;
        }
    }

    public void setCurrentAnimation(Model.Animation currentAnimation) {
        currentFrameIdx = 0;
        this.currentAnimation = currentAnimation;
    }
}

もちろん、インスタンスEntityへの参照を保持するようにクラスを変更する必要があります。AnimationData

public class Entity {
    ...
    private AnimationData animationData;
    ...
    public AnimationData getAnimationData() {
        return animationData;
    }
    ...
    public void setAnimationData(AnimationData animationData) {
        this.animationData = animationData;
    }
    ...
}

シーンの頂点シェーダー ( scene.vert) を変更して、アニメーション データを再生する必要があります。いくつかの定数と、ボーンの重みとインデックスの新しい入力属性を定義することから始めます (頂点ごとに 4 つの要素を使用するため、 と を使用vec4しますivec4)。現在のアニメーションに関連付けられているボーン マトリックスもユニフォームとして渡します。

#version 330

const int MAX_WEIGHTS = 4;
const int MAX_BONES = 150;

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;
layout (location=5) in vec4 boneWeights;
layout (location=6) in ivec4 boneIndices;
...
uniform mat4 bonesMatrices[MAX_BONES];
...

このmain関数では、関連付けられたボーン インデックスによって指定され、関連付けられた重みによって変調された行列を使用して、ボーン ウェイトを繰り返し処理し、位置と法線を変更します。各ボーンが位置 (および法線) の修正に寄与するが、重みを使用して変調される場合のように考えることができます。静的モデルを使用している場合、重みはゼロになるため、元の位置と法線の値に固執します。

...
void main()
{
    vec4 initPos = vec4(0, 0, 0, 0);
    vec4 initNormal = vec4(0, 0, 0, 0);
    vec4 initTangent = vec4(0, 0, 0, 0);
    vec4 initBitangent = vec4(0, 0, 0, 0);

    int count = 0;
    for (int i = 0; i < MAX_WEIGHTS; i++) {
        float weight = boneWeights[i];
        if (weight > 0) {
            count++;
            int boneIndex = boneIndices[i];
            vec4 tmpPos = bonesMatrices[boneIndex] * vec4(position, 1.0);
            initPos += weight * tmpPos;

            vec4 tmpNormal = bonesMatrices[boneIndex] * vec4(normal, 0.0);
            initNormal += weight * tmpNormal;

            vec4 tmpTangent = bonesMatrices[boneIndex] * vec4(tangent, 0.0);
            initTangent += weight * tmpTangent;

            vec4 tmpBitangent = bonesMatrices[boneIndex] * vec4(bitangent, 0.0);
            initTangent += weight * tmpBitangent;
        }
    }
    if (count == 0) {
        initPos = vec4(position, 1.0);
        initNormal = vec4(normal, 0.0);
        initTangent = vec4(tangent, 0.0);
        initBitangent = vec4(bitangent, 0.0);
    }

    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    vec4 mvPosition =  modelViewMatrix * initPos;
    gl_Position   = projectionMatrix * mvPosition;
    outPosition   = mvPosition.xyz;
    outNormal     = normalize(modelViewMatrix * initNormal).xyz;
    outTangent    = normalize(modelViewMatrix * initTangent).xyz;
    outBitangent  = normalize(modelViewMatrix * initBitangent).xyz;
    outTextCoord  = texCoord;
}

次の図は、プロセスを示しています

このMainクラスでは、アニメーション モデルをロードし、アンチエイリアシングを有効にする必要があります。また、更新ごとにアニメーション フレームをインクリメントします。

public class Main implements IAppLogic {
    ...
    private AnimationData animationData;
    ...
    public static void main(String[] args) {
        Main main = new Main();
        Window.WindowOptions opts = new Window.WindowOptions();
        opts.antiAliasing = true;
        Engine gameEng = new Engine("chapter-15", opts, main);
        gameEng.start();
    }
    ...
    @Override
    public void init(Window window, Scene scene, Render render) {
        String terrainModelId = "terrain";
        Model terrainModel = ModelLoader.loadModel(terrainModelId, "resources/models/terrain/terrain.obj",
                scene.getTextureCache(), false);
        scene.addModel(terrainModel);
        Entity terrainEntity = new Entity("terrainEntity", terrainModelId);
        terrainEntity.setScale(100.0f);
        terrainEntity.updateModelMatrix();
        scene.addEntity(terrainEntity);

        String bobModelId = "bobModel";
        Model bobModel = ModelLoader.loadModel(bobModelId, "resources/models/bob/boblamp.md5mesh",
                scene.getTextureCache(), true);
        scene.addModel(bobModel);
        Entity bobEntity = new Entity("bobEntity", bobModelId);
        bobEntity.setScale(0.05f);
        bobEntity.updateModelMatrix();
        animationData = new AnimationData(bobModel.getAnimationList().get(0));
        bobEntity.setAnimationData(animationData);
        scene.addEntity(bobEntity);

        SceneLights sceneLights = new SceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        ambientLight.setIntensity(0.5f);
        ambientLight.setColor(0.3f, 0.3f, 0.3f);

        DirLight dirLight = sceneLights.getDirLight();
        dirLight.setPosition(0, 1, 0);
        dirLight.setIntensity(1.0f);
        scene.setSceneLights(sceneLights);

        SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache());
        skyBox.getSkyBoxEntity().setScale(100);
        skyBox.getSkyBoxEntity().updateModelMatrix();
        scene.setSkyBox(skyBox);

        scene.setFog(new Fog(true, new Vector3f(0.5f, 0.5f, 0.5f), 0.02f));

        Camera camera = scene.getCamera();
        camera.setPosition(-1.5f, 3.0f, 4.5f);
        camera.addRotation((float) Math.toRadians(15.0f), (float) Math.toRadians(390.f));

        lightAngle = 0;
    }
    ...
    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        animationData.nextFrame();
    }
}

最後に、クラスのメソッドが変更されているSkyBoxため、クラスも変更する必要があります。loadModelModelLoader

public class SkyBox {
    ...
    public SkyBox(String skyBoxModelPath, TextureCache textureCache) {
        skyBoxModel = ModelLoader.loadModel("skybox-model", skyBoxModelPath, textureCache, false);
        ...
    }
}

次のようなものが表示されます。

Java 3D LWJGL Gitbook~第14章 – 法線マッピング~

第14章 - 法線マッピング

この章では、3D モデルの外観を劇的に改善するテクニックについて説明します。ここまでで、複雑な 3D モデルにテクスチャを適用できるようになりましたが、実際のオブジェクトがどのように見えるかにはまだほど遠い状態です。現実世界の表面は完全に平らではなく、現在の 3D モデルにはない不完全さがあります。

よりリアルなシーンをレンダリングするために、法線マップを使用します。現実世界の平らな表面を見ると、光が反射する方法によって、遠くからでもこれらの欠陥が見えることがわかります。3D シーンでは、平らな表面には欠陥がなく、テクスチャを適用できますが、光が反射する方法は変更しません。それが違いを生むものです。

三角形の数を増やしてモデルの詳細を増やし、それらの不完全さを反映することを考えるかもしれませんが、パフォーマンスは低下します。必要なのは、表面での光の反射方法を変更してリアリズムを高める方法です。これは法線マッピング技術で達成されます。

サンプルコードの実行

コンセプト

プレーン サーフェスの例に戻りましょう。平面は、四角形を形成する 2 つの三角形によって定義できます。ライティングの章で覚えていると思いますが、光がどのように反射するかをモデル化する要素はサーフェス法線です。この場合、サーフェス全体に対して単一の法線があり、サーフェスの各フラグメントは、光がそれらにどのように影響するかを計算するときに同じ法線を使用します。これを次の図に示します。

サーフェスの各フラグメントの法線を変更できれば、サーフェスの不完全性をモデル化して、より現実的な方法でレンダリングできます。これを次の図に示します。

これを実現する方法は、サーフェスの法線を保存する別のテクスチャをロードすることです。通常のテクスチャの各ピクセルには、
X, Y, Z。 RGB 値として格納された法線の座標。次のテクスチャを使用してクワッドを描画してみましょう。

上の画像の法線マップ テクスチャの例を次に示します。

ご覧のとおり、元のテクスチャに色変換を適用したかのようです。各ピクセルは、色成分を使用して法線情報を格納します。通常、法線マップを表示するときに目にすることの 1 つは、支配的な色が青色になる傾向があることです。これは、法線が正の方向を指しているという事実によるものです。

Z軸。のZコンポーネントは通常、よりもはるかに高い値を持ちますXとY法線が表面の外を指しているため、平らな表面用のもの。
以来 X, Y, Z座標が RGB にマッピングされると、青のコンポーネントもより高い値になります。

したがって、法線マップを使用してオブジェクトをレンダリングするには、追加のテクスチャが必要であり、フラグメントをレンダリングするときにそれを使用して適切な法線値を取得します。

実装

通常、法線マップはそのように定義されず、いわゆるタンジェント スペースで定義されます。接線空間は、モデルの各三角形にローカルな座標系です。その座標空間では、X軸は常にサーフェスの外を指します。これが、向かい合った面を持つ複雑なモデルであっても、法線マップが通常青みがかっている理由です。接空間を処理するには、ノルム、アル、タンジェント、バイタンジェント ベクトルが必要です。すでに法線ベクトルがあり、接線ベクトルと従接線ベクトルは法線ベクトルに垂直なベクトルです。これらのベクトルTBNは、シェーダーで使用している座標系の接空間にあるデータを使用できるようにする行列を計算するために必要です。

ここで、この側面に関する優れたチュートリアルを確認できます

したがって、最初のステップは、ModelLoaderタンジェントおよびバイタンジェント情報を含む、クラスをロードする法線マッピングのサポートを追加することです。assimp のモデル読み込みフラグを設定するときに、これを含めたことを思い出してください: aiProcess_CalcTangentSpace. このフラグを使用すると、タンジェント データとバイタンジェント データを自動的に計算できます。

このprocessMaterialメソッドでは、まず法線マップ テクスチャの存在を照会します。その場合は、そのテクスチャを読み込み、そのテクスチャ パスをマテリアルに関連付けます。

public class ModelLoader {
    ...
    private static Material processMaterial(AIMaterial aiMaterial, String modelDir, TextureCache textureCache) {
        ...
        try (MemoryStack stack = MemoryStack.stackPush()) {
            ...
            AIString aiNormalMapPath = AIString.calloc(stack);
            Assimp.aiGetMaterialTexture(aiMaterial, aiTextureType_NORMALS, 0, aiNormalMapPath, (IntBuffer) null,
                    null, null, null, null, null);
            String normalMapPath = aiNormalMapPath.dataString();
            if (normalMapPath != null && normalMapPath.length() > 0) {
                material.setNormalMapPath(modelDir + File.separator + new File(normalMapPath).getName());
                textureCache.createTexture(material.getNormalMapPath());
            }
            return material;
        }
    }
    ...
}

このprocessMeshメソッドでは、タンジェントとバイタンジェントのデータもロードする必要があります。

public class ModelLoader {
    ...
    private static Mesh processMesh(AIMesh aiMesh) {
        ...
        float[] tangents = processTangents(aiMesh, normals);
        float[] bitangents = processBitangents(aiMesh, normals);
        ...
        return new Mesh(vertices, normals, tangents, bitangents, textCoords, indices);
    }
    ...
}

processTangentsおよびメソッドは、processBitangents法線をロードするものと非常によく似ています。

public class ModelLoader {
    ...
    private static float[] processBitangents(AIMesh aiMesh, float[] normals) {

        AIVector3D.Buffer buffer = aiMesh.mBitangents();
        float[] data = new float[buffer.remaining() * 3];
        int pos = 0;
        while (buffer.remaining() > 0) {
            AIVector3D aiBitangent = buffer.get();
            data[pos++] = aiBitangent.x();
            data[pos++] = aiBitangent.y();
            data[pos++] = aiBitangent.z();
        }

        // Assimp may not calculate tangents with models that do not have texture coordinates. Just create empty values
        if (data.length == 0) {
            data = new float[normals.length];
        }
        return data;
    }
    ...
    private static float[] processTangents(AIMesh aiMesh, float[] normals) {

        AIVector3D.Buffer buffer = aiMesh.mTangents();
        float[] data = new float[buffer.remaining() * 3];
        int pos = 0;
        while (buffer.remaining() > 0) {
            AIVector3D aiTangent = buffer.get();
            data[pos++] = aiTangent.x();
            data[pos++] = aiTangent.y();
            data[pos++] = aiTangent.z();
        }

        // Assimp may not calculate tangents with models that do not have texture coordinates. Just create empty values
        if (data.length == 0) {
            data = new float[normals.length];
        }
        return data;
    }
    ...
}

ご覧のとおり、新しいデータを保持するためにクラスも変更する必要がありMeshますMaterial。Meshクラスから始めましょう:

public class Mesh {
    ...
    public Mesh(float[] positions, float[] normals, float[] tangents, float[] bitangents, float[] textCoords, int[] indices) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            ...
            // Tangents VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer tangentsBuffer = stack.callocFloat(tangents.length);
            tangentsBuffer.put(0, tangents);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, tangentsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(2);
            glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, 0);

            // Bitangents VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer bitangentsBuffer = stack.callocFloat(bitangents.length);
            bitangentsBuffer.put(0, bitangents);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, bitangentsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(3);
            glVertexAttribPointer(3, 3, GL_FLOAT, false, 0, 0);

            // Texture coordinates VBO
            ...
            glEnableVertexAttribArray(4);
            glVertexAttribPointer(4, 2, GL_FLOAT, false, 0, 0);
            ...
        }
    }
    ...
}

タンジェント データとバイタンジェント データ (法線データと同様の構造に従う) 用に 2 つの新しい VBO を作成し、テクスチャ座標 VBO の位置を更新する必要があります。

クラスにはMaterial、法線マッピング テクスチャ パスへのパスを含める必要があります。

public class Material {
    ...
    private String normalMapPath;
    ...
    public String getNormalMapPath() {
        return normalMapPath;
    }
    ...
    public void setNormalMapPath(String normalMapPath) {
        this.normalMapPath = normalMapPath;
    }
    ...
}

次に、シーンの頂点シェーダー ( scene.vert)から始めて、シェーダーを変更する必要があります。

#version 330

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 outPosition;
out vec3 outNormal;
out vec3 outTangent;
out vec3 outBitangent;
out vec2 outTextCoord;

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

void main()
{
    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    vec4 mvPosition =  modelViewMatrix * vec4(position, 1.0);
    gl_Position   = projectionMatrix * mvPosition;
    outPosition   = mvPosition.xyz;
    outNormal     = normalize(modelViewMatrix * vec4(normal, 0.0)).xyz;
    outTangent    = normalize(modelViewMatrix * vec4(tangent, 0)).xyz;
    outBitangent  = normalize(modelViewMatrix * vec4(bitangent, 0)).xyz;
    outTextCoord  = texCoord;
}

ご覧のとおり、bitangent と tangent に関連付けられた新しい入力データを定義する必要があります。法線を処理したのと同じ方法でこれらの要素を変換し、そのデータを入力としてフラグメント シェーダーに渡します ( scene.frag)。

#version 330
...
in vec3 outTangent;
in vec3 outBitangent;
...
struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float reflectance;
    int hasNormalMap;
};
...
uniform sampler2D normalSampler;
...

頂点シェーダーからの新しい入力を定義することから始めます。Materialこれには、使用可能な法線マップがあるかどうかを通知する構造体の追加要素が含まれます ( hasNormalMap)。また、法線マップ テクスチャの新しいユニフォームを追加します ( normalSampler))。次のステップは、法線マップ テクスチャに基づいて法線を更新する関数を定義することです。

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

void main() {
    vec4 text_color = texture(txtSampler, outTextCoord);
    vec4 ambient = calcAmbient(ambientLight, text_color + material.ambient);
    vec4 diffuse = text_color + material.diffuse;
    vec4 specular = text_color + material.specular;

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

    vec4 diffuseSpecularComp = calcDirLight(diffuse, specular, dirLight, outPosition, normal);

    for (int i=0; i<MAX_POINT_LIGHTS; i++) {
        if (pointLights[i].intensity > 0) {
            diffuseSpecularComp += calcPointLight(diffuse, specular, pointLights[i], outPosition, normal);
        }
    }

    for (int i=0; i<MAX_SPOT_LIGHTS; i++) {
        if (spotLights[i].pl.intensity > 0) {
            diffuseSpecularComp += calcSpotLight(diffuse, specular, spotLights[i], outPosition, normal);
        }
    }
    fragColor = ambient + diffuseSpecularComp;

    if (fog.activeFog == 1) {
        fragColor = calcFog(outPosition, fragColor, fog, ambientLight.color, dirLight);
    }
}

このcalcNormal関数は次のパラメータを取ります。

  • 頂点法線。
  • 頂点接線。
  • 頂点バイタンジェント。
  • テクスチャ座標。
    その関数で最初に行うことは、TBN 行列を計算することです。その後、法線マップ テクスチャから法線値を取得し、TBN マトリックスを使用して接線空間からビュー空間に渡します。取得する色は通常の座標ですが、RGB 値として保存されるため、範囲 [0, 1] に含まれることを思い出してください。[-1, 1] の範囲になるように変換する必要があるため、2 を掛けて 1 を引くだけです。
    最後に、マテリアルが法線マップ テクスチャを定義する場合にのみ、その関数を使用します。

SceneRenderシェーダーで使用する新しい法線を作成して使用するには、クラスも変更する必要があります。

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("normalSampler");
        ...
        uniformsMap.createUniform("material.hasNormalMap");
        ...
    }
    public void render(Scene scene) {
        ...
        uniformsMap.setUniform("normalSampler", 1);
        ...
        for (Model model : models) {
            ...
            for (Material material : model.getMaterialList()) {
                ...
                String normalMapPath = material.getNormalMapPath();
                boolean hasNormalMapPath = normalMapPath != null;
                uniformsMap.setUniform("material.hasNormalMap", hasNormalMapPath ? 1 : 0);
                ...
                if (hasNormalMapPath) {
                    Texture normalMapTexture = textureCache.getTexture(normalMapPath);
                    glActiveTexture(GL_TEXTURE1);
                    normalMapTexture.bind();
                }
                ...
            }
        }
        ...
    }
    ...    
}

Main最後のステップは、この効果を示すためにクラスを更新することです。法線マップが関連付けられている場合と関連付けられていない場合の 2 つのクワッドをロードします。また、左矢印と右矢印を使用して光の角度を制御し、効果を示します。

public class Main implements IAppLogic {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-14", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        String wallNoNormalsModelId = "quad-no-normals-model";
        Model quadModelNoNormals = ModelLoader.loadModel(wallNoNormalsModelId, "resources/models/wall/wall_nonormals.obj",
                scene.getTextureCache());
        scene.addModel(quadModelNoNormals);

        Entity wallLeftEntity = new Entity("wallLeftEntity", wallNoNormalsModelId);
        wallLeftEntity.setPosition(-3f, 0, 0);
        wallLeftEntity.setScale(2.0f);
        wallLeftEntity.updateModelMatrix();
        scene.addEntity(wallLeftEntity);

        String wallModelId = "quad-model";
        Model quadModel = ModelLoader.loadModel(wallModelId, "resources/models/wall/wall.obj",
                scene.getTextureCache());
        scene.addModel(quadModel);

        Entity wallRightEntity = new Entity("wallRightEntity", wallModelId);
        wallRightEntity.setPosition(3f, 0, 0);
        wallRightEntity.setScale(2.0f);
        wallRightEntity.updateModelMatrix();
        scene.addEntity(wallRightEntity);

        SceneLights sceneLights = new SceneLights();
        sceneLights.getAmbientLight().setIntensity(0.2f);
        DirLight dirLight = sceneLights.getDirLight();
        dirLight.setPosition(1, 1, 0);
        dirLight.setIntensity(1.0f);
        scene.setSceneLights(sceneLights);

        Camera camera = scene.getCamera();
        camera.moveUp(5.0f);
        camera.addRotation((float) Math.toRadians(90), 0);

        lightAngle = -35;
    }
        ...
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        float move = diffTimeMillis * MOVEMENT_SPEED;
        Camera camera = scene.getCamera();
        if (window.isKeyPressed(GLFW_KEY_W)) {
            camera.moveForward(move);
        } else if (window.isKeyPressed(GLFW_KEY_S)) {
            camera.moveBackwards(move);
        }
        if (window.isKeyPressed(GLFW_KEY_A)) {
            camera.moveLeft(move);
        } else if (window.isKeyPressed(GLFW_KEY_D)) {
            camera.moveRight(move);
        }
        if (window.isKeyPressed(GLFW_KEY_LEFT)) {
            lightAngle -= 2.5f;
            if (lightAngle < -90) {
                lightAngle = -90;
            }
        } else if (window.isKeyPressed(GLFW_KEY_RIGHT)) {
            lightAngle += 2.5f;
            if (lightAngle > 90) {
                lightAngle = 90;
            }
        }

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

        SceneLights sceneLights = scene.getSceneLights();
        DirLight dirLight = sceneLights.getDirLight();
        double angRad = Math.toRadians(lightAngle);
        dirLight.getDirection().x = (float) Math.sin(angRad);
        dirLight.getDirection().y = (float) Math.cos(angRad);
    }
    ...
}

結果を次の図に示します。
ご覧のとおり、通常のテクスチャが適用されたクワッドは、よりボリュームのある印象を与えます。本質的には、他のクワッドと同じように平らな面ですが、光がどのように反射するかを見ることができます。

Java 3D LWJGL Gitbook ~第13章 霧~

第13章 霧

この章では、ゲーム エンジンでフォグ エフェクトを作成する方法を確認します。その効果を使用して、遠くのオブジェクトが薄暗くなり、濃霧に消えていくように見える様子をシミュレートします。

コンセプト

まず、フォグを定義する属性を調べてみましょう。1つ目は霧の色です。現実の世界では、霧は灰色ですが、この効果を使用して、さまざまな色の霧が侵入する広い領域をシミュレートできます。アトリビュートはフォグの密度です。

したがって、フォグ エフェクトを適用するには、3D シーン オブジェクトがカメラから遠く離れている限り、フォグ カラーにフェードする方法を見つける必要があります。カメラに近いオブジェクトは霧の影響を受けませんが、遠くにあるオブジェクトは区別できません。そのため、その効果をシミュレートするために、フォグ カラーと各フラグメント カラーをブレンドするために使用できる係数を計算できる必要があります。その要因は、カメラまでの距離に依存する必要があります。

その要因を呼びましょう
フォググファクター
、その範囲を 0 から 1 に設定します。
フォググファクター
が 1 の場合、オブジェクトがフォグの影響を受けない、つまり近くのオブジェクトであることを意味します。とき
フォググファクター
値が 0 の場合、オブジェクトが完全にフォグに隠れることを意味します。

したがって、フォグ カラーの計算に必要な式は次のとおりです。

f
i
n
a
l
C
o
l
o
r
=
(
1

f
o
g
F
a
c
t
o
r
)

f
o
g
C
o
l
o
r
+
f
o
g
F
a
c
t
o
r

f
r
a
m
e
n
t
C
o
l
o
r

霧効果を適用した結果の色です。
フォグ カラーとフラグメント カラーのブレンド方法を制御するパラメータです。基本的にオブジェクトの可視性を制御します。
霧の色です。
フォグ効果を適用していないフラグメントの色です。
次に、計算方法を見つける必要があります
距離にもよる。さまざまなモデルを選択できますが、最初のモデルは線形モデルを使用することです。これは、距離が与えられると、fogFactor 値を直線的に変化させるモデルです。

線形モデルは、次のパラメーターによって定義できます。

: フォグ エフェクトが適用され始める距離。fogStart
: フォグ エフェクトが最大値に達する距離。fogFinish
: カメラまでの距離。distance


f
o
g
F
a
c
t
o
r
=


(
f
o
g
F
i
n
i
s
h

d
i
s
t
a
n
c
e
)


(
f
o
g
F
i
n
i
s
h

f
o
g
S
t
a
r
t
)



以下の距離にあるオブジェクトの場合
単に設定するだけです

. 次のグラフは、
距離で変わります。

線形モデルは計算が簡単ですが、あまり現実的ではなく、霧の密度が考慮されていません。実際には、霧はより滑らかに成長する傾向があります。したがって、次の適切なモデルは指数モデルです。そのモデルの式は次のとおりです。
作用する新しい変数は次のとおりです。

フォグの厚さまたは密度をモデル化します。
これは、霧が距離とともに増加する速度を制御するために使用されます。
次の図は、指数のさまざまな値に対する上記の式の 2 つのグラフを示しています (青い線は $$2$$、

このコードでは、指数の値を 2 に設定する数式を使用します (別の値を使用するように例を簡単に変更できます)。

実装

理論が説明されたので、それを実践することができます。scene.frag必要なすべての変数がそこにあるので、シーン フラグメント シェーダー ( ) にエフェクトを実装します。フォグ属性をモデル化する構造体を定義することから始めます。

...
struct Fog
{
    int activeFog;
    vec3 color;
    float density;
};
...

このactiveアトリビュートは、フォグ エフェクトを有効または無効にするために使用されます。フォグは、 という名前の別のユニフォームを介してシェーダーに渡されfogます。

...
uniform Fog fog;
...

calcFogこのように定義されているという名前の関数を作成します。

...
vec4 calcFog(vec3 pos, vec4 color, Fog fog, vec3 ambientLight, DirLight dirLight) {
    vec3 fogColor = fog.color * (ambientLight + dirLight.color * dirLight.intensity);
    float distance = length(pos);
    float fogFactor = 1.0 / exp((distance * fog.density) * (distance * fog.density));
    fogFactor = clamp(fogFactor, 0.0, 1.0);

    vec3 resultColor = mix(fogColor, color.xyz, fogFactor);
    return vec4(resultColor.xyz, color.w);
}
...

ご覧のとおり、最初に頂点までの距離を計算します。頂点座標はpos変数で定義されており、長さを計算するだけです。次に、指数が 2 の指数モデルを使用してフォグ ファクターを計算します (これは、2 倍するのと同じです)。fogFactor間の範囲にクランプします
0と1
機能を使用しmixます。GLSL では、mix関数はフォグ カラーとフラグメント カラー (変数で定義color) をブレンドするために使用されます。これは、次の方程式を適用することと同じです。

r
e
s
u
l
t
C
o
l
o
r
=
(
1

f
o
g
F
a
c
t
o
r
)

f
o
g
.
c
o
l
o
r
+
f
o
g
F
a
c
t
o
r

c
o
l
o
r

また、元の色の透明度である w コンポーネントも保持します。フラグメントはその透過性レベルを維持する必要があるため、このコンポーネントが影響を受けることは望ましくありません。

フラグメント シェーダーの最後で、すべてのライト エフェクトを適用した後、フォグがアクティブな場合は、返された値をフラグメント カラーに割り当てるだけです。

...
    if (fog.activeFog == 1) {
        fragColor = calcFog(outPosition, fragColor, fog, ambientLight.color, dirLight);
    }
...

Fogフォグ属性を含む別の POJO (Plain Old Java Object)という名前の新しいクラスも作成します。

package org.lwjglb.engine.scene;

import org.joml.Vector3f;

public class Fog {

    private boolean active;
    private Vector3f color;
    private float density;

    public Fog() {
        active = false;
        color = new Vector3f();
    }

    public Fog(boolean active, Vector3f color, float density) {
        this.color = color;
        this.density = density;
        this.active = active;
    }

    public Vector3f getColor() {
        return color;
    }

    public float getDensity() {
        return density;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setDensity(float density) {
        this.density = density;
    }
}

Fogクラスにインスタンスを追加しますScene。

public class Scene {
    ...
    private Fog fog;
    ...
    public Scene(int width, int height) {
        ...
        fog = new Fog();
    }
    ...
    public Fog getFog() {
        return fog;
    }
    ...
    public void setFog(Fog fog) {
        this.fog = fog;
    }
    ...
}

ここで、これらすべての要素をクラスに設定する必要があります。最初に、構造SceneRenderに均一な値を設定します。Fog

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("fog.activeFog");
        uniformsMap.createUniform("fog.color");
        uniformsMap.createUniform("fog.density");
    }
    ...
}

このrenderメソッドでは、最初にブレンドを有効にしてからFogユニフォームを設定する必要があります。

public class SceneRender {
    ...
     public void render(Scene scene) {
        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        shaderProgram.bind();
        ...
        Fog fog = scene.getFog();
        uniformsMap.setUniform("fog.activeFog", fog.isActive() ? 1 : 0);
        uniformsMap.setUniform("fog.color", fog.getColor());
        uniformsMap.setUniform("fog.density", fog.getDensity());
        ...
        shaderProgram.unbind();
        glDisable(GL_BLEND);
    }
    ...
}

最後に、Main霧を設定するようにクラスを変更し、霧の効果を示すためにスケーリングされた地形として単一のクワッドを使用します。

public class Main implements IAppLogic {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-13", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        String terrainModelId = "terrain";
        Model terrainModel = ModelLoader.loadModel(terrainModelId, "resources/models/terrain/terrain.obj",
                scene.getTextureCache());
        scene.addModel(terrainModel);
        Entity terrainEntity = new Entity("terrainEntity", terrainModelId);
        terrainEntity.setScale(100.0f);
        terrainEntity.updateModelMatrix();
        scene.addEntity(terrainEntity);

        SceneLights sceneLights = new SceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        ambientLight.setIntensity(0.5f);
        ambientLight.setColor(0.3f, 0.3f, 0.3f);

        DirLight dirLight = sceneLights.getDirLight();
        dirLight.setPosition(0, 1, 0);
        dirLight.setIntensity(1.0f);
        scene.setSceneLights(sceneLights);

        SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache());
        skyBox.getSkyBoxEntity().setScale(50);
        scene.setSkyBox(skyBox);

        scene.setFog(new Fog(true, new Vector3f(0.5f, 0.5f, 0.5f), 0.95f));

        scene.getCamera().moveUp(0.1f);
    }
    ...
    public void update(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done here
    }
}

強調すべき重要な点の 1 つは、霧の色を賢く選択する必要があるということです。スカイボックスがなく固定色の背景がある場合、これはさらに重要です。霧の色をクリアの色と同じになるように設定する必要があります。スカイボックスをレンダリングするコードのコメントを外してサンプルを再実行すると、このような結果が得られます。

ようなものが表示されるはずです。

Java 3D LWJGL Gitbook~ 第12章 スカイボックス ~

第12章 スカイボックス

この章では、スカイ ボックスの作成方法について説明します。スカイボックスを使用すると、背景を設定して、3D 世界がより広いという錯覚を与えることができます。その背景はカメラの位置を包み込み、空間全体を覆います。ここで使用するテクニックは、3D シーンの周りに表示される大きな立方体を作成することです。つまり、カメラ位置の中心が立方体の中心になります。その立方体の側面は、画像が連続した風景のように見える方法でマッピングされる丘、青い空、雲のテクスチャでラップされます。

参照するドキュメントとプログラムソースへのリンクは以下になります。

サンプルコードの実行結果

スカイボックス

次の図は、スカイボックスの概念を示しています。

スカイ ボックスを作成するプロセスは、次の手順に要約できます。

・大きなキューブを作成します。
エッジのない巨大な風景を見ているような錯覚を与えるテクスチャを適用します。
・立方体をレンダリングして、側面が遠くにあり、原点がカメラの中心にくるようにします。
SkyBoxスカイ ボックス キューブ (テクスチャ付き) とテクスチャ キャッシュへの参照を含む 3D モデルへのパスを受け取るコンストラクタで名前を付けた新しいクラスを作成することから始めます。このクラスはそのモデルをロードし、そのモデルにEntity関連付けられたインスタンスを作成します。SkyBoxクラスの定義は以下の通りです。

package org.lwjglb.engine.scene;

import org.lwjglb.engine.graph.*;

public class SkyBox {

    private Entity skyBoxEntity;
    private Model skyBoxModel;

    public SkyBox(String skyBoxModelPath, TextureCache textureCache) {
        skyBoxModel = ModelLoader.loadModel("skybox-model", skyBoxModelPath, textureCache);
        skyBoxEntity = new Entity("skyBoxEntity-entity", skyBoxModel.getId());
    }

    public Entity getSkyBoxEntity() {
        return skyBoxEntity;
    }

    public Model getSkyBoxModel() {
        return skyBoxModel;
    }
}

SkyBoxクラスへの参照をクラスに保存しますScene。

public class Scene {
    ...
    private SkyBox skyBox;
    ...
    public SkyBox getSkyBox() {
        return skyBox;
    }
    ...
    public void setSkyBox(SkyBox skyBox) {
        this.skyBox = skyBox;
    }
    ...
}

次のステップは、スカイ ボックス用の頂点シェーダーとフラグメント シェーダーの別のセットを作成することです。しかし、既にあるシーン シェーダーを再利用してみませんか? 答えは、実際には、必要なシェーダーはそれらのシェーダーの簡略化されたバージョンであるということです。たとえば、スカイ ボックスにはライトを適用しません。以下に、スカイ ボックスの頂点シェーダーを示します ( skybox.vert)。

#version 330

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

out vec2 outTextCoord;

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

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

まだモデル マトリックスを使用していることがわかります。スカイボックスをスケーリングするため、モデル マトリックスが必要です。開始時にスカイ ボックスをモデル化する立方体のサイズを大きくし、モデルとビュー マトリックスを乗算する必要がない他の実装がいくつか見られる場合があります。このアプローチを選択したのは、より柔軟で、実行時にスカイボックスのサイズを変更できるためですが、必要に応じて他のアプローチに簡単に切り替えることができます。

フラグメント シェーダー ( skybox.frag) も非常に単純で、テクスチャまたは拡散色から色を取得するだけです。

#version 330

in vec2 outTextCoord;
out vec4 fragColor;

uniform vec4 diffuse;
uniform sampler2D txtSampler;
uniform int hasTexture;

void main()
{
    if (hasTexture == 1) {
        fragColor = texture(txtSampler, outTextCoord);
    } else {
        fragColor = diffuse;
    }
}

SkyBoxRenderこれらのシェーダーを使用してレンダリングを実行する名前の新しいクラスを作成します。クラスは、シェーダー プログラムを作成し、必要なユニフォームをセットアップすることから始まります。

package org.lwjglb.engine.graph;

import org.joml.Matrix4f;
import org.lwjglb.engine.scene.*;

import java.util.*;

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

public class SkyBoxRender {

    private ShaderProgram shaderProgram;

    private UniformsMap uniformsMap;

    private Matrix4f viewMatrix;

    public SkyBoxRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/skybox.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/skybox.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        viewMatrix = new Matrix4f();
        createUniforms();
    }
    ...
}

次のステップでは、グローバルなレンダリング メソッドで呼び出されるスカイボックスの新しいレンダリング メソッドを作成します。

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);

        Model skyBoxModel = skyBox.getSkyBoxModel();
        Entity skyBoxEntity = skyBox.getSkyBoxEntity();
        TextureCache textureCache = scene.getTextureCache();
        for (Material material : skyBoxModel.getMaterialList()) {
            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);

            for (Mesh mesh : material.getMeshList()) {
                glBindVertexArray(mesh.getVaoId());

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

        glBindVertexArray(0);

        shaderProgram.unbind();
    }
}

関連するユニフォームにそのデータをロードする前に、ビュー マトリックスを変更していることがわかります。カメラを動かすとき、実際に行っていることは世界全体を動かしていることを忘れないでください。したがって、ビュー マトリックスをそのまま乗算すると、カメラが移動するとスカイボックスが移動します。しかし、これは必要ありません。(0, 0, 0) の原点座標に貼り付けたいのです。これは、平行移動の増分を含むビュー マトリックスの部分 ( m30、m31およびm32コンポーネント)。スカイ ボックスは原点に固定する必要があるため、ビュー マトリックスの使用をまったく避けることができると考えるかもしれません。その場合、スカイボックスがカメラと一緒に回転しないことがわかりますが、これは私たちが望んでいるものではありません。回転する必要がありますが、平行移動は必要ありません。スカイボックスをレンダリングするには、ユニフォームをセットアップし、スカイ ボックスに関連付けられた立方体をレンダリングします。

クラスでは、Renderクラスをインスタンス化しSkyBoxRender、render メソッドを呼び出すだけです。

public class Render {
    ...
    private SkyBoxRender skyBoxRender;
    ...

    public Render(Window window) {
        ...
        skyBoxRender = new SkyBoxRender();
    }

    public void render(Window window, Scene scene) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, window.getWidth(), window.getHeight());

        skyBoxRender.render(scene);
        sceneRender.render(scene);
        guiRender.render(scene);
    }
    ...
}

スカイ ボックスを最初にレンダリングしていることがわかります。これは、シーンに透明度のある 3D モデルがある場合、それらを (黒い背景ではなく) スカイボックスとブレンドしたいという事実によるものです。

最後に、このMainクラスでは、シーンにスカイ ボックスを設定し、タイルのセットを作成して、無限の地形の錯覚を与えます。カメラの位置に合わせて移動するタイルのチャンクが常に表示されるように設定します。

public class Main implements IAppLogic {
    ...
    private static final int NUM_CHUNKS = 4;

    private Entity[][] terrainEntities;

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

    @Override
    public void init(Window window, Scene scene, Render render) {
        String quadModelId = "quad-model";
        Model quadModel = ModelLoader.loadModel("quad-model", "resources/models/quad/quad.obj",
                scene.getTextureCache());
        scene.addModel(quadModel);

        int numRows = NUM_CHUNKS * 2 + 1;
        int numCols = numRows;
        terrainEntities = new Entity[numRows][numCols];
        for (int j = 0; j < numRows; j++) {
            for (int i = 0; i < numCols; i++) {
                Entity entity = new Entity("TERRAIN_" + j + "_" + i, quadModelId);
                terrainEntities[j][i] = entity;
                scene.addEntity(entity);
            }
        }

        SceneLights sceneLights = new SceneLights();
        sceneLights.getAmbientLight().setIntensity(0.2f);
        scene.setSceneLights(sceneLights);

        SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache());
        skyBox.getSkyBoxEntity().setScale(50);
        scene.setSkyBox(skyBox);

        scene.getCamera().moveUp(0.1f);

        updateTerrain(scene);
    }

    @Override
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        float move = diffTimeMillis * MOVEMENT_SPEED;
        Camera camera = scene.getCamera();
        if (window.isKeyPressed(GLFW_KEY_W)) {
            camera.moveForward(move);
        } else if (window.isKeyPressed(GLFW_KEY_S)) {
            camera.moveBackwards(move);
        }
        if (window.isKeyPressed(GLFW_KEY_A)) {
            camera.moveLeft(move);
        } else if (window.isKeyPressed(GLFW_KEY_D)) {
            camera.moveRight(move);
        }

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

    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        updateTerrain(scene);
    }
        public void updateTerrain(Scene scene) {
        int cellSize = 10;
        Camera camera = scene.getCamera();
        Vector3f cameraPos = camera.getPosition();
        int cellCol = (int) (cameraPos.x / cellSize);
        int cellRow = (int) (cameraPos.z / cellSize);

        int numRows = NUM_CHUNKS * 2 + 1;
        int numCols = numRows;
        int zOffset = -NUM_CHUNKS;
        float scale = cellSize / 2.0f;
        for (int j = 0; j < numRows; j++) {
            int xOffset = -NUM_CHUNKS;
            for (int i = 0; i < numCols; i++) {
                Entity entity = terrainEntities[j][i];
                entity.setScale(scale);
                entity.setPosition((cellCol + xOffset) * 2.0f, 0, (cellRow + zOffset) * 2.0f);
                entity.getModelMatrix().identity().scale(scale).translate(entity.getPosition());
                xOffset++;
            }
            zOffset++;
        }
    }
}

Java 3D LWJGL GitBook 〜Chapter11:ライト~

第11章 - ライト

この章では、3D ゲーム エンジンにライトを追加する方法を学習します。複雑さを除けば、膨大な量のコンピューター リソースが必要になるため、物理的に完全なライト モデルは実装しません。代わりに、適切な結果を提供する近似を実装します。フォン シェーディング (Bui Tuong Phong によって開発された) という名前のアルゴリズムを使用します。指摘すべきもう 1 つの重要な点は、ライトのみをモデル化し、それらのライトによって生成されるシャドウをモデル化しないことです (これは別の章で行います)。

参照するドキュメントとプログラムソースへのリンクは以下になります。

ライトの考え方(理論編)

はじめに、ライトの考え方に関しての記述があり、その後実装に関しての記述があります。

そして、「モデル読み込みの変更」の項目部分がとても重要に思えます。大まかに次のような変更を行っています。

  1. ライトクラスの作成=ライトのモデル(データクラス)を作成する
  2. Sceneクラスにライトクラスを格納する
  3. ModelLoader(3DModelの読み込みクラス)の変更
    ・法線データの読み込み
    ・アンビエント カラー、スペキュラ カラー、光沢係数を取得
  4. 上記の変更に伴い修正する必要があるクラスの修正

ImGuiに関して

このサンプルコードでは、ImGuiの実装部分はライトコントロールクラスに実装されています。
次のように、IGuiInstanceインターフェースを実装(implements)しているクラスがImGuiをコントロールするクラスになります。

class XXX implements IGuiInstance

そして、IAppLogicインターフェースの実装クラス=Mainクラスで入力があったときに何かしらの処理をする予定だと思いますが、このサンプルでは何も実装していませんでした。

サンプルプログラムの実行結果

いくつかの概念

始める前に、いくつかのライト タイプを定義しましょう。

  • ・ポイント ライト: このタイプのライトは、空間内の 1 点から全方向に均一に放出される光源をモデル化します。
  • ・スポット ライト: このタイプのライトは、空間内の 1 点から放射される光源をモデル化しますが、すべての方向に放射するのではなく、円錐に制限されます。
  • ・指向性ライト: このタイプのライトは、太陽から受け取る光をモデル化します。3D 空間内のすべてのオブジェクトは、特定の方向から来る平行* ・光線ライトに当てられます。オブジェクトが近くにあるか遠くにあるかに関係なく、すべてのレイ ライトは同じ角度でオブジェクトに影響を与えます。
  • ・環境光: このタイプの光は、空間のあらゆる場所から来て、すべてのオブジェクトを同じように照らします。

したがって、ライトをモデル化するには、ライトのタイプ、その位置、および色などのその他のパラメータを考慮する必要があります。もちろん、レイ ライトの影響を受けたオブジェクトが光を吸収および反射する方法も考慮する必要があります。

フォン シェーディング アルゴリズムは、モデルの各ポイント、つまりすべての頂点の光の効果をモデル化します。これがローカル イルミネーション シミュレーションと呼ばれる理由であり、このアルゴリズムが影を計算しない理由です。頂点が光をブロックするオブジェクトの背後にあるかどうかを考慮せずに、すべての頂点に適用される光を計算するだけです。 . 後の章でこの欠点を克服します。しかし、そのため、非常に優れた効果を提供するシンプルで高速なアルゴリズムです。ここでは、材料を深く考慮しない単純化したバージョンを使用します。

Phong アルゴリズムは、ライティングの 3 つのコンポーネントを考慮します。

3 つのコンポーネント

  • ・環境光: どこからでも来る光をモデル化します。これは、光が当たっていない領域を (必要な強度で) 照らすのに役立ちます。これは背景光のようなものです。
  • ・拡散反射率: 光源に面している表面がより明るいことを考慮します。
  • ・鏡面反射率: 研磨面または金属面で光がどのように反射するかをモデル化します。

最後に取得したいのは、フラグメントに割り当てられた色を掛けて、受ける光に応じてその色を明るくまたは暗く設定する係数です。コンポーネントに名前を付けましょう

つけた名前

  • A: アンビエント
  • D: 拡散反射光
  • S: スペキュラ

実際、これらのコンポーネントは色であり、各光コンポーネントが寄与する色コンポーネントです。これは、光コンポーネントがある程度の強度を提供するだけでなく、モデルの色を変更できるという事実によるものです。フラグメント シェーダーでは、その明るい色を元のフラグメント カラー (テクスチャまたはベース カラーから取得) で乗算するだけです。

アンビエント、ディフューズ、スペキュラー コンポーネントで使用される、同じマテリアルに異なる色を割り当てることもできます。したがって、これらのコンポーネントは、マテリアルに関連付けられた色によって調整されます。マテリアルにテクスチャがある場合は、コンポーネントごとに 1 つのテクスチャを使用します。

したがって、非テクスチャ マテリアルの最終的な色は次のようになります。

L
=
A

anbientColor
+
D

diffuseColor
+
S

specularColor

テクスチャ マテリアルの最終的な色は次のようになります。

L
=
A

textureColor
+
D

textureColor
+
S

textureColor

法線

法線は、ライトを操作するときの ket 要素です。まず定義しましょう。平面の法線は、長さが 1 に等しい平面に垂直なベクトルです。

上の図からわかるように、平面には 2 つの法線があります。どちらを使用する必要がありますか? 3D グラフィックスの法線は照明に使用されるため、光源に向けられた法線を選択する必要があります。言い換えれば、モデルの外面から突き出ている法線を選択する必要があります。

3D モデルがある場合、ポリゴン、この場合は三角形で構成されます。各三角形は 3 つの頂点で構成されます。三角形の法線ベクトルは、長さが 1 に等しい三角形の表面に垂直なベクトルになります。

頂点法線は特定の頂点に関連付けられており、周囲の三角形の法線の組み合わせです (もちろん、その長さは 1 です)。ここでは、3D メッシュの頂点モデルを確認できます (ウィキペディアから取得) 。

拡散反射率

拡散反射率について話しましょう。これは、光源に対して垂直に面している面が、より間接的な角度で光を受けている面よりも明るく見えるという事実をモデル化しています。これらのオブジェクトはより多くの光を受け取り、光の密度 (このように呼びましょう) が高くなります。

しかし、これをどのように計算するのでしょうか。ここで、まず法線の使用を開始します。前の図の 3 点の法線を描きましょう。ご覧のとおり、各ポイントの法線は、各ポイントの接平面に垂直なベクトルになります。光源から来る光線を描く代わりに、各点から光の点へのベクトルを描きます (つまり、反対方向)。

ご覧のとおり、に関連付けられている法線

、名前付き

P1, N1

等しい角度を持つ

P1, 0

光源を指すベクトルを使用します。その表面は光源に対して垂直であり、一番明るいポイントになります。

P1

関連付けられている法線

P2

、名前付き

N2

、光源を指すベクトルと約 30 度の角度を持っているため、より暗い黄褐色になるはずです。

P1, P3

. 最後に、関連付けられている法線

P3

、名前付き

N3

も光源を指すベクトルに平行ですが、2 つのベクトルは反対方向です。

P3

は、光源を指すベクトルと 180 度の角度を持ち、まったく光を取得しないはずです。
したがって、点に到達する光の強度を決定するための適切なアプローチがあるようです。これは、光源を指すベクトルで法線を形成する角度に関連しています。これをどのように計算できますか?

内積という算術演算

内積という算術演算を使用できます。この操作は 2 つのベクトルを取り、それらの間の角度が鋭角の場合は正の数値 (スカラー) を生成し、それらの間の角度が広い場合は負の数値を生成します。両方のベクトルが正規化されている場合、つまり両方の長さが 1 の場合、内積は次のようになります。

-1 と 1

両方のベクトルがまったく同じ方向 (角度) を向いている場合、内積は 1 になります。
); そうなる 0 両方のベクトルが正方形の角度を形成する場合、それは -1
両方のベクトルが反対方向を指している場合。2 つのベクトルを定義しましょう。

v1 と v2

、そしてみましょう

alpha

それらの間の角度になります。内積は次の式で定義されます。

両方のベクトルが正規化されている場合、それらの長さ、モジュールは 1 に等しいため、内積はそれらの間の角度の余弦に等しくなります。この操作を使用して、拡散反射率コンポーネントを計算します。

したがって、光源を指すベクトルを計算する必要があります。これをどのように行うのですか?各点の位置 (頂点の位置) と光源の位置があります。まず、両方の座標が同じ座標空間にある必要があります。簡単にするために、それらが両方ともワールド座標空間にあると仮定しましょう。これらの位置は、頂点位置 ($$VP$$) と光源 ($$VS$$) を指すベクトルの座標です。次の図に示します。

差し引くと

V

探しているベクトルを取得します

L

これで、光源を指すベクトルと法線の間の内積を計算できます。この積は、表面の明るさをモデル化するためにその関係を最初に提案した Johann Lambert にちなんで、Lambert 項と呼ばれます。

計算方法をまとめてみます。次の変数を定義します。

計算方法をまとめ

vPos: モデル ビュー空間座標での頂点の位置。

lPos: ビュー空間座標でのライトの位置。

intensity: 光の強度 (0 から 1)。

lColor: 光の色。

normal: 頂点法線。

まず、現在の位置から光源を指すベクトルを計算する必要があります。

toLightDirection
=
lPos
-
vPos

. その操作の結果は正規化する必要があります。

次に、拡散係数 (スカラー) を計算する必要があります。

defuseFuctor
=
normal
-
toLightDirection

2 つのベクトル間の内積として計算されます。-1と1両方のベクトルを正規化する必要があります。色は間にある必要があります0と1
したがって、値がより低い場合0 に設定します。
最後に、拡散係数と光の強度によって光の色を調整する必要があります。

color
=
diffuseColor
*
lColor
*
diffuseColor
*
intensity

鏡面成分

鏡面反射光コンポーネントを検討する前に、まず光がどのように反射されるかを調べる必要があります。光が表面に当たると、その一部が吸収され、他の部分が反射されます。物理の授業で思い出したように、反射とは、光が物体から跳ね返ることです。

もちろん、表面は完全に磨かれているわけではなく、近くで見ると多くの欠陥が見られます。それに加えて、多くのレイ ライト (実際にはフォトン) があり、そのサーフェスに影響を与え、さまざまな角度で反射します。したがって、私たちが見ているのは、表面から反射された光線のようなものです。つまり、光は表面に当たると拡散します。これが、前に説明した拡散コンポーネントです。

しかし、金属などの研磨された表面に光が当たると、光の拡散が低下し、その表面に当たるとほとんどが反対方向に反射されます。

これはスペキュラ コンポーネントがモデル化するものであり、マテリアルの特性に依存します。鏡面反射率に関しては、カメラが適切な位置にある場合、つまり反射光が放出される領域にある場合にのみ、反射光が見えることに注意することが重要です。

鏡面反射の背後にあるメカニズムが説明されたので、その成分を計算する準備が整いました。まず、光源から頂点を指すベクトルが必要です。ディフューズ コンポーネントを計算していたとき、正反対の、光源を指すベクトルを計算しました。toLightDirectionですので、次のように計算してみましょう。

fromLightDirection
=
-(toLightDirection)

次に、衝撃による反射光を計算する必要があります。fromLightDirection
法線を考慮してサーフェスに挿入します。reflectまさにそれを行うGLSL 関数があります。そう、

reflectedLight
=
reflect(toLightSource, normal)

カメラを指すベクトルも必要です。名前を付けましょうcameraDirection
となり、カメラ位置と頂点位置の差として計算されます。

cameraDirection
=
cameraPos - vPos

. カメラ位置ベクトルと頂点位置は同じ座標系にある必要があり、結果のベクトルを正規化する必要があります。次の図は、これまでに計算した主なコンポーネントをスケッチしたものです。

次に、私たちが見る光の強度を計算する必要があります。specularFactor
. このコンポーネントは、cameraDirection
そしてそのreflectedLight
ベクトルは平行で同じ方向を指し、反対方向を指している場合はより低い値を取ります。これを計算するために、内積が再び役に立ちます。そう

specularFactor
=
cameraDirection - reflectedLight

. この値が間にあることのみが必要です0と1
それよりも低い場合0
に設定されます。
カメラが反射光円錐を指している場合、この光はより強くなければならないことも考慮する必要があります。これは、specularFactorという名前のパラメーターにspecularPower

specularFactor
=
specularFactor

specularPower

最後に、マテリアルの反射率をモデル化する必要があります。これは、光が反射した場合の強度も変調します。これは、reflectance という名前の別のパラメーターで行われます。したがって、鏡面反射光コンポーネントの色は次のようになります。

specularColor
*
lColor
*
refrectance
*
specularFactor
*
intensity

減衰

これで、アンビエント ライトを使用してポイント ライトをモデル化するのに役立つ 3 つのコンポーネントを計算する方法がわかりました。しかし、オブジェクトが反射する光は光源からの距離に依存しないため、ライト モデルはまだ完全ではありません。つまり、光の減衰をシミュレートする必要があります。

減衰は、距離と光の関数です。光の強さは距離の二乗に反比例します。光はそのエネルギーを球の表面に沿って伝搬し、その半径は光が移動した距離と同じであり、球の表面はその半径の 2 乗に比例するため、この事実は簡単に視覚化できます。減衰係数は次の式で計算できます。

1.0
/
(atConstant
+
atLinar
*
dist
+
atExponent
*
dist

2

減衰をシミュレートするには、その減衰係数を最終的な色で乗算するだけです。

指向性ライト

指向性照明は、すべて同じ方向から来る平行光線によってすべてのオブジェクトに当たります。太陽のように遠くにあるが強度の高い光源をモデル化します。

ディレクショナル ライトのもう 1 つの特徴は、減衰の影響を受けないことです。太陽光についてもう一度考えてみてください。太陽光線が当たったすべてのオブジェクトは、同じ強度で照らされます。太陽からの距離が非常に大きいため、オブジェクトの位置は関係ありません。実際、ディレクショナル ライトは無限遠に配置された光源としてモデル化されており、減衰の影響を受けた場合、どのオブジェクトにも影響しません (その色の寄与は0)。
それに加えて、ディレクショナル ライトは、ディフューズ コンポーネントとスペキュラ コンポーネントによっても構成されます。ポイント ライトとの唯一の違いは、位置ではなく方向があり、減衰の影響を受けないことです。ディレクショナル ライトの方向アトリビュートに戻り、3D ワールド全体の太陽の動きをモデリングしていると想像してください。北が増加する z 軸に向かって配置されていると仮定すると、次の図は、夜明け、日中、および夕暮れ時の光源の方向を示しています。

スポットライト

ここで、ポイント ライトに非常に似ているスポット ライトを実装しますが、放射されるライトは 3D コーンに制限されます。焦点から出る光、またはすべての方向に放射しないその他の光源をモデル化します。スポット ライトはポイント ライトと同じアトリビュートを持ちますが、円錐角度と円錐方向という 2 つの新しいパラメータが追加されています。

スポット ライトの影響は、いくつかの例外を除いて、ポイント ライトと同じ方法で計算されます。頂点位置から光源を指すベクトルがライト コーン内に含まれないポイントは、ポイント ライトの影響を受けません。

光円錐の内側にあるかどうかをどのように計算しますか? 光源からのベクトルとコーン方向ベクトル (どちらも正規化されています) の間で内積を再度行う必要があります。

間の内積:LとCベクトルは次の通りです。



L






C



=
|


L



|

|


C



|

C
o
s
(
α
)

. スポット ライトの定義でカットオフ角度のコサインを格納すると、内積がその値よりも大きい場合、それがライト コーンの内側にあることがわかります (コサイン グラフを思い出してください。
角度は

α

、余弦は0、角度が小さいほど余弦が大きくなります)。1
2 番目の違いは、円錐ベクトルから遠く離れたポイントは、より少ない光を受け取ります。つまり、減衰が高くなります。これを計算するにはいくつかの方法があります。減衰に次の係数を掛けることにより、単純なアプローチを選択します。

1

(
1

C
o
s
(
α
)
)

/

(
1

C
o
s
(
c
u
t
O
f
f
A
n
g
l
e
)

(フラグメント シェーダーでは角度ではなく、カットオフ角度のコサインを使用します。上記の式が 0 から 1 までの値を生成することを確認できます。角度がカットオフ角度と等しい場合は 0、角度が等しい場合は 1 です。 0)。

スポット ライトのサンプル

ライト クラスの実装

モデルというのはここでは、GetterとSetterを持っているデータクラスのことを指していると思ってよさそうです。

データクラスに関して

フィールド変数とGetter, Setterを持ったクラスは具体的に次のようなものです。
下のクラスは、「身長(tall)」というデータを持った、データクラスです。

public class Body {
    /** 整数で身長を表す */
    private int tall;
    /** Getter */
    public int getTall() { return tall;}
    /** Setter */
    public void setTall(int tall) {this.tall = tall}
}

PointLightクラス

まず、さまざまなタイプのライトをモデル化する一連のクラスを作成することから始めましょう。ポイント ライトをモデル化するクラスから始めます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class PointLight {

    private Attenuation attenuation;
    private Vector3f color;
    private float intensity;
    private Vector3f position;

    public PointLight(Vector3f color, Vector3f position, float intensity) {
        attenuation = new Attenuation(0, 0, 1);
        this.color = color;
        this.position = position;
        this.intensity = intensity;
    }

    public Attenuation getAttenuation() {
        return attenuation;
    }

    public Vector3f getColor() {
        return color;
    }

    public float getIntensity() {
        return intensity;
    }

    public Vector3f getPosition() {
        return position;
    }

    public void setAttenuation(Attenuation attenuation) {
        this.attenuation = attenuation;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }

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

    public static class Attenuation {

        private float constant;
        private float exponent;
        private float linear;

        public Attenuation(float constant, float linear, float exponent) {
            this.constant = constant;
            this.linear = linear;
            this.exponent = exponent;
        }

        public float getConstant() {
            return constant;
        }

        public float getExponent() {
            return exponent;
        }

        public float getLinear() {
            return linear;
        }

        public void setConstant(float constant) {
            this.constant = constant;
        }

        public void setExponent(float exponent) {
            this.exponent = exponent;
        }

        public void setLinear(float linear) {
            this.linear = linear;
        }
    }
}

ご覧のとおり、ポイント ライトは、色、強度、位置、および減衰モデルによって定義されます。
環境光は、色と強度だけで定義されます。

AmbientLightクラス

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class AmbientLight {

    private Vector3f color;

    private float intensity;

    public AmbientLight(float intensity, Vector3f color) {
        this.intensity = intensity;
        this.color = color;
    }

    public AmbientLight() {
        this(1.0f, new Vector3f(1.0f, 1.0f, 1.0f));
    }

    public Vector3f getColor() {
        return color;
    }

    public float getIntensity() {
        return intensity;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }
}

DirLightクラス

指向性ライトは次のように定義されます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class DirLight {

    private Vector3f color;

    private Vector3f direction;

    private float intensity;

    public DirLight(Vector3f color, Vector3f direction, float intensity) {
        this.color = color;
        this.direction = direction;
        this.intensity = intensity;
    }

    public Vector3f getColor() {
        return color;
    }

    public Vector3f getDirection() {
        return direction;
    }

    public float getIntensity() {
        return intensity;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setDirection(Vector3f direction) {
        this.direction = direction;
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }

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

SpotLightクラス

最後に、スポット ライトには、ポイント ライト リファレンスとライト コーン パラメータが含まれます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class SpotLight {

    private Vector3f coneDirection;
    private float cutOff;
    private float cutOffAngle;
    private PointLight pointLight;

    public SpotLight(PointLight pointLight, Vector3f coneDirection, float cutOffAngle) {
        this.pointLight = pointLight;
        this.coneDirection = coneDirection;
        this.cutOffAngle = cutOffAngle;
        setCutOffAngle(cutOffAngle);
    }

    public Vector3f getConeDirection() {
        return coneDirection;
    }

    public float getCutOff() {
        return cutOff;
    }

    public float getCutOffAngle() {
        return cutOffAngle;
    }

    public PointLight getPointLight() {
        return pointLight;
    }

    public void setConeDirection(float x, float y, float z) {
        coneDirection.set(x, y, z);
    }

    public void setConeDirection(Vector3f coneDirection) {
        this.coneDirection = coneDirection;
    }

    public final void setCutOffAngle(float cutOffAngle) {
        this.cutOffAngle = cutOffAngle;
        cutOff = (float) Math.cos(Math.toRadians(cutOffAngle));
    }

    public void setPointLight(PointLight pointLight) {
        this.pointLight = pointLight;
    }
}

SceneLightsクラス

すべてのライトは Scene クラスに格納されます。そのために、すべてのタイプのライトへの参照を格納する という名前の新しいクラスを作成しSceneLightsます (1 つのアンビエント ライト インスタンスと 1 つのディレクショナル ライトのみが必要であることに注意してください)。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

import java.util.*;

public class SceneLights {

    private AmbientLight ambientLight;
    private DirLight dirLight;
    private List<PointLight> pointLights;
    private List<SpotLight> spotLights;

    public SceneLights() {
        ambientLight = new AmbientLight();
        pointLights = new ArrayList<>();
        spotLights = new ArrayList<>();
        dirLight = new DirLight(new Vector3f(1, 1, 1), new Vector3f(0, 1, 0), 1.0f);
    }

    public AmbientLight getAmbientLight() {
        return ambientLight;
    }

    public DirLight getDirLight() {
        return dirLight;
    }

    public List<PointLight> getPointLights() {
        return pointLights;
    }

    public List<SpotLight> getSpotLights() {
        return spotLights;
    }

    public void setSpotLights(List<SpotLight> spotLights) {
        this.spotLights = spotLights;
    }
}

Sceneクラス

クラスSceneLightsには次の参照があります。Scene

public class Scene {
    ...
    private SceneLights sceneLights;
    ...
    public SceneLights getSceneLights() {
        return sceneLights;
    }
    ...
    public void setSceneLights(SceneLights sceneLights) {
        this.sceneLights = sceneLights;
    }
}

モデル読み込みの変更

ModelLoaderクラスを次のように変更する必要があります。

・マテリアルのより多くのプロパティ、特にアンビエント カラー、スペキュラ カラー、光沢係数を取得します。
・各メッシュの法線データを読み込みます。
マテリアルのより多くのプロパティを取得するには、processMaterialメソッドを変更する必要があります。

ModelLoaderクラス

public class ModelLoader {
    ...
    private static Material processMaterial(AIMaterial aiMaterial, String modelDir, TextureCache textureCache) {
        Material material = new Material();
        try (MemoryStack stack = MemoryStack.stackPush()) {
            AIColor4D color = AIColor4D.create();

            int result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_AMBIENT, aiTextureType_NONE, 0,
                    color);
            if (result == aiReturn_SUCCESS) {
                material.setAmbientColor(new Vector4f(color.r(), color.g(), color.b(), color.a()));
            }

            result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_DIFFUSE, aiTextureType_NONE, 0,
                    color);
            if (result == aiReturn_SUCCESS) {
                material.setDiffuseColor(new Vector4f(color.r(), color.g(), color.b(), color.a()));
            }

            result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_SPECULAR, aiTextureType_NONE, 0,
                    color);
            if (result == aiReturn_SUCCESS) {
                material.setSpecularColor(new Vector4f(color.r(), color.g(), color.b(), color.a()));
            }
            float reflectance = 0.0f;
            float[] shininessFactor = new float[]{0.0f};
            int[] pMax = new int[]{1};
            result = aiGetMaterialFloatArray(aiMaterial, AI_MATKEY_SHININESS_STRENGTH, aiTextureType_NONE, 0, shininessFactor, pMax);
            if (result != aiReturn_SUCCESS) {
                reflectance = shininessFactor[0];
            }
            material.setReflectance(reflectance);

            AIString aiTexturePath = AIString.calloc(stack);
            aiGetMaterialTexture(aiMaterial, aiTextureType_DIFFUSE, 0, aiTexturePath, (IntBuffer) null,
                    null, null, null, null, null);
            String texturePath = aiTexturePath.dataString();
            if (texturePath != null && texturePath.length() > 0) {
                material.setTexturePath(modelDir + File.separator + new File(texturePath).getName());
                textureCache.createTexture(material.getTexturePath());
                material.setDiffuseColor(Material.DEFAULT_COLOR);
            }

            return material;
        }
    }
    ...

ご覧のとおり、プロパティを取得することでマテリアルのアンビエント カラーを取得しAI_MATKEY_COLOR_AMBIENTます。プロパティを使用してスペキュラー カラーを取得しAI_MATKEY_COLOR_SPECULARます。光沢はAI_MATKEY_SHININESS_STRENGTHフラグを使用して照会されます。

法線をロードするには、という名前の新しいメソッドを作成し、メソッドprocessNormalsで呼び出す必要がありますprocessMesh。

public class ModelLoader {
    ...
    private static Mesh processMesh(AIMesh aiMesh) {
        float[] vertices = processVertices(aiMesh);
        float[] normals = processNormals(aiMesh);
        float[] textCoords = processTextCoords(aiMesh);
        int[] indices = processIndices(aiMesh);

        // Texture coordinates may not have been populated. We need at least the empty slots
        if (textCoords.length == 0) {
            int numElements = (vertices.length / 3) * 2;
            textCoords = new float[numElements];
        }

        return new Mesh(vertices, normals, textCoords, indices);
    }

    private static float[] processNormals(AIMesh aiMesh) {
        AIVector3D.Buffer buffer = aiMesh.mNormals();
        float[] data = new float[buffer.remaining() * 3];
        int pos = 0;
        while (buffer.remaining() > 0) {
            AIVector3D normal = buffer.get();
            data[pos++] = normal.x();
            data[pos++] = normal.y();
            data[pos++] = normal.z();
        }
        return data;
    }
    ...
}

Materialクラス

ご覧のとおり、新しい情報を格納するためにMaterialおよびMeshクラスも変更する必要があります。クラスの変更点は次のMaterialとおりです。

public class Material {
    ...
    private Vector4f ambientColor;
    ...
    private float reflectance;
    private Vector4f specularColor;
    ...
    public Material() {
        ...
        ambientColor = DEFAULT_COLOR;
        ...
    } 
    ...
    public Vector4f getAmbientColor() {
        return ambientColor;
    }
    ...
    public float getReflectance() {
        return reflectance;
    }

    public Vector4f getSpecularColor() {
        return specularColor;
    }
    ...
    public void setAmbientColor(Vector4f ambientColor) {
        this.ambientColor = ambientColor;
    }
    ...
    public void setReflectance(float reflectance) {
        this.reflectance = reflectance;
    }

    public void setSpecularColor(Vector4f specularColor) {
        this.specularColor = specularColor;
    }
    ...
}

Meshクラス

Meshクラスは法線データの新しい float 配列を受け入れるようになり、そのために新しい VBO を作成します。

public class Mesh {
    ...
    public Mesh(float[] positions, float[] normals, float[] textCoords, int[] indices) {
        ...
            // Normals VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer normalsBuffer = stack.callocFloat(normals.length);
            normalsBuffer.put(0, normals);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, normalsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

            // Texture coordinates VBO
            ...
            glEnableVertexAttribArray(2);
            glVertexAttribPointer(2, 2, GL_FLOAT, false, 0, 0);

            // Index VBO
            ...
        ...
    }
    ...
}

ライトでレンダリングする

レンダリング中にライトを使用する時が来ました。シェーダー、特に頂点シェーダー ( scene.vert)から始めましょう。

#version 330

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

out vec3 outPosition;
out vec3 outNormal;
out vec2 outTextCoord;

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

void main()
{
    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    vec4 mvPosition =  modelViewMatrix * vec4(position, 1.0);
    gl_Position = projectionMatrix * mvPosition;
    outPosition = mvPosition.xyz;
    outNormal = normalize(modelViewMatrix * vec4(normal, 0.0)).xyz;
    outTextCoord = texCoord;
}

ご覧のとおり、別の入力属性として通常のデータがあり、そのデータをフラグメント シェーダーに渡すだけです。フラグメント シェーダーの説明を続ける前に、強調しなければならない非常に重要な概念があります。mvVertexNormal上記のコードから、変数には頂点法線が含まれ、モデル ビュー空間座標に変換されることがわかります。これは、 に頂点位置を掛けることnormalによって行われます。modelViewMatrixただし、微妙な違いがあります。その頂点法線の w コンポーネントは、行列を乗算する前に 0 に設定されます。vec4(vertexNormal, 0.0). なぜこれを行うのですか?法線を回転およびスケーリングしたいが、平行移動したくないため、関心があるのはその方向だけであり、その位置には関心がありません。これは w コンポーネントを 0 に設定することで実現され、同次座標を使用する利点の 1 つです。w コンポーネントを設定することで、適用される変換を制御できます。行列の乗算を手動で行うことができ、これが発生する理由を確認できます。

フラグメント シェーダーの変更scene.fragは非常に複雑です。1 つずつステップを進めていきましょう。

<scene.frag>

#version 330

const int MAX_POINT_LIGHTS = 5;
const int MAX_SPOT_LIGHTS = 5;
const float SPECULAR_POWER = 10;

in vec3 outPosition;
in vec3 outNormal;
in vec2 outTextCoord;

out vec4 fragColor;
...

最初に、サポートするポント ライトとスポット ライトの最大数に対する定数の最大値を定義します。これらのライトのデータは、コンパイル時に適切に定義されたサイズを持つ必要があるユニフォームの配列として渡されるため、これが必要です。また、頂点シェーダーから通常のデータを受け取っていることもわかります。その後、ライト データをモデル化する構造体を定義します。

...
struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};
struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float reflectance;
};
struct AmbientLight
{
    float factor;
    vec3 color;
};
struct PointLight {
    vec3 position;
    vec3 color;
    float intensity;
    Attenuation att;
};
struct SpotLight
{
    PointLight pl;
    vec3 conedir;
    float cutoff;
};
struct DirLight
{
    vec3 color;
    vec3 direction;
    float intensity;
};
...

その後、ライト データの新しいユニフォームを定義します。

...
uniform sampler2D txtSampler;
uniform Material material;
uniform AmbientLight ambientLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
uniform DirLight dirLight
...

次に、周囲光から始めて、各ライト タイプの効果を計算する関数をいくつか定義します。

...
vec4 calcAmbient(AmbientLight ambientLight, vec4 ambient) {
    return vec4(ambientLight.factor * ambientLight.color, 1) * ambient;
}
...

ご覧のとおり、マテリアルのアンビエント カラーに適用される係数によってアンビエント ライトの色を変調するだけです。ここで、さまざまな種類のライトに対してカラー ライトを計算する方法を定義する関数を定義します。

...
vec4 calcLightColor(vec4 diffuse, vec4 specular, vec3 lightColor, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal) {
    vec4 diffuseColor = vec4(0, 0, 0, 1);
    vec4 specColor = vec4(0, 0, 0, 1);

    // Diffuse Light
    float diffuseFactor = max(dot(normal, to_light_dir), 0.0);
    diffuseColor = diffuse * vec4(lightColor, 1.0) * light_intensity * diffuseFactor;

    // Specular Light
    vec3 camera_direction = normalize(-position);
    vec3 from_light_dir = -to_light_dir;
    vec3 reflected_light = normalize(reflect(from_light_dir, normal));
    float specularFactor = max(dot(camera_direction, reflected_light), 0.0);
    specularFactor = pow(specularFactor, SPECULAR_POWER);
    specColor = specular * light_intensity  * specularFactor * material.reflectance * vec4(lightColor, 1.0);

    return (diffuseColor + specColor);
}
...

前のコードは比較的単純で、拡散コンポーネントの色を計算し、鏡面コンポーネントの別の色を計算し、処理中の頂点への移動中に光が受ける減衰によって変調します。これで、各タイプのライトに対して呼び出される関数を定義できます。ポイント ライトから始めます。

...
vec4 calcPointLight(vec4 diffuse, vec4 specular, PointLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec4 light_color = calcLightColor(diffuse, specular, light.color, light.intensity, position, to_light_dir, normal);

    // Apply Attenuation
    float distance = length(light_direction);
    float attenuationInv = light.att.constant + light.att.linear * distance +
    light.att.exponent * distance * distance;
    return light_color / attenuationInv;
}
...

ご覧のとおり、光の方向を (法線として) 計算し、その情報を使用して、マテリアルの拡散色と反射色、光の色、強度、位置、方向、および法線方向を使用して、光の色を計算します。 . その後、減衰を適用します。スポット ライトの機能は次のとおりです。

...
vec4 calcSpotLight(vec4 diffuse, vec4 specular, SpotLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.pl.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec3 from_light_dir  = -to_light_dir;
    float spot_alfa = dot(from_light_dir, normalize(light.conedir));

    vec4 color = vec4(0, 0, 0, 0);

    if (spot_alfa > light.cutoff)
    {
        color = calcPointLight(diffuse, specular, light.pl, position, normal);
        color *= (1.0 - (1.0 - spot_alfa)/(1.0 - light.cutoff));
    }
    return color;
}
...

手順は、光の円錐の内側にいるかどうかを制御する必要があることを除いて、ポイント ライトに似ています。先に説明したように、円錐状の光の内側にも減衰を適用する必要があります。最後に、ディレクショナル ライトの関数を以下に定義します。

...
vec4 calcDirLight(vec4 diffuse, vec4 specular, DirLight light, vec3 position, vec3 normal) {
    return calcLightColor(diffuse, specular, light.color, light.intensity, position, normalize(light.direction), normal);
}
...

SceneRenderクラス

この場合、光の方向はすでにわかっており、減衰がないため、光の位置を考慮する必要はありません。最後に、mainメソッドでは、最終的なフラグメント カラーの拡散鏡面コンポーネントに寄与するさまざまなライト タイプを反復処理します。

public class SceneRender {

    private static final int MAX_POINT_LIGHTS = 5;
    private static final int MAX_SPOT_LIGHTS = 5;
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("material.ambient");
        uniformsMap.createUniform("material.diffuse");
        uniformsMap.createUniform("material.specular");
        uniformsMap.createUniform("material.reflectance");
        uniformsMap.createUniform("ambientLight.factor");
        uniformsMap.createUniform("ambientLight.color");

        for (int i = 0; i < MAX_POINT_LIGHTS; i++) {
            String name = "pointLights[" + i + "]";
            uniformsMap.createUniform(name + ".position");
            uniformsMap.createUniform(name + ".color");
            uniformsMap.createUniform(name + ".intensity");
            uniformsMap.createUniform(name + ".att.constant");
            uniformsMap.createUniform(name + ".att.linear");
            uniformsMap.createUniform(name + ".att.exponent");
        }
        for (int i = 0; i < MAX_SPOT_LIGHTS; i++) {
            String name = "spotLights[" + i + "]";
            uniformsMap.createUniform(name + ".pl.position");
            uniformsMap.createUniform(name + ".pl.color");
            uniformsMap.createUniform(name + ".pl.intensity");
            uniformsMap.createUniform(name + ".pl.att.constant");
            uniformsMap.createUniform(name + ".pl.att.linear");
            uniformsMap.createUniform(name + ".pl.att.exponent");
            uniformsMap.createUniform(name + ".conedir");
            uniformsMap.createUniform(name + ".cutoff");
        }

        uniformsMap.createUniform("dirLight.color");
        uniformsMap.createUniform("dirLight.direction");
        uniformsMap.createUniform("dirLight.intensity");
    }
    ...
}

配列を使用している場合、リストの各要素に対してユニフォームを作成する必要があります。たとえば、pointLights
pointLights[0]、などの統一された名前の配列を作成する必要がありますpointLights[1]。もちろん、これは構造体の属性にも変換されるため、、などになりpointLights[0].colorますpointLights[1], color。

ダー呼び出しごとにライトのユニフォームを更新する新しいメソッドを作成します。このメソッドにupdateLightsは次のように名前が付けられ、定義されます。

public class SceneRender {
    ...
    private void updateLights(Scene scene) {
        Matrix4f viewMatrix = scene.getCamera().getViewMatrix();

        SceneLights sceneLights = scene.getSceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        uniformsMap.setUniform("ambientLight.factor", ambientLight.getIntensity());
        uniformsMap.setUniform("ambientLight.color", ambientLight.getColor());

        DirLight dirLight = sceneLights.getDirLight();
        Vector4f auxDir = new Vector4f(dirLight.getDirection(), 0);
        auxDir.mul(viewMatrix);
        Vector3f dir = new Vector3f(auxDir.x, auxDir.y, auxDir.z);
        uniformsMap.setUniform("dirLight.color", dirLight.getColor());
        uniformsMap.setUniform("dirLight.direction", dir);
        uniformsMap.setUniform("dirLight.intensity", dirLight.getIntensity());

        List<PointLight> pointLights = sceneLights.getPointLights();
        int numPointLights = pointLights.size();
        PointLight pointLight;
        for (int i = 0; i < MAX_POINT_LIGHTS; i++) {
            if (i < numPointLights) {
                pointLight = pointLights.get(i);
            } else {
                pointLight = null;
            }
            String name = "pointLights[" + i + "]";
            updatePointLight(pointLight, name, viewMatrix);
        }

        List<SpotLight> spotLights = sceneLights.getSpotLights();
        int numSpotLights = spotLights.size();
        SpotLight spotLight;
        for (int i = 0; i < MAX_SPOT_LIGHTS; i++) {
            if (i < numSpotLights) {
                spotLight = spotLights.get(i);
            } else {
                spotLight = null;
            }
            String name = "spotLights[" + i + "]";
            updateSpotLight(spotLight, name, viewMatrix);
        }
    }
    ...
}

コードは非常に単純です。環境光をディレクショナル ライト ユニフォームに設定することから始め、その後、配列の各要素のユニフォームを設定する専用のメソッドを持つポイント ライトとスポット ライトを反復処理します。

public class SceneRender {
    ...
    private void updatePointLight(PointLight pointLight, String prefix, Matrix4f viewMatrix) {
        Vector4f aux = new Vector4f();
        Vector3f lightPosition = new Vector3f();
        Vector3f color = new Vector3f();
        float intensity = 0.0f;
        float constant = 0.0f;
        float linear = 0.0f;
        float exponent = 0.0f;
        if (pointLight != null) {
            aux.set(pointLight.getPosition(), 1);
            aux.mul(viewMatrix);
            lightPosition.set(aux.x, aux.y, aux.z);
            color.set(pointLight.getColor());
            intensity = pointLight.getIntensity();
            PointLight.Attenuation attenuation = pointLight.getAttenuation();
            constant = attenuation.getConstant();
            linear = attenuation.getLinear();
            exponent = attenuation.getExponent();
        }
        uniformsMap.setUniform(prefix + ".position", lightPosition);
        uniformsMap.setUniform(prefix + ".color", color);
        uniformsMap.setUniform(prefix + ".intensity", intensity);
        uniformsMap.setUniform(prefix + ".att.constant", constant);
        uniformsMap.setUniform(prefix + ".att.linear", linear);
        uniformsMap.setUniform(prefix + ".att.exponent", exponent);
    }

    private void updateSpotLight(SpotLight spotLight, String prefix, Matrix4f viewMatrix) {
        PointLight pointLight = null;
        Vector3f coneDirection = new Vector3f();
        float cutoff = 0.0f;
        if (spotLight != null) {
            coneDirection = spotLight.getConeDirection();
            cutoff = spotLight.getCutOff();
            pointLight = spotLight.getPointLight();
        }

        uniformsMap.setUniform(prefix + ".conedir", coneDirection);
        uniformsMap.setUniform(prefix + ".conedir", cutoff);
        updatePointLight(pointLight, prefix + ".pl", viewMatrix);
    }
    ...
}

すでに述べたように、これらのライトの座標はビュー スペース内にある必要があります。通常、ワールド空間座標でライト座標を設定するため、シェーダーで使用できるようにするには、それらをビュー マトリックスで乗算する必要があります。最後に、メソッドを更新してrenderメソッドを呼び出しupdateLights、モデル マテリアルの新しい要素を適切に設定する必要があります。

public class SceneRender {
    ...
    public void render(Scene scene) {
        ...
        updateLights(scene);
        ...
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                uniformsMap.setUniform("material.ambient", material.getAmbientColor());
                uniformsMap.setUniform("material.diffuse", material.getDiffuseColor());
                uniformsMap.setUniform("material.specular", material.getSpecularColor());
                uniformsMap.setUniform("material.reflectance", material.getReflectance());
                ...
            }
        }
        ...
    }
    ...
}

UniformsMapクラス

UniformsMapまた、float と 3D ベクトルの値を設定するためのユニフォームを作成するためのメソッドのペアをクラスに追加する必要があります。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, float value) {
        glUniform1f(getUniformLocation(uniformName), value);
    }

    public void setUniform(String uniformName, Vector3f value) {
        glUniform3f(getUniformLocation(uniformName), value.x, value.y, value.z);
    }
    ...
}

ライトコントロール

最後のステップは、Mainクラスでライトを使用することです。ただし、その前に、Imgui を使用して GUI を作成し、ライト パラメータを制御する要素をいくつか提供します。という名前の新しいクラスでこれを行いますLightControls。コードは長すぎますが、理解するのは非常に簡単です。GUI コントロールから値を取得するための一連の属性と、必要なパネルとウィジェットを描画するためのメソッドを設定するだけで済みます。

package org.lwjglb.game;

import imgui.*;
import imgui.flag.ImGuiCond;
import org.joml.*;
import org.lwjglb.engine.*;
import org.lwjglb.engine.scene.Scene;
import org.lwjglb.engine.scene.lights.*;

public class LightControls implements IGuiInstance {

    private float[] ambientColor;
    private float[] ambientFactor;
    private float[] dirConeX;
    private float[] dirConeY;
    private float[] dirConeZ;
    private float[] dirLightColor;
    private float[] dirLightIntensity;
    private float[] dirLightX;
    private float[] dirLightY;
    private float[] dirLightZ;
    private float[] pointLightColor;
    private float[] pointLightIntensity;
    private float[] pointLightX;
    private float[] pointLightY;
    private float[] pointLightZ;
    private float[] spotLightColor;
    private float[] spotLightCuttoff;
    private float[] spotLightIntensity;
    private float[] spotLightX;
    private float[] spotLightY;
    private float[] spotLightZ;

    public LightControls(Scene scene) {
        SceneLights sceneLights = scene.getSceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        Vector3f color = ambientLight.getColor();

        ambientFactor = new float[]{ambientLight.getIntensity()};
        ambientColor = new float[]{color.x, color.y, color.z};

        PointLight pointLight = sceneLights.getPointLights().get(0);
        color = pointLight.getColor();
        Vector3f pos = pointLight.getPosition();
        pointLightColor = new float[]{color.x, color.y, color.z};
        pointLightX = new float[]{pos.x};
        pointLightY = new float[]{pos.y};
        pointLightZ = new float[]{pos.z};
        pointLightIntensity = new float[]{pointLight.getIntensity()};

        SpotLight spotLight = sceneLights.getSpotLights().get(0);
        pointLight = spotLight.getPointLight();
        color = pointLight.getColor();
        pos = pointLight.getPosition();
        spotLightColor = new float[]{color.x, color.y, color.z};
        spotLightX = new float[]{pos.x};
        spotLightY = new float[]{pos.y};
        spotLightZ = new float[]{pos.z};
        spotLightIntensity = new float[]{pointLight.getIntensity()};
        spotLightCuttoff = new float[]{spotLight.getCutOffAngle()};
        Vector3f coneDir = spotLight.getConeDirection();
        dirConeX = new float[]{coneDir.x};
        dirConeY = new float[]{coneDir.y};
        dirConeZ = new float[]{coneDir.z};

        DirLight dirLight = sceneLights.getDirLight();
        color = dirLight.getColor();
        pos = dirLight.getDirection();
        dirLightColor = new float[]{color.x, color.y, color.z};
        dirLightX = new float[]{pos.x};
        dirLightY = new float[]{pos.y};
        dirLightZ = new float[]{pos.z};
        dirLightIntensity = new float[]{dirLight.getIntensity()};
    }

    @Override
    public void drawGui() {
        ImGui.newFrame();
        ImGui.setNextWindowPos(0, 0, ImGuiCond.Always);
        ImGui.setNextWindowSize(450, 400);

        ImGui.begin("Lights controls");
        if (ImGui.collapsingHeader("Ambient Light")) {
            ImGui.sliderFloat("Ambient factor", ambientFactor, 0.0f, 1.0f, "%.2f");
            ImGui.colorEdit3("Ambient color", ambientColor);
        }

        if (ImGui.collapsingHeader("Point Light")) {
            ImGui.sliderFloat("Point Light - x", pointLightX, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Point Light - y", pointLightY, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Point Light - z", pointLightZ, -10.0f, 10.0f, "%.2f");
            ImGui.colorEdit3("Point Light color", pointLightColor);
            ImGui.sliderFloat("Point Light Intensity", pointLightIntensity, 0.0f, 1.0f, "%.2f");
        }

        if (ImGui.collapsingHeader("Spot Light")) {
            ImGui.sliderFloat("Spot Light - x", spotLightX, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Spot Light - y", spotLightY, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Spot Light - z", spotLightZ, -10.0f, 10.0f, "%.2f");
            ImGui.colorEdit3("Spot Light color", spotLightColor);
            ImGui.sliderFloat("Spot Light Intensity", spotLightIntensity, 0.0f, 1.0f, "%.2f");
            ImGui.separator();
            ImGui.sliderFloat("Spot Light cutoff", spotLightCuttoff, 0.0f, 360.0f, "%2.f");
            ImGui.sliderFloat("Dir cone - x", dirConeX, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir cone - y", dirConeY, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir cone - z", dirConeZ, -1.0f, 1.0f, "%.2f");
        }

        if (ImGui.collapsingHeader("Dir Light")) {
            ImGui.sliderFloat("Dir Light - x", dirLightX, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir Light - y", dirLightY, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir Light - z", dirLightZ, -1.0f, 1.0f, "%.2f");
            ImGui.colorEdit3("Dir Light color", dirLightColor);
            ImGui.sliderFloat("Dir Light Intensity", dirLightIntensity, 0.0f, 1.0f, "%.2f");
        }

        ImGui.end();
        ImGui.endFrame();
        ImGui.render();
    }
    ...
}

最後に、GUI 入力を処理するメソッドが必要です。ここでは、マウスの状態に基づいて Imgui を更新し、入力が GUI コントロールによって消費されたかどうかを確認します。その場合、ユーザー入力に従ってクラスの属性を入力するだけです。

public class LightControls implements IGuiInstance {
    ...
    @Override
    public boolean handleGuiInput(Scene scene, Window window) {
        ImGuiIO imGuiIO = ImGui.getIO();
        MouseInput mouseInput = window.getMouseInput();
        Vector2f mousePos = mouseInput.getCurrentPos();
        imGuiIO.setMousePos(mousePos.x, mousePos.y);
        imGuiIO.setMouseDown(0, mouseInput.isLeftButtonPressed());
        imGuiIO.setMouseDown(1, mouseInput.isRightButtonPressed());

        boolean consumed = imGuiIO.getWantCaptureMouse() || imGuiIO.getWantCaptureKeyboard();
        if (consumed) {
            SceneLights sceneLights = scene.getSceneLights();
            AmbientLight ambientLight = sceneLights.getAmbientLight();
            ambientLight.setIntensity(ambientFactor[0]);
            ambientLight.setColor(ambientColor[0], ambientColor[1], ambientColor[2]);

            PointLight pointLight = sceneLights.getPointLights().get(0);
            pointLight.setPosition(pointLightX[0], pointLightY[0], pointLightZ[0]);
            pointLight.setColor(pointLightColor[0], pointLightColor[1], pointLightColor[2]);
            pointLight.setIntensity(pointLightIntensity[0]);

            SpotLight spotLight = sceneLights.getSpotLights().get(0);
            pointLight = spotLight.getPointLight();
            pointLight.setPosition(spotLightX[0], spotLightY[0], spotLightZ[0]);
            pointLight.setColor(spotLightColor[0], spotLightColor[1], spotLightColor[2]);
            pointLight.setIntensity(spotLightIntensity[0]);
            spotLight.setCutOffAngle(spotLightColor[0]);
            spotLight.setConeDirection(dirConeX[0], dirConeY[0], dirConeZ[0]);

            DirLight dirLight = sceneLights.getDirLight();
            dirLight.setPosition(dirLightX[0], dirLightY[0], dirLightZ[0]);
            dirLight.setColor(dirLightColor[0], dirLightColor[1], dirLightColor[2]);
            dirLight.setIntensity(dirLightIntensity[0]);
        }
        return consumed;
    }
}

Mainクラス

最後のステップは、Mainクラスを更新してライトを作成し、以前のメソッドdrawGuiとhandleGuiInputメソッドを削除することです (LightControlsクラスでの処理は省略します)。

public class Main implements IAppLogic {
    ...
    private LightControls lightControls;

    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-11", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        Model cubeModel = ModelLoader.loadModel("cube-model", "resources/models/cube/cube.obj",
                scene.getTextureCache());
        scene.addModel(cubeModel);

        cubeEntity = new Entity("cube-entity", cubeModel.getId());
        cubeEntity.setPosition(0, 0f, -2);
        cubeEntity.updateModelMatrix();
        scene.addEntity(cubeEntity);

        SceneLights sceneLights = new SceneLights();
        sceneLights.getAmbientLight().setIntensity(0.3f);
        scene.setSceneLights(sceneLights);
        sceneLights.getPointLights().add(new PointLight(new Vector3f(1, 1, 1),
                new Vector3f(0, 0, -1.4f), 1.0f));

        Vector3f coneDir = new Vector3f(0, 0, -1);
        sceneLights.getSpotLights().add(new SpotLight(new PointLight(new Vector3f(1, 1, 1),
                new Vector3f(0, 0, -1.4f), 0.0f), coneDir, 140.0f));

        lightControls = new LightControls(scene);
        scene.setGuiInstance(lightControls);
    }
    ...
    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done here
    }
}

最後に、これに似たものを見ることができます。

Java 3D LWJGL 改造 〜Imgui を使用した GUI 描画を作成する〜

Imgui を使用した GUI 描画を改造

依然学習したLWJGL Gitbookをヒントにして現在実装しているテキストRPGのLWJGL化を行いたいと考えております。

現状のテキストRPG

上の動画にあるように、コマンドプロンプトなどのCUIで実行する前提で作成しています。しかし、表示する内容として余計なものが多すぎると感じています。
なので、LWJGLを使用してウィンドウから起動できるように改造しようと考えております。

しかし、学習を開始してからいろいろと上手くいかない状況になり。。。
Imguiを単体で使用することにしました。ちょうどImguiのjavaバインディングがあったのでそちらを使用します。

【参照するドキュメント一覧】

1.LWJGLのGitbook

  1. テキストRPGのドキュメント
  2. Imguiのドキュメント
  3. ImguiのJavaDoc
  4. Imguiのチュートリアル

Imguiの実装に関して(Java Binding)

この動画を参照しました。

動画の参照ページ

Imguiのセットアップ

  1. こちらのサイトより「java-libraries.zip」をダウンロードします。
  2. imgui-app-X.XX.X-all.jarをプロジェクトから参照できるように設定します。
  3. 下のコードをコピペします。参照先はこちらです。
    
    import imgui.ImGui;
    import imgui.app.Application;

public class Main extends Application {
@Override
protected void configure(Configuration config) {
config.setTitle("Dear ImGui is Awesome!");
}

@Override
public void process() {
    ImGui.text("Hello, World!");
}

public static void main(String[] args) {
    launch(new Main());
}

}


自分が作成したクラスは次のようになっています。(ImguiMain.java)
```java
package jp.zenryoku.imgui;

import imgui.ImGui;
import imgui.app.Application;
import imgui.app.Configuration;

/** ImGuiの学習用メインクラス */
public class ImguiMain extends Application {
    @Override
    protected void configure(Configuration config) {
        config.setTitle("Dear ImGui is Awesome!");
    }

    @Override
    public void process() {
        ImGui.text("Hello, World!");
    }

    public static void main(String[] args) {
        launch(new ImguiMain());
    }
}

書いて動かしてみた動画です。

どこから学習するか?

チュートリアルの動画など見ましたが、結局のところImguiを基本文法の学習時と同じように学んだほうが早いと判断しました。
具体的には次のようなところを学びます。

  1. Imguiでハローワールド(これは、上記で実施済み)
  2. 同様に、ハローワールドを改造する。参考ページはこちら
  3. こちらのFAQを見ながらやりたいことを実行する

まずは、実行したのが2の参考ページを見ながら作成したコードを見てください。

public class ImguiMain extends Application {
    @Override
    protected void configure(Configuration config) {
        config.setTitle("Dear ImGui is Awesome!");
    }

    @Override
    public void process() {
        // Windowその1
        ImGui.begin("Title A");
        ImGui.text("Hello, World!");
        ImGui.end();

        // Windowその2
        ImGui.begin("Title B");
        ImGui.text("A nice World!");
        ImGui.end();
    }

    public static void main(String[] args) {
        launch(new ImguiMain());
    }
}

ここから、基本を学びます。参照先はこちらのページにあるものを見ます。

ImGui::ShowDemoWindow()はどこに?

上の参照先にある記事を読むと、「デモコードをみて理解してね」とあるのでそれを見るようにします。

  1. デモ・コードに移動します。
  2. 下のような画面で247行目からのプログラムがそうです。

ImGui::ShowDemoWindow()を読む

289-302行目では、デモウィンドウを表示するメソッドが呼ばれているようです。しかし、どのようにウィンドウを作成するのかわからないので細かく見ません。

330-334行目にあるコードが参考になりそうです。

    // We specify a default position/size in case there's no data in the .ini file.
    // We only do it to make the demo applications a little more welcoming, but typically this isn't required.
    const ImGuiViewport* main_viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver);
    ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_FirstUseEver);

上記のコードは、メソッド呼び出しを行っているだけなので、プログラムを動かしてみるのが早そうです。というわけでコードを書いて実行します。
ここでもポイントは、呼び出しているメソッド(関数 or ファンクション)の名前がC言語と変わっているかどうか?です。

<動かしてみる>

Javaのデモ・コードをみる

上記の「参照先はこちらのページ」にもあるように、デモ・コードから基本を学びます。
実際のプログラムは、ImGui::ShowDemoWindow();に実装しているようです。

しかし、上記のでもコードはC/C++なので、Javaでのサンプルコードが欲しいところ。。。
そんなわけで、見つけました。サンプルはGithubにありました。

ImGui Java-bindingがありました。

まずは、Mainクラスを参照します。

中身を読んでみたところ、同じディレクトリにあるクラスがすべて必要なようなのでこのクラスたちをダウンロードしてきます。

このページにある下のような部分をクリック

ダウンロードしてきたファイルを展開して、次のディレクトリに各ファイルがあります。

  • imgui-java/example/src/main/java/: 実行するJavaファイル
  • imgui-java/example/src/main/resources/: 使用するファイル(Tahoma.ttfなど)

実行してみた!

コードを書いてみる

ここで、サンプルコードをいじって書いたコードを紹介します。
ほぼ、Mainクラスをコピーしたものですが、process()メソッドの中身を書き換えて下のように修正、実行しました。

    @Override
    public void process() {
        if (ImGui.begin("Stories", ImGuiWindowFlags.AlwaysAutoResize)) {
            ImGui.text("Hello, World! " + FontAwesomeIcons.Smile);
            ImGui.sameLine();
            ImGui.text(String.valueOf(count));
            ExampleImGuiColorTextEdit.show(new ImBoolean(true));
        }
        ImGui.end();
    }

キーポイントになるのは「ExampleImGuiColorTextEdit.show(new ImBoolean(true));」の部分です。
ExampleImGuiColorTextEditクラスのstaticメソッド「show()」を呼び出しています。
早速、クラスの中身を見ます。

import imgui.ImGui;
import imgui.extension.texteditor.TextEditor;
import imgui.extension.texteditor.TextEditorLanguageDefinition;
import imgui.flag.ImGuiWindowFlags;
import imgui.type.ImBoolean;

import java.util.HashMap;
import java.util.Map;

public class ExampleImGuiColorTextEdit {
    private static final TextEditor EDITOR = new TextEditor();

    static {
        TextEditorLanguageDefinition lang = TextEditorLanguageDefinition.c();

        String[] ppnames = {
            "NULL", "PM_REMOVE",
            "ZeroMemory", "DXGI_SWAP_EFFECT_DISCARD", "D3D_FEATURE_LEVEL", "D3D_DRIVER_TYPE_HARDWARE", "WINAPI", "D3D11_SDK_VERSION", "assert"};
        String[] ppvalues = {
            "#define NULL ((void*)0)",
            "#define PM_REMOVE (0x0001)",
            "Microsoft's own memory zapper function\n(which is a macro actually)\nvoid ZeroMemory(\n\t[in] PVOID  Destination,\n\t[in] SIZE_T Length\n); ",
            "enum DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_DISCARD = 0",
            "enum D3D_FEATURE_LEVEL",
            "enum D3D_DRIVER_TYPE::D3D_DRIVER_TYPE_HARDWARE  = ( D3D_DRIVER_TYPE_UNKNOWN + 1 )",
            "#define WINAPI __stdcall",
            "#define D3D11_SDK_VERSION (7)",
            " #define assert(expression) (void)(                                                  \n" +
                "    (!!(expression)) ||                                                              \n" +
                "    (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \n" +
                " )"
        };

        // Adding custom preproc identifiers
        Map<String, String> preprocIdentifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            preprocIdentifierMap.put(ppnames[i], ppvalues[i]);
        }
        lang.setPreprocIdentifiers(preprocIdentifierMap);

        String[] identifiers = {
            "HWND", "HRESULT", "LPRESULT","D3D11_RENDER_TARGET_VIEW_DESC", "DXGI_SWAP_CHAIN_DESC","MSG","LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "TextEditor" };
        String[] idecls = {
            "typedef HWND_* HWND", "typedef long HRESULT", "typedef long* LPRESULT", "struct D3D11_RENDER_TARGET_VIEW_DESC", "struct DXGI_SWAP_CHAIN_DESC",
                "typedef tagMSG MSG\n * Message structure","typedef LONG_PTR LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "class TextEditor" };

        // Adding custom identifiers
        Map<String, String> identifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            identifierMap.put(identifiers[i], idecls[i]);
        }
        lang.setIdentifiers(identifierMap);

        EDITOR.setLanguageDefinition(lang);

        // Adding error markers
        Map<Integer, String> errorMarkers = new HashMap<>();
        errorMarkers.put(1, "Expected '>'");
        EDITOR.setErrorMarkers(errorMarkers);

        EDITOR.setTextLines(new String[]{
            "#include <iostream",
            "",
            "int main() {",
            "   std::cout << \"Hello, World!\" << std::endl;",
            "}"
        });
    }
    public static void show(final ImBoolean showImColorTextEditWindow) {
        ImGui.setNextWindowSize(500, 400);
        if (ImGui.begin("Text Editor", showImColorTextEditWindow,
                ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.MenuBar)) {
            if (ImGui.beginMenuBar()) {
                if (ImGui.beginMenu("File")) {
                    if (ImGui.menuItem("Save")) {
                        String textToSave = EDITOR.getText();
                        /// save text....
                    }

                    ImGui.endMenu();
                }
                if (ImGui.beginMenu("Edit")) {
                    final boolean ro = EDITOR.isReadOnly();
                    if (ImGui.menuItem("Read-only mode", "", ro)) {
                        EDITOR.setReadOnly(!ro);
                    }

                    ImGui.separator();

                    if (ImGui.menuItem("Undo", "ALT-Backspace", !ro && EDITOR.canUndo())) {
                        EDITOR.undo(1);
                    }
                    if (ImGui.menuItem("Redo", "Ctrl-Y", !ro && EDITOR.canRedo())) {
                        EDITOR.redo(1);
                    }

                    ImGui.separator();

                    if (ImGui.menuItem("Copy", "Ctrl-C", EDITOR.hasSelection())) {
                        EDITOR.copy();
                    }
                    if (ImGui.menuItem("Cut", "Ctrl-X", !ro && EDITOR.hasSelection())) {
                        EDITOR.cut();
                    }
                    if (ImGui.menuItem("Delete", "Del", !ro && EDITOR.hasSelection())) {
                        EDITOR.delete();
                    }
                    if (ImGui.menuItem("Paste", "Ctrl-V", !ro && ImGui.getClipboardText() != null)) {
                        EDITOR.paste();
                    }

                    ImGui.endMenu();
                }

                ImGui.endMenuBar();
            }

            int cposX = EDITOR.getCursorPositionLine();
            int cposY = EDITOR.getCursorPositionColumn();

            String overwrite = EDITOR.isOverwrite() ? "Ovr" : "Ins";
            String canUndo = EDITOR.canUndo() ? "*" : " ";

            ImGui.text(cposX + "/" + cposY + " " + EDITOR.getTotalLines() + " lines | " + overwrite + " | " + canUndo);

            EDITOR.render("TextEditor");

            ImGui.end();
        }
    }
}

処理の中身を見ていると「ImGui.XXX()」のメソッドを呼び出している処理がほとんどです。
つまり、ほぼ、ImGuiクラスで画面の作成を行っているということです。ということは、JavaDocを見れば何とかなりそうです。

ImGuiでテキストエリア

JavaDocを見ながら、なんとか作成しました。実行結果は下のようになりました。

プログラムは、元のコードから変更しました。
<Main.java>

    @Override
    public void process() {
        ExampleImGuiColorTextEdit.show(new ImBoolean(false));
    }

元々は、ImGui.begin()を使用して、メニューバーを追加する処理を行っていましたが、これを削除して「ExampleImGuiColorTextEdit」クラスのshow()メソッドで定義している処理を呼び出すだけに修正しました。

<ExampleImGuiColorTextEdit.java>

public class ExampleImGuiColorTextEdit {
    private static final TextEditor EDITOR = new TextEditor();
    private static final String SEP = System.lineSeparator();

    static {
        TextEditorLanguageDefinition lang = TextEditorLanguageDefinition.c();

        String[] ppnames = {
            "NULL", "PM_REMOVE",
            "ZeroMemory", "DXGI_SWAP_EFFECT_DISCARD", "D3D_FEATURE_LEVEL", "D3D_DRIVER_TYPE_HARDWARE", "WINAPI", "D3D11_SDK_VERSION", "assert"};
        String[] ppvalues = {
            "#define NULL ((void*)0)",
            "#define PM_REMOVE (0x0001)",
            "Microsoft's own memory zapper function\n(which is a macro actually)\nvoid ZeroMemory(\n\t[in] PVOID  Destination,\n\t[in] SIZE_T Length\n); ",
            "enum DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_DISCARD = 0",
            "enum D3D_FEATURE_LEVEL",
            "enum D3D_DRIVER_TYPE::D3D_DRIVER_TYPE_HARDWARE  = ( D3D_DRIVER_TYPE_UNKNOWN + 1 )",
            "#define WINAPI __stdcall",
            "#define D3D11_SDK_VERSION (7)",
            " #define assert(expression) (void)(                                                  \n" +
                "    (!!(expression)) ||                                                              \n" +
                "    (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \n" +
                " )"
        };

        // Adding custom preproc identifiers
        Map<String, String> preprocIdentifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            preprocIdentifierMap.put(ppnames[i], ppvalues[i]);
        }
        lang.setPreprocIdentifiers(preprocIdentifierMap);

        String[] identifiers = {
            "HWND", "HRESULT", "LPRESULT","D3D11_RENDER_TARGET_VIEW_DESC", "DXGI_SWAP_CHAIN_DESC","MSG","LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "TextEditor" };
        String[] idecls = {
            "typedef HWND_* HWND", "typedef long HRESULT", "typedef long* LPRESULT", "struct D3D11_RENDER_TARGET_VIEW_DESC", "struct DXGI_SWAP_CHAIN_DESC",
                "typedef tagMSG MSG\n * Message structure","typedef LONG_PTR LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "class TextEditor" };

        // Adding custom identifiers
        Map<String, String> identifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            identifierMap.put(identifiers[i], idecls[i]);
        }
        lang.setIdentifiers(identifierMap);

        EDITOR.setLanguageDefinition(lang);
    }

    public static void show(final ImBoolean showImColorTextEditWindow) {
        ImGui.setNextWindowSize(500, 400);
        if (ImGui.begin("Story", showImColorTextEditWindow)) {
            int cposX = EDITOR.getCursorPositionLine();
            int cposY = EDITOR.getCursorPositionColumn();

            String overwrite = EDITOR.isOverwrite() ? "Ovr" : "Ins";
            String canUndo = EDITOR.canUndo() ? "*" : " ";
            EDITOR.setReadOnly(true);

            //ImGui.text(cposX + "/" + cposY + " " + EDITOR.getTotalLines() + " lines | " + overwrite + " | " + canUndo);

            // 実装部分
            EDITOR.setText("GoodMorning" + SEP + "Hello World");
            EDITOR.render("TextEditor");

            ImGui.end();
        }
    }
}

同様に、メニューバー内に設定する文字列、Saveをクリックしたときの処理(処理内容はなし)が定義してありましたが、それを削除。
Mainクラスでshow(true)としていた部分をshow(false)に変更して、赤いラインを表示しないようにしました。

これで、目的の文字表示領域を作成することができました。

ImGui + TextRPG

テキストRPGを作成中でしたので、これを単体の画面で実行できるようにしたいと思っていたので、ImGuiは格好のライブラリです。
LWJGLを実行しないと実現できないと思っていましたが、ImGuiで事足りそうです。

TextRPGに関して、現状の考えとしてはImGuiをメインにして追加でLWJGLを実行したいと考えています。

TextRPGとは、下の動画のようなものです。これは、コマンドプロンプトで実行しているので「ゲーム」って感じがしない。。。と思っていたところです。