play.TechGeneral : Classes, multiple programs, and instanced drawing

tl;dr

In my previous post I wrote a set of classes for shaders and programs, along with some compile-time function execution to being in compile-time-checked variables from the shaders. My next step is to take OpenGL concepts like Vertex Array Objects, Array Buffers, Element Array Buffers, and wrap them in simple classes to ease their usage, and then to prove it out a bit by having two different sets of programs, shaders, buffers, and so forth to draw different things.

Here's the demo video I posted at the end of previous post that teased about these two sets of objects I'm describing in this post, in case you missed it:

OpenGL wrapper

Here are the classes that wrap the OpenGL concepts of arrays, buffers, and so forth:

module abandonedtemple.demos.demo3_glwrapper;

import std.range : ElementType;
import std.stdio : writefln;
import std.traits : isArray;

import derelict.opengl3.gl3:
    glGenBuffers,
    glGenVertexArrays,
    glBindBuffer,
    glBindVertexArray,
    glBufferData,
    glDeleteBuffers,
    glDeleteVertexArrays,
    GL_ARRAY_BUFFER,
    GL_ELEMENT_ARRAY_BUFFER
    ;

class VertexArray {
    private uint _location;
    this() {
        glGenVertexArrays(1, &_location);
    }

    ~this() {
        writefln("Destroying Vertex Array at location %d", _location);
        glDeleteVertexArrays(1, &_location);
    }

    void bind() {
        glBindVertexArray(_location);
    }

    void unbind() {
        glBindVertexArray(0);
    }
}

mixin template Buffer() {
    private uint _location;
    this() {
        glGenBuffers(1, &_location);
    }
    ~this() {
        writefln("Destroying buffer of type %s at location %d", _type, _location);
        glDeleteBuffers(1, &_location);
    }

    void bind() {
        glBindBuffer(_type, _location);
    }

    void unbind() {
        glBindBuffer(_type, 0);
    }

    void setData(T)(const auto ref T data, uint usage) if (isArray!T) {
        glBindBuffer(_type, _location);
        auto size = data.length * ElementType!T.sizeof;
        glBufferData(_type,
            size,
            data.ptr,
            usage);
    }
}

class ArrayBuffer {
    static uint _type = GL_ARRAY_BUFFER;
    mixin Buffer;
}

class ElementArrayBuffer {
    static uint _type = GL_ELEMENT_ARRAY_BUFFER;
    mixin Buffer;
}

I'm erring on the side of explicitness in my imports - only grabbing what I'm using from the derelict.opengl3.gl3 module. I'm not a particular fan (yet?) of not knowing where my functions are coming from.

The VertexArray class is fairly simple, since basically all you can do with vertex array objects are create, bind, unbind, and delete them.

ArrayBuffer and ElementArrayBuffer are basically identical except for the type. I used a mixin template here instead of inheritance because I didn't see the value of treating ArrayBuffer and ElementArrayBuffer objects ever as something similar. I don't need to create a generic Buffer argument that could take either, so there is no reason they should share an interface or base class. Might turn out to be the wrong choice, but it should be easy to change.

The Buffer mixin template is fairly straight-forward except for setData. Here's that function declaration again:

