DPE - Instance Rendering

Instance rendering

So what if you want to render millions of cubes instead of a handful? The answer is instance rendering and luckily DPE supports all of that!

instancing.png

We update the boilerplate code by adding tons of cubes. Find the code section where you add the cubes and change it to:

    /**
     * Create the shapes.
     */
    std::shared_ptr<DreadedPE::Cube> cube(std::make_shared<DreadedPE::Cube>(1, 1, 1));

    for (int x = -10; x < 11; ++x)
    {
        for (int y = -10; y < 11; ++y)
        {
            for (int z = -10; z < 11; ++z)
            {
                DreadedPE::Entity* cube_entity = new DreadedPE::Entity(scene_manager, &scene_manager.getRoot(), glm::translate(glm::mat4(1.0f), glm::vec3(x * 3, y * 3, z * 3)), DreadedPE::OBSTACLE, "Cube");
                DreadedPE::SceneLeafModel* scene_leaf_model = new DreadedPE::SceneLeafModel(*cube_entity, NULL, cube, wfl_material, InstanceShader::getShader(), false, false);
            }
        }
    }

This creates 8000 cubes. It will probably still render fine using the BasicShadowShader (probalby not with tons of lights though!), so feel free to pump up the number and see where it breaks. The main difference is that we use a different shader now. I'll show you how this shader looks like.

The instance shader inherrits from the DreadedPE::LightShader. The main difference with the other shaders is that the render function is implemented. The prepareToRender function collects the render calls and the render function renders them all in one go.

InstanceShader.h

#ifndef DEMO_INSTANCE_RENDERING_INSTANCE_SHADER_H
#define DEMO_INSTANCE_RENDERING_INSTANCE_SHADER_H

#include <glm/glm.hpp>
#include <vector>

#include <dpengine/shaders/LightShader.h>

namespace DreadedPE
{
    class SceneLeafModel;
    class SceneLeafLight;
};

class InstanceShader : public DreadedPE::LightShader
{
public:
    void prepareToRender(const DreadedPE::SceneLeafModel& model_node, const glm::mat4& view_matrix, const glm::mat4& model_matrix, const glm::mat4& projection_matrix, const std::vector<const DreadedPE::SceneLeafLight*>& lights);
    void render();
    static InstanceShader& getShader();

protected:
    InstanceShader(const std::string& vertex_shader, const std::string& fragment_shader);
    
    GLuint modelview_matrix_loc_, model_matrix_loc_, projection_matrix_loc_, view_matrix_loc_, texture0_loc_, material_ambient_loc_, material_diffuse_loc_, material_specular_loc_, material_emissive_loc_, transparency_loc_;
    
private:
    static InstanceShader* shader_;
    GLuint model_matixes_buffer_id_;

    const DreadedPE::SceneLeafModel* model_node_;
    glm::mat4 view_matrix_;
    std::vector<glm::mat4> model_matrixes_;
    glm::mat4 projection_matrix_;
    std::vector<const DreadedPE::SceneLeafLight*> lights_;
};

#endif

InstanceShader.cpp

#include "InstanceShader.h"

#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_inverse.hpp>

#include <dpengine/scene/SceneLeafModel.h>
#include <dpengine/scene/Material.h>
#include <dpengine/shapes/Shape.h>

#include <dpengine/texture/Texture.h>

InstanceShader* InstanceShader::shader_ = NULL;

InstanceShader::InstanceShader(const std::string& vertex_shader, const std::string& fragment_shader)
    : LightShader(vertex_shader, fragment_shader)
{

}

void InstanceShader::prepareToRender(const DreadedPE::SceneLeafModel& model_node, const glm::mat4& view_matrix, const glm::mat4& model_matrix, const glm::mat4& projection_matrix, const std::vector<const DreadedPE::SceneLeafLight*>& lights)
{
    model_node_ = &model_node;
    view_matrix_ = view_matrix;
    model_matrixes_.push_back(model_matrix);
    projection_matrix_ = projection_matrix;
    lights_ = lights;
}

So here you prepare the rendering and we accumulate the model matrixes that are used for rendering. Note that this code is faulty and only good for this demo! If any other shapes or view / projection matrixes are used it will be messed up!

