Skip to content

Commit fa4d441

Browse files
committed
Code optimizations of indirect drawing
1 parent 15d1c20 commit fa4d441

File tree

1 file changed

+142
-18
lines changed

1 file changed

+142
-18
lines changed

chapter-20/chapter-20.md

Lines changed: 142 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ void main() {
667667
The main changes are related to the way we access material information and textures. We will now have an array of materials information, which will be accessed by the index we calculated in the vertex shader which is now in the `outMaterialIdx` input variable (which has the `flat` modifier which states that this value should not be interpolated from vertex to fragment stage). We will be using an array of textures to access either regular textures or normal maps. The index to those textures are stored now in the `Material` struct.
668668

669669

670-
Now it is the turn to examine the changes in the `SceneRender` class. We will start by defining a set of constants that will be used in the code and by modifying the `createUniforms` according to the changes in the shaders shown before:
670+
Now it is the turn to examine the changes in the `SceneRender` class. We will start by defining a set of constants that will be used in the code, one handle for the buffer that will have the indirect drawing instructions (`staticRenderBufferHandle`) and the number of drawing commands (`staticDrawCount`). We will need also to modify the `createUniforms` method according to the changes in the shaders shown before:
671671

672672
```java
673673
public class RenderBuffers {
@@ -678,6 +678,9 @@ public class RenderBuffers {
678678
private static final int MAX_MATERIALS = 20;
679679
private static final int MAX_TEXTURES = 16;
680680
...
681+
private int staticDrawCount;
682+
private int staticRenderBufferHandle;
683+
...
681684
private void createUniforms() {
682685
uniformsMap = new UniformsMap(shaderProgram.getProgramId());
683686
uniformsMap.createUniform("projectionMatrix");
@@ -715,7 +718,7 @@ The main changes are in the `render` method, which is defined like this:
715718
```java
716719
public class RenderBuffers {
717720
...
718-
public void render(Scene scene, RenderBuffers globalBuffer, GBuffer gBuffer) {
721+
public void render(Scene scene, RenderBuffers renderBuffers, GBuffer gBuffer) {
719722
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId());
720723
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
721724
glViewport(0, 0, gBuffer.getWidth(), gBuffer.getHeight());
@@ -739,18 +742,20 @@ public class RenderBuffers {
739742
texture.bind();
740743
}
741744

742-
Map<String, Integer> entitiesIdxMap = new HashMap<>();
743745
int entityIdx = 0;
744746
for (Model model : scene.getModelMap().values()) {
745747
List<Entity> entities = model.getEntitiesList();
746748
for (Entity entity : entities) {
747-
entitiesIdxMap.put(entity.getId(), entityIdx);
748749
uniformsMap.setUniform("modelMatrices[" + entityIdx + "]", entity.getModelMatrix());
749750
entityIdx++;
750751
}
751752
}
752753

753-
renderStaticMeshes(scene, globalBuffer, entitiesIdxMap);
754+
// Static meshes
755+
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
756+
glBindVertexArray(renderBuffers.getStaticVaoId());
757+
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, staticDrawCount, 0);
758+
glBindVertexArray(0);
754759

755760
glEnable(GL_BLEND);
756761
shaderProgram.unbind();
@@ -759,34 +764,153 @@ public class RenderBuffers {
759764
}
760765
```
761766

762-
You can see that we now have to bind the array of texture samplers and activate all the texture units. In addition to that, we iterate over the entities and set up the uniform values for the model matrices. After that, we call the `renderStaticMeshes` method which will be the one that populates the indirect drawing buffer. In the next chapter we will see that we need to separate how we do this for static vs animated meshes. The `renderStaticMeshes` method is defined like this:
767+
You can see that we now have to bind the array of texture samplers and activate all the texture units. In addition to that, we iterate over the entities and set up the uniform values for the model matrices. After that, we call the `glMultiDrawElementsIndirect` function to perform the indirect drawing. Prior to that, we need to bind the buffers that hold drawing instructions (drawing commands) and the VAO that holds the meshes and indices data. But, when do we populate the buffer for indirect drawing=? The answer is that this not need to be performed each render call, if there are no changes in the number of entities, you can record that buffer once, and use it in each render call. In this specific example, we will just populate that buffer at start-up. This means, that, if you want to make changes in the number of entities, you would nee to re-create that buffer again (you should do that for your own engine).
763768

769+
The method that actually builds the indirect draw buffer is called `setupStaticCommandBuffer` and starts like this:
764770
```java
765771
public class RenderBuffers {
766772
...
767-
private void renderStaticMeshes(Scene scene, RenderBuffers globalBuffer, Map<String, Integer> entitiesIdxMap) {
773+
private void setupStaticCommandBuffer(Scene scene) {
768774
List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
775+
Map<String, Integer> entitiesIdxMap = new HashMap<>();
776+
int entityIdx = 0;
777+
int numMeshes = 0;
778+
for (Model model : scene.getModelMap().values()) {
779+
List<Entity> entities = model.getEntitiesList();
780+
numMeshes += model.getMeshDrawDataList().size();
781+
for (Entity entity : entities) {
782+
entitiesIdxMap.put(entity.getId(), entityIdx);
783+
entityIdx++;
784+
}
785+
}
786+
...
787+
}
788+
...
789+
}
790+
```
769791

770-
ByteBuffer commandBuffer = buildStaticCommandBuffer(modelList, entitiesIdxMap);
771-
int drawCount = commandBuffer.remaining() / COMMAND_SIZE;
772-
int bufferHandle = glGenBuffers();
773-
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, bufferHandle);
774-
glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);
792+
We firs start by iterating over the models to get the position in the list of entity instances each instance is. We store that information in a `Map`` using entity identifier as key. We will need this info later on since, the indirect drawing commands will be recorded iterating over meshes associated to each model. In addition to that, we calculate the total number of meshes. After that, we will create the buffer that wil hold indirect drawing instructions and populate it:
775793

776-
glBindVertexArray(globalBuffer.getStaticVaoId());
777-
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, drawCount, 0);
778-
glBindVertexArray(0);
794+
```java
795+
public class RenderBuffers {
796+
...
797+
private void setupStaticCommandBuffer(Scene scene) {
798+
...
799+
int firstIndex = 0;
800+
int baseInstance = 0;
801+
int drawElement = 0;
802+
shaderProgram.bind();
803+
ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
804+
for (Model model : modelList) {
805+
List<Entity> entities = model.getEntitiesList();
806+
int numEntities = entities.size();
807+
for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
808+
// count
809+
commandBuffer.putInt(meshDrawData.vertices());
810+
// instanceCount
811+
commandBuffer.putInt(numEntities);
812+
commandBuffer.putInt(firstIndex);
813+
// baseVertex
814+
commandBuffer.putInt(meshDrawData.offset());
815+
commandBuffer.putInt(baseInstance);
816+
817+
firstIndex += meshDrawData.vertices();
818+
baseInstance += entities.size();
819+
820+
for (Entity entity : entities) {
821+
String name = "drawElements[" + drawElement + "]";
822+
uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
823+
drawElement++;
824+
}
825+
}
826+
}
827+
commandBuffer.flip();
828+
shaderProgram.unbind();
829+
830+
staticDrawCount = commandBuffer.remaining() / COMMAND_SIZE;
831+
832+
staticRenderBufferHandle = glGenBuffers();
833+
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
834+
glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);
779835

780836
MemoryUtil.memFree(commandBuffer);
781-
glDeleteBuffers(bufferHandle);
782837
}
783838
...
784839
}
785840
```
786841

787-
In this case, we will populate the buffer that will hold the draw indirect instructions (by calling the `buildStaticCommandBuffer`). Each set of draw instructions si composed by five attributes, ech of the with a length of 4 bytes (total length of each set of parameters is what defines the `COMMAND_SIZE` constant). In this case, we are creating a new buffer in each draw call. This is not the most efficient way of doing it at all, but it keeps the things simple enough. In your game engine you will need to reuse a buffer. Also, there is no need to populate the indirect drawing buffer. We will keep this approach as an example, it gives you some flexibility so you can add new entities, but should think in caching for your engine.
842+
As you can see we firs allocate a `ByteBuffer`. This buffer will hold as many instruction sets as meshes. Each set of draw instructions si composed by five attributes, ech of the with a length of 4 bytes (total length of each set of parameters is what defines the `COMMAND_SIZE` constant). We cannot allocate this buffer using `MemoryStack` since we will run out of space quickly (the stack that LWJGL uses for this is limited in size). Therefore, we need to allocate it using `MemoryUtil` and remember to manually de-allocate that once we are done. Once we have the buffer we start iterating over the meshes associated to the model. You may have a look at the beginning of this chapter to check
843+
the struct that draw indirect requires. In addition to that, we also populate the `drawElements` uniform using the `Map` we calculated previously, to properly get the model matrix index for each entity. Finally, we just create a GPU buffer and dump the data into it.
844+
845+
We will need to update the `cleanup` method to free the indirect drawing buffer:
846+
847+
```java
848+
public class RenderBuffers {
849+
...
850+
public void cleanup() {
851+
shaderProgram.cleanup();
852+
glDeleteBuffers(staticRenderBufferHandle);
853+
}
854+
...
855+
}
856+
```
857+
858+
We will need a new method to the set up the values for the materials uniform:
859+
860+
```java
861+
public class RenderBuffers {
862+
...
863+
public void setupMaterialsUniform(TextureCache textureCache, MaterialCache materialCache) {
864+
List<Texture> textures = textureCache.getAll().stream().toList();
865+
int numTextures = textures.size();
866+
if (numTextures > MAX_TEXTURES) {
867+
Logger.warn("Only " + MAX_TEXTURES + " textures can be used");
868+
}
869+
Map<String, Integer> texturePosMap = new HashMap<>();
870+
for (int i = 0; i < Math.min(MAX_TEXTURES, numTextures); i++) {
871+
texturePosMap.put(textures.get(i).getTexturePath(), i);
872+
}
873+
874+
shaderProgram.bind();
875+
List<Material> materialList = materialCache.getMaterialsList();
876+
int numMaterials = materialList.size();
877+
for (int i = 0; i < numMaterials; i++) {
878+
Material material = materialCache.getMaterial(i);
879+
String name = "materials[" + i + "]";
880+
uniformsMap.setUniform(name + ".diffuse", material.getDiffuseColor());
881+
uniformsMap.setUniform(name + ".specular", material.getSpecularColor());
882+
uniformsMap.setUniform(name + ".reflectance", material.getReflectance());
883+
String normalMapPath = material.getNormalMapPath();
884+
int idx = 0;
885+
if (normalMapPath != null) {
886+
idx = texturePosMap.computeIfAbsent(normalMapPath, k -> 0);
887+
}
888+
uniformsMap.setUniform(name + ".normalMapIdx", idx);
889+
Texture texture = textureCache.getTexture(material.getTexturePath());
890+
idx = texturePosMap.computeIfAbsent(texture.getTexturePath(), k -> 0);
891+
uniformsMap.setUniform(name + ".textureIdx", idx);
892+
}
893+
shaderProgram.unbind();
894+
}
895+
...
896+
}
897+
```
898+
899+
We just check that we are not surpassing the maximum number of supported textures (`MAX_TEXTURES`) and just create an array of materials information with the information we used in the previous chapters. The only change is that we will need to store the index of the associated texture and normal maps in the material information.
900+
901+
To complete the changes in the `SceneRender` class, we will create a method that wraps the `setupXX` so it can be invoked from the `Render` class:
902+
903+
```java
904+
public class RenderBuffers {
905+
...
906+
public void setupData(Scene scene) {
907+
setupStaticCommandBuffer(scene);
908+
setupMaterialsUniform(scene.getTextureCache(), scene.getMaterialCache());
909+
}
910+
...
911+
}
912+
```
788913

789-
SceneRender
790914
ShadowRender
791915
shadow.vert
792916
Render

0 commit comments

Comments
 (0)