openglglslshader-storage-bufferbuffer-objects

Dynamic-length arrays as Shader Storage Buffer Objects


Let's say I have a dynamic number of "balls" which I want to access in my OpenGL shaders. In C++ the data might be like this:

struct Ball
{
    glm::vec3 position;
    glm:vec3 colour;
    float size;
};

std::vector<Ball> all_balls;

If I want to iterate over all_balls in my fragment shader, I believe I will need a Shader Storage Buffer Object.

This documentation touches on arrays, but is notably incomplete.

I assume I can send the data to the buffer like this

glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, all_balls.size() * sizeof(Ball), &(all_balls[0]), usage);

In GLSL, how do I specify that the buffer is an array, and how does my shader know the size of this array?


Solution

  • When working an array with a length that is not a compile time constant, one can declare a member of a SSBO Interface Block to be of undetermined length.

    Assuming that there exists a GLSL structure that fits to the C++ ball struct, the code can look somehow like this:

    struct GLSLBall {...};
    
    layout(std430, binding = 0) buffer BallBuffer
    {
        GLSLBall ball_data[];
    }
    
    

    You can than iterator over all elements like this:

    for (int i = 0; i < ball_data.length(); ++i)
    {
        GLSLBall currentBall = ball_data[i];
    }
    

    When the number of elements changes very often, then I suggest not to resize/reallocate the SSBO every time, but to reserve a large enough buffer once and pass the number of elements actually used to the shader. This can either be an independent uniform variable (uniform uint ballCount;), or you can pack it into the SSBO itself like this:

    struct GLSLBall {...};
    
    layout(std430, binding = 0) buffer BallBuffer
    {
        uint ball_length;
        GLSLBall ball_data[];
    }
    
    

    Then you can allocate the memory only once:

    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
    glBufferData(GL_SHADER_STORAGE_BUFFER, ENOUGH_MEMORY_FOR_ALL_CASES, null, usage);
    

    and upload the data every time the content changes like this:

    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(unsigned int), (unsigned int)all_balls.size());
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, sizeof(unsigned int), all_balls.size() * sizeof(Ball), &(all_balls[0]));
    

    The glsl loop is then similar to

    for (int i = 0; i < BallBuffer.length; ++i)
    {
        GLSLBall currentBall = ball_data[i];
        ...
    }
    

    Please note, that you current C++ struct layout might cause some troubles with alignment due to the use of vec3. You might want to read Should I ever use a vec3 inside of a uniform buffer or shader storage buffer object? (thanks to Rabbid76 for the hint)