Search code examples
c++openglglslsdl-2uniform

Uniform block for variable amount of game objects not passing uniforms correctly?


I have been trying to get dynamic game objects to have uniforms passed to the fragment shader as uniform blocks, one is shown for simplicity in some boiler-plate code. The code compiles, and the vertex and fragment shader also compile correctly, but nothing is being displayed, implying I have not passed the uniforms correctly.

#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>

// Compile
// g++ test.cpp -lSDL2 -lGLEW -lGL -lglut

// point type for vertices of drawing plane
typedef struct
{

    GLfloat vec2[2];
    
} point_t;


// Drawing plane
const point_t drawing_plane_vertices[4] = 
{
     {-1.0, -1.0 }, 
     { 1.0, -1.0 }, 
     { 1.0,  1.0 }, 
     {-1.0,  1.0 }, 
};



typedef struct 
{

    float health;
    float attackStrength;
    
} Swordsman_t;


const GLuint MAX_NUM_SWORDSMAN = 100;

// Vertex shader source
const char* vertexShaderSource = R"(
    
    #version 430 core

    layout (location = 0) in vec2 position;

    out vec2 texCoord;

    void main() {
        gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
    }
)";

// Fragment shader source
const char* fragmentShaderSource = R"(
#version 430 core

#define MAX_NUM_SWORDSMAN 100

struct Swordsman
{

    float health;
    float attackStrength;

};

layout (std140) uniform uSwordsmanBlock
{


    Swordsman swordsman[MAX_NUM_SWORDSMAN];
    int num;

} uSwordsmanBlock_t;

uniform vec2 uresolution;
uniform float iTime;
vec2 fragCoord = gl_FragCoord.xy;

out vec4 fragColor;

void main(void)
{

    vec2 uv = fragCoord / uresolution * 2.0 - 1.0;
    uv.x *= (uresolution.x / uresolution.y);

    if (uSwordsmanBlock_t.num == 4)
    {

        // first problem this line isn't triggered.
        fragColor = vec4(1.0, 0.0, 0.0, 1.0);

    } else {
        // nothing, just black
        vec4 color = vec4(0.0);

        color.r = uSwordsmanBlock_t.swordsman[0].attackStrength;
        color.g = uSwordsmanBlock_t.swordsman[1].attackStrength;
        color.b = uSwordsmanBlock_t.swordsman[2].attackStrength;
        color.a = uSwordsmanBlock_t.swordsman[3].attackStrength;

        fragColor = vec4(color);

    }

}

)";


