第 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();
}
...
}
}
すべての変更を実装すると、これに似たものが表示されるはずです。