void setData(T)(const auto ref T data, uint usage) if (isArray!T) {

setData is a method templated by a single type T, which is the base type of the data argument. data is qualified as being const auto ref. const means that the function will not be changing data. auto ref means that the "refness" of data will depend on whether it is "refable". It will be a ref if it can be (ie, data is an lvalue), and not ref if it can't.

More at Function Templates with Auto Ref Parameters.

The is (isArray!T) is the final puzzle - it says that this method only applies if T has the trait isArray. This is a template constraint. More traits live in std.traits, and you can create your own too.

In this case, setData being given a D array is able to get the pointer to the first value, as well as the size in bytes. The latter uses ElementType, which is a template that returns the type of the element in the array (and in a range, per its location in std.range). From the length (all D arrays know their length) and the size of the element type, the size in bytes can be calculated.

RainbowCube

Now I want to encapsulate all that's involved in drawing the rainbow cubes into a class.

class RainbowCube {
    VertexArray va;
    ArrayBuffer vertices;
    ElementArrayBuffer cube;
    ElementArrayBuffer lines;

    RainbowProgram program;

    this(RainbowProgram p) {
        program = p;

        va = new VertexArray();
        va.bind();

        const float vertices_[] = [
            -1f, -1f, -1f, 1f,   1f,  0f, 0f,
            -1f,  1f, -1f, 1f,   0f,  1f, 0f,
             1f,  1f, -1f, 1f,   0f,  0f, 1f,
             1f, -1f, -1f, 1f,   1f,  1f, 0f,

            -1f, -1f,  1f, 1f,   0f,  0f, 1f,
            -1f,  1f,  1f, 1f,   1f,  1f, 0f,
             1f,  1f,  1f, 1f,   1f,  0f, 0f,
             1f, -1f,  1f, 1f,   0f,  1f, 0f,
        ];
        vertices = new ArrayBuffer();
        vertices.setData!(const float[])(vertices_, GL_STATIC_DRAW);

        ushort cube_elements[] = [
            // back face
            0, 1, 2,
            0, 2, 3,
            // front face
            4, 5, 6,
            4, 6, 7,
            // left face
            0, 4, 5,
            0, 1, 5,
            // right face
            2, 6, 7,
            2, 3, 7,
            // bottom face
            0, 3, 4,
            3, 4, 7,
            // top face
            1, 2, 5,
            2, 5, 6,
        ];
        cube = new ElementArrayBuffer();
        cube.setData!(ushort[])(cube_elements, GL_STATIC_DRAW);

        ushort line_elements[] = [
            // back face
            0, 1,
            1, 2,
            2, 3,
            3, 0,

            // front face
            4, 5,
            5, 6,
            6, 7,
            7, 4,

            // remainder of left face
            // 0, 1, // already in back face
            // 4, 5, // already in front face
            0, 4,
            1, 5,

            // remainder of right face
            // 2, 3, // already in back face
            // 6, 7, // already in front face
            2, 6,
            3, 7,

            // top and bottom faces already have all lines
        ];
        lines = new ElementArrayBuffer();
        lines.setData!(ushort[])(line_elements, GL_STATIC_DRAW);

        lines.unbind();
        va.unbind();
    }

    void draw() {
        program.uniforms.is_line = 0;
        vertices.bind();
        cube.bind();
        // Layout of the stuff to draw
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 7 * float.sizeof, cast(void*)(0 * float.sizeof));
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 7 * float.sizeof, cast(void*)(4 * float.sizeof));

        // Draw it!
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, cast(void *)0);

        program.uniforms.is_line = 1;
        lines.bind();

        glDrawElements(GL_LINES, 24, GL_UNSIGNED_SHORT, cast(void *)0);
    }

    void bind() {
        va.bind();
        program.use();
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
    }

    void unbind() {
        glDisableVertexAttribArray(1);
        glDisableVertexAttribArray(0);
        va.unbind();
    }
}

The top-level functions are initialisation (done in the constructor), drawing, and the inevitable binding and unbinding. The latter can't (yet) be put into draw, since some activities that might be involved in drawing aren't (yet) encapsulated in the class - for example, setting up the rotation uniforms.

There's still a lot of code in initialisation - although mostly in setting up the vertices and the arrays. The downside of classes also shows here - we need to construct them (not needed with a struct). And the bind and unbind calls are still a little annoying.

This should be greatly simplified when I store the vertex information in external files instead of code (and remember, I still have the option to put them in the binary by using compile-time function execution, if it makes sense). I will probably look at converting to struct for the wrappers, and make a bind/unbind wrapper template class I can use when I want to use them.

draw, bind, and unbind are a wash in terms of number of lines of code, but not having to call the different underlying OpenGL functions saves some headache.