void InstanceShader::render()
{
    if (model_matrixes_.empty()) return;

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);

    glBindBuffer(GL_ARRAY_BUFFER, model_node_->model_node_->getShape()getVertexBufferId());
    glVertexAttribPointer((GLint)0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, model_node_->model_node_->getShape()getTexCoordBufferId());
    glVertexAttribPointer((GLint)1, 2, GL_FLOAT, GL_FALSE, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, model_node_->model_node_->getShape()getNormalBufferId());
    glVertexAttribPointer((GLint)2, 3, GL_FLOAT, GL_FALSE, 0, 0);

    // Create the buffer that is to be changed per instance.
    glBindBuffer(GL_ARRAY_BUFFER, model_matixes_buffer_id_);
    for (unsigned int i = 0; i < 4; ++i)
    {
        glEnableVertexAttribArray(3 + i);
        glVertexAttribPointer(3 + i, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (const GLvoid*)(sizeof(GLfloat) * i * 4));
        glVertexAttribDivisor(3 + i, 1);
    }

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, model_node_->model_node_->getShape()getIndexBufferId());

    bindShader();

    LightShader::prepareToRender(*model_node_, view_matrix_, model_matrixes_[0], projection_matrix_, lights_);

    // TODO Normal matrix is false.
    glm::mat4 normal_matrix = glm::inverseTranspose(view_matrix_);

    //Send the modelview and projection matrices to the shaders
    glUniformMatrix4fv(projection_matrix_loc_, 1, false, glm::value_ptr(projection_matrix_));
    glUniformMatrix4fv(view_matrix_loc_, 1, false, glm::value_ptr(view_matrix_));

    assert(model_node_->getMaterial().get1DTextures().size() == 0);
    assert(model_node_->getMaterial().get2DTextures().size() == 1);

    glUniform1i(texture0_loc_, model_node_->getMaterial().get2DTextures()[0]->getActiveTextureId());

    const DreadedPE::MaterialLightProperty& material_ambient = model_node_->getMaterial().getAmbient();
    const DreadedPE::MaterialLightProperty& material_diffuse = model_node_->getMaterial().getDiffuse();
    const DreadedPE::MaterialLightProperty& material_specular = model_node_->getMaterial().getSpecular();
    const DreadedPE::MaterialLightProperty& material_emissive = model_node_->getMaterial().getEmissive();

    glUniform4f(material_ambient_loc_, material_ambient.red_, material_ambient.green_, material_ambient.blue_, material_ambient.alpha_);
    glUniform4f(material_diffuse_loc_, material_diffuse.red_, material_diffuse.green_, material_diffuse.blue_, material_diffuse.alpha_);
    glUniform4f(material_specular_loc_, material_specular.red_, material_specular.green_, material_specular.blue_, material_specular.alpha_);
    glUniform4f(material_emissive_loc_, material_emissive.red_, material_emissive.green_, material_emissive.blue_, material_emissive.alpha_);
    glUniform1f(transparency_loc_, model_node_->getMaterial().getTransparency());

    glBindBuffer(GL_ARRAY_BUFFER, model_matixes_buffer_id_);
    glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4) * model_matrixes_.size(), &model_matrixes_[0], GL_DYNAMIC_DRAW);

    // Render it!
    glDrawElementsInstanced(model_node_->model_node_->getShape()getRenderingMode(), model_node_->model_node_->getShape()getIndices().size(), GL_UNSIGNED_INT, 0, model_matrixes_.size());
    model_matrixes_.clear();
}

And here is the actual rendering. We bind all the buffers and buffer the model matrixes. Finally we make the instance render call and the result is a screen full of cubes! You can extend this by making the colours of the boxes instanced as well.

InstanceShader& InstanceShader::getShader()
{
    if (shader_ == NULL)
    {
        shader_ = new InstanceShader("DPE Demos/instance rendering/shaders/instanced.vert", "DPE Demos/instance rendering/shaders/instanced.frag");

        // Load the shader.
        if (!shader_->initialize())
        {
            std::cerr << "Failed to initialise the animated shadow shader." << std::endl;
            exit(1);
        }

        //Bind the attribute locations
        shader_->bindAttrib(0, "a_Vertex");
        shader_->bindAttrib(1, "a_TexCoord0");
        shader_->bindAttrib(2, "a_Normal");
        shader_->bindAttrib(3, "model_matrix");

        //Re link the program
        shader_->linkProgram();
        shader_->bindShader();
        shader_->resolveUniformNames();

        // Cache the locations of all the uniform variables.
        shader_->projection_matrix_loc_ = shader_->getUniformLocation("projection_matrix");
        shader_->view_matrix_loc_ = shader_->getUniformLocation("view_matrix");
        shader_->texture0_loc_ = shader_->getUniformLocation("texture0");
        shader_->material_ambient_loc_ = shader_->getUniformLocation("material_ambient");
        shader_->material_diffuse_loc_ = shader_->getUniformLocation("material_diffuse");
        shader_->material_specular_loc_ = shader_->getUniformLocation("material_specular");
        shader_->material_emissive_loc_ = shader_->getUniformLocation("material_emissive");

        // Create a buffer to hold the model matrixes.
        glGenBuffers(1, &shader_->model_matixes_buffer_id_);
    }
    return *shader_;
}

The last bit is the initialisation of the shader that is pretty much exactly like other shaders. The only difference is that glGenBuffers call to create a buffer for the set of model matrixes.