int main() {
    // Initialize SDL
    assert(SDL_Init(SDL_INIT_VIDEO) >= 0);

    // Set OpenGL context attributes
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    // Full screen desktop display mode
    SDL_DisplayMode fullscreen_desktopm;
    assert(SDL_GetDesktopDisplayMode(0, &fullscreen_desktopm) == 0);

    // Create SDL window
    SDL_Window* sdl_window = SDL_CreateWindow("OpenGL Circle Example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, fullscreen_desktopm.w, fullscreen_desktopm.h, SDL_WINDOW_OPENGL);
    assert(sdl_window);

    SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP);

    // Create OpenGL context
    SDL_GLContext gl_context = SDL_GL_CreateContext(sdl_window);
    assert(gl_context);

    // Initialize GLEW
    assert(glewInit() == GLEW_OK);

    // Compile and link shaders
    GLuint vertexShader_id = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader_id, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader_id);

    GLuint fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader_id);

    GLuint shaderProgram_id = glCreateProgram();
    glAttachShader(shaderProgram_id, vertexShader_id);
    glAttachShader(shaderProgram_id, fragmentShader_id);
    glLinkProgram(shaderProgram_id);


    // Check shader compilation and linking errors
    GLint success;
    GLchar infoLog[512];

    glGetShaderiv(vertexShader_id, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader_id, 512, nullptr, infoLog);
        std::cerr << "Vertex shader compilation failed: " << infoLog << std::endl;
        return -1;
    }

    glGetShaderiv(fragmentShader_id, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader_id, 512, nullptr, infoLog);
        std::cerr << "Fragment shader compilation failed: " << infoLog << std::endl;
        return -1;
    }

    glGetProgramiv(shaderProgram_id, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram_id, 512, nullptr, infoLog);
        std::cerr << "Shader program linking failed: " << infoLog << std::endl;
        return -1;
    }


    // Illustrating dynamic drawing using uniform block
    // This would would be done with dynamic allocation...
    int numSwordsman = 4;

    // Example Swordsman struct objects
    Swordsman_t sm1 = {50.0,  0.0f};
    Swordsman_t sm2 = {25.0,  1.0f};
    Swordsman_t sm3 = {100.0, 0.0f};
    Swordsman_t sm4 = {15.0,  1.0f};

    Swordsman_t Swordsmen[numSwordsman] = {sm1, sm2, sm3, sm4};

    GLuint uSwordsmanBlock = glGetUniformBlockIndex(shaderProgram_id, "uSwordsmanBlock");
    assert(uSwordsmanBlock != GL_INVALID_INDEX);
    
    GLuint swordsmanuniformblock_id = 0;
    glUniformBlockBinding(shaderProgram_id, uSwordsmanBlock, swordsmanuniformblock_id);

    // Size of swords man block for use with subdata bind
    GLint blocksize = numSwordsman * sizeof(Swordsman_t);

    GLuint ubo_swordsman_id;
    glGenBuffers(1, &ubo_swordsman_id);
    glBindBuffer(GL_UNIFORM_BUFFER, ubo_swordsman_id);
    glBufferData(GL_UNIFORM_BUFFER, blocksize, nullptr, GL_DYNAMIC_DRAW); 
    glBindBufferBase(GL_UNIFORM_BUFFER, uSwordsmanBlock, ubo_swordsman_id);

    glDeleteShader(vertexShader_id);
    glDeleteShader(fragmentShader_id);

    // Vertex Buffer Object (VBO) and Vertex Array Object (VAO) setup
    GLuint vbo_id, vao_id;
    glGenVertexArrays(1, &vao_id);
    glBindVertexArray(vao_id);

    glGenBuffers(1, &vbo_id);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_id);
    glBufferData(GL_ARRAY_BUFFER, sizeof(drawing_plane_vertices), drawing_plane_vertices, GL_STATIC_DRAW);

    // Position attribute
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // Unbind vao_id
    glBindVertexArray(0);

    // Main loop
    bool quit = false;
    SDL_Event event;

    GLint resolutionLoc = glGetUniformLocation(shaderProgram_id, "uresolution");
    GLint itimeLoc = glGetUniformLocation(shaderProgram_id, "iTime");

    float outerlcount = 0;

    while (!quit) 
    {

        Uint64 tickstartc = SDL_GetPerformanceCounter();

        while (SDL_PollEvent(&event)) 
        {
            if (event.type == SDL_QUIT) 
            {
                
                quit = true;
                break;
            
            } else if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {

                quit = true;
                break;

            }
        }

        // Clear the buffer
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Use the shader program
        glUseProgram(shaderProgram_id);
    
        glUniform2f(resolutionLoc, static_cast<float>(fullscreen_desktopm.w), static_cast<float>(fullscreen_desktopm.h));
        glUniform1f(itimeLoc, outerlcount);

        // dynamic uniforms
        GLint blockindex = glGetUniformLocation(shaderProgram_id, "uSwordsmanBlock");
        glUniformBlockBinding(shaderProgram_id, blockindex, swordsmanuniformblock_id);

        glBindBuffer(GL_ARRAY_BUFFER, ubo_swordsman_id); // bound to correct buffer for this next operation.
        glBufferSubData(GL_UNIFORM_BUFFER, 0, numSwordsman * sizeof(Swordsman_t), Swordsmen);
        glUniform1i(glGetUniformLocation(shaderProgram_id, "uSwordsmanBlock.num"), numSwordsman);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        

        // Drawing plane vertices 
        glBindVertexArray(vao_id);
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

        // Swap the front and back buffers
        SDL_GL_SwapWindow(sdl_window);

        // Unbind vao_id and shader program
        // glBindBufferBase(GL_UNIFORM_BUFFER, 0);
        glBindVertexArray(0);
        glUseProgram(0);

        Uint64 tendc = SDL_GetPerformanceCounter();
        float t_elapsedms = (tendc - tickstartc) / static_cast<float>(SDL_GetPerformanceCounter()) * 1000.0f;

        SDL_Delay(floor(5.0f - t_elapsedms));

        outerlcount += 0.1;

    }

    // Cleanup
    glDeleteVertexArrays(1, &vao_id);
    glDeleteBuffers(1, &vbo_id);

    SDL_GL_DeleteContext(gl_context);
    SDL_DestroyWindow(sdl_window);
    SDL_Quit();

    return 0;
}


I have tried using the uniform block like this:

struct Swordsman
{

    float health;
    float attackStrength;

};

layout (std140) uniform uSwordsmanBlock
{


    Swordsman swordsman[MAX_NUM_SWORDSMAN];
    int num;

};

and indexing into uSwordsmanBlock.swordsman[i] but it causes a compilation error. I have also tried GLint uNumSwordsman = glGetUniformLocation(shaderProgram_id, "uSwordsmanBlock.num"); assert(uNumSwordsman != GL_INVALID_INDEX); and ChatGPT is telling me that this "isn't a problem" and that my code "works fine."


Solution

  • It seems you signed up to std140 layout without realizing what it means for arrays: (OpenGL 4.5 sec 7.6.2.2 "Standard Uniform Block Layout")

    If the member is an array of scalars or vectors, the base alignment and array stride are set to match the base alignment of a single array element, according to rules (1), (2), and (3), and rounded up to the base alignment of a vec4. The array may have padding at the end; the base offset of the member following the array is rounded up to the next multiple of the base alignment.

    In other words, OpenGL requires that every Swordsman must be aligned at 16 bytes, whereas C++ aligns it at 8 bytes. (because the struct is exactly that big)

    Cribbing from this answer:

    struct alignas(16) Swordsman
    {
        float health;
        float attackStrength;
    };
    

    Or, much easier, switch the UBO layout to the std430 layout, which

    works like std140, except with a few optimizations in the alignment and strides for arrays and structs of scalars and vector elements (except for vec3 elements, which remain unchanged from std140). Specifically, they are no longer rounded up to a multiple of 16 bytes. So an array of floats will match with a C++ array of floats.