Demo class

Here's a simplified version of what Demo looks like now:

class Demo : DemoBase {
    private {
        RainbowCube rainbowCube;
        RainbowProgram rainbowProgram;

        mat4 frustumMatrix;

        void bufferInit() {
            rainbowCube = new RainbowCube(rainbowProgram);
        }

        void drawRainbowCubes() {
            rainbowCube.bind();

            rainbowProgram.uniforms.u_frustum.setTranspose(true);
            rainbowProgram.uniforms.u_frustum = frustumMatrix;

            auto cube_translations = [
                [ -1.2f, -1.0f, -1.2f, 2f, -4.5f ], // left, bottom, back
                [ -1.2f,    0f,  1.2f, -2f, -3.5f ], // left, middle, front
                [ -1.2f,  1.2f,    0f, 1f, 2.5f ], // left, top, middle
                [    0f, -1.0f,  1.2f, -1f, 2.5f ], // middle, bottom, front
                [    0f,    0f,    0f, 9f, 5.5f ], // middle, middle, middle
                [    0f,  1.2f, -1.2f, -3f, 6.5f ], // middle, top, back
                [  1.2f, -1.0f,    0f, 1f, -2.5f ], // right, bottom, middle
                [  1.2f,    0f, -1.2f, -2f, -4.5f ], // right, middle, back
                [  1.2f,  1.2f,  1.2f, 2f, 7.5f ], // right, top, front
            ];

            foreach (float[] translation; cube_translations) {
                auto matrix = mat4.identity
                    .rotatez(timeDiff * translation[3])
                    .rotatex(timeDiff * translation[4])
                    .scale(0.3, 0.3, 0.3)
                    ;
                rainbowProgram.uniforms.u_transform = matrix;
                rainbowProgram.uniforms.u_offset = vec4((translation[0] + 0.1) * (1 + sin(timeDiff * (4 / translation[3])) / 2), translation[1] * (1 + sin(timeDiff / 2) / 4), -3.5 + translation[2], 0f);

                // What to draw
                rainbowCube.draw();
            }

            // Disable all the things
            rainbowCube.unbind();
            glUseProgram(0);
        }

        void display() {
            glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

            drawRainbowCubes();
        }

        void init() {
            rainbowProgram = new RainbowProgram();
            glClearColor(0.0f, 0.0f, 0.3f, 0.0f);

            bufferInit();
        }
    }
}

It's a lot shorter now, and how to add new programs and things to draw with them is fairly straight-forward. And there is relatively little use of underlying OpenGL functions at this layer.

The rainbowProgram.uniforms having compile-time type checking has been pretty useful while playing around, so I'm currently fairly happy with it despite the gnarliness of the code.

Something new

The above changes just changed the code, but the demo does basically the same thing it did before.

I wanted to put a floor in, and here's what I did:

class ChessCube {
    VertexArray va;
    ArrayBuffer vertices;
    ElementArrayBuffer cube;
    ElementArrayBuffer lines;

    ChessProgram program;

    this(ChessProgram p) {
        program = p;

        va = new VertexArray();
        va.bind();

        const float vertices_[] = [
            -1f, -1f, -1f, 1f,   0f,  0f, 0f,
            -1f,  1f, -1f, 1f,   1f,  1f, 1f,
             1f,  1f, -1f, 1f,   1f,  1f, 1f,
             1f, -1f, -1f, 1f,   0f,  0f, 0f,

            -1f, -1f,  1f, 1f,   0f,  0f, 0f,
            -1f,  1f,  1f, 1f,   1f,  1f, 1f,
             1f,  1f,  1f, 1f,   1f,  1f, 1f,
             1f, -1f,  1f, 1f,   0f,  0f, 0f,
        ];
        vertices = new ArrayBuffer();
        vertices.setData!(const float[])(vertices_, GL_STATIC_DRAW);

        ushort cube_elements[] = [
            // back face
            0, 1, 2,
            0, 2, 3,
            // front face
            4, 5, 6,
            4, 6, 7,
            // left face
            0, 4, 5,
            0, 1, 5,
            // right face
            2, 6, 7,
            2, 3, 7,
            // bottom face
            0, 3, 4,
            3, 4, 7,
            // top face
            1, 2, 5,
            2, 5, 6,
        ];
        cube = new ElementArrayBuffer();
        cube.setData!(ushort[])(cube_elements, GL_STATIC_DRAW);

        cube.unbind();
        va.unbind();
    }

    void draw() {
        vertices.bind();
        cube.bind();
        // Layout of the stuff to draw
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 7 * float.sizeof, cast(void*)(0 * float.sizeof));
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 7 * float.sizeof, cast(void*)(4 * float.sizeof));

        // Draw it!
        glDrawElementsInstanced(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, cast(void *)0, 8192);
    }

    void bind() {
        va.bind();
        program.use();
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
    }

    void unbind() {
        glDisableVertexAttribArray(1);
        glDisableVertexAttribArray(0);
        va.unbind();
    }
}

The first thing that strikes me is that it is almost identical to the RainbowCube, which makes me want to run off and find some way to share code. But ignoring that for a second...

glDrawElementsInstanced is new! Here is the old and new lines next to each other:

glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, cast(void *)0);
glDrawElementsInstanced(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, cast(void *)0, 8192);

The only difference is the 8192 at the end. That says to do 8192 instances of the draw command. That sounds a bit silly at first, since what is the point of drawing 8192 cubes at the same location?

The vertex shader tells the story:

#version 330 core
layout(location = 0) in vec4 pos;
layout(location = 1) in vec3 color;

uniform mat4 u_transform;
uniform vec4 u_offset;
uniform mat4 u_frustum;
uniform int width;

out vec3 Color;

void main(){
    float x;
    x = floor(gl_InstanceID / width);
    float y = mod(gl_InstanceID, width);
    if (mod(gl_InstanceID + x, 2) > 0.1) {
        Color = vec3(0.9, 0.9, 0.9);
    } else {
        Color = vec3(0.7, 0.7, 0.7);
    }
    vec4 offset = u_offset + vec4(y * 0.8, 0, -x * 0.8, 0);
    gl_Position = (pos * u_transform + offset) * u_frustum;
}

This could surely be written better, but I am still figuring out life with only floats! gl_InstanceID contains the instance number (from 0 to 8191 in this case) of the draw command. I use that to calculate x and y offsets to the original offset given to me (based on the new width uniform). This ends up creating a chess/checker board effect.

The fragment shader is the same as the one used for the rainbow cubes, and RainbowProgram is defined with:

mixin(program_from_shader_filenames("ChessProgram",
    ["demo3/FragmentShader.frag","demo3/ChessBoard.vert"]));

Mechanically putting code lines wherever rainbow cubes are being set up to set up the chess program, we're left with drawChessCubes:

void drawChessCubes() {
    chessCube.bind();

    chessProgram.uniforms.u_frustum.setTranspose(true);
    chessProgram.uniforms.u_frustum = frustumMatrix;

    auto matrix = mat4.identity.scale(0.4, 0.4, 0.4);
    chessProgram.uniforms.u_transform = matrix;
    chessProgram.uniforms.u_offset = vec4(-48, -2, -2, 0);
    chessProgram.uniforms.width = 128;

    // What to draw
    chessCube.draw();

    // Disable all the things
    chessCube.unbind();
    glUseProgram(0);
}

With a width of 128 and with 8192 instances, we'll end up with a 128x64 board of cubes.

Putting it together

Rainbow Cubes and Chess Board

Here is the commit that creates the classes that wrap the glBindBuffer and glBufferData calls and implements ChessCube. In the next commit I set the rainbow cubes spinning as shown in the example video and picture.

Next up

Textures! I make a textured six-sided die, including the texture itself.