7/14

4.3. Sending data to GPU

Input data of shaders (typically relative to meshes such as vertices and their attributes) have to be loaded onto the GPU memory in order to be efficiently access during the drawing step. This memory transfert from CPU RAM memory to GPU memory is a costly operation when applied on large quantity of data and should be performed in an initial setup stage before starting the animation loop.

GPU data are designed to work on so called buffers, i.e. contiguous array of values. These buffers are called VBO - Vertex Buffer Object. They typically contain vertex coordinates, but also their attributes such as colors, texture coordinates, etc. More generally, these buffers contains data interpreted as per vertex data which are received as input parameters in the shaders.

OpenGL functions set these buffers using raw C-type pointer types [1].

Vectors and matrices in GLSL are computed using single-floating point numbers. This corresponds to common float values in C++, and is designated as GLfloat to ensure coherent type on various architectures. Buffer of data should therefore be classically be designed as contiguous array of single-precision floating point numbers (and not double-precision).

As data of VBO can have various meaning (and be stored in different ways in the buffer), the relation between the placement of data within the buffer, and the input variables used in the shader has to be explicitly provided by the user. These relations are set using the so called VAO - Vertex Array Object. VAO store the relation between the placement and memory organization with respect to the index of input variable (indicated by its layout in the shader).

Three general steps can be identified to send data to GPU

  1. Setup your data in C++ code as contiguous array of values.

  2. Send data to GPU and store its identifiant as VBO.

  3. Setup the relation between VBO memory organization and variables in the shader using VAO.

Application

The three previous steps are illustrated in the case of the single triangle by the following code

    // ************************************************* //
    //           2 - Sending data to GPU                 //
    // ************************************************* //

    std::cout<<"*** Setup Data ***"<<std::endl;

    // 2.1 Setup contiguous array of floating point value
    // ******************************************* //
    //     Here the coordinates of the vertices position
    const std::vector<GLfloat> position = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f
    };

    // 2.2 Create VBO - Send data to GPU
    // ******************************************* //

    GLuint vbo = 0;
    // Create an empty VBO identifiant
    glGenBuffers(1, &vbo);
    // Setup the current VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Send data to GPU: Fill the currently designated VBO with the buffer of data passed as parameter
    glBufferData(GL_ARRAY_BUFFER, position.size()*sizeof(GLfloat), &position[0], GL_STATIC_DRAW );
    // Good practice to set the current VBO to 0 (=disable VBO) after its use
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 2.3 Create VAO - Relation between VBO organization and input variables of shaders
    // ******************************************* //
    GLuint vao = 0;
    // Create an empty VBO identifiant
    glGenVertexArrays(1,&vao);
    // Setup the current VAO
    glBindVertexArray(vao);
    // Indicate the VBO we will refer to in the next lines
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Activate the use of the variable at index layout=0 in the shader
    glEnableVertexAttribArray( 0 );
    // Define the memory model of this VBO: here contiguous triplet of floating values (x y z) at index layout=0 in the shader
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr );
    // As a good practice, disable VBO and VAO after their use
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

More explanations

glBufferData(GL_ARRAY_BUFFER, position.size()*sizeof(GLfloat), &position[0], GL_STATIC_DRAW );
  • glBufferData(GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage) is the function that actually perform the data transfert.

    • target: Set to GL_ARRAY_BUFFER when sending per vertex attributes

    • size: Size in bytes of the complete buffer. In this case, we compute it as the number of elements of the vector times the size of an element (here GLfloat)

    • data: Adress of the first element of the buffer (in C style). The syntax &vector[0] is a standard way to obtain this address and convert C++ vector data to C pointer.

    • usage: Parameter used to optimize the memory organization on GPU if the data will be modified or not during the drawing. We use GL_STATIC_DRAW to indicate that data are not going to be modified at run time.

glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr );
  • glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid* pointer)

    • index: Specifies the index of the variable in the shader. Here 0 to set the variable position at layout=0.

    • size: Number of components to be read from the buffer. Here 3 as the buffer contains x, y, and z component.

    • type: Enumeration indicating the type of the variable in the buffer. Here GLfloat values are indicated by the enumeration GL_FLOAT (see OpenGL types conventions).

    • normalized: Indicate weather the values should be normalized when received by the shader.

    • stride: Used to indicate the gap in bytes between two consecutive values when buffers values are interleaved between multiple attributes. In our case, vertices coordinates are consecutively placed in the buffer, there is no stride.

    • pointer: Offset indicating the first component to start reading the values in the buffer. In our case, the first element of the buffer is also the first coordinate value, therefore the offset is 0 (nullptr) [2].

General remarks on OpenGL functions

Note the general way of OpenGL way of working with identifiants and handlers (glGen, glBind, etc) that you may encounter in all programs.

  1. First an identifiant/handler is created using some glGenXXX function

  2. Before being used, the handler should be activated as the current one using glBindXXX function.

  3. Once binded, some operation can be performed in relation of this buffer (data transfert, etc)

  4. Finally, it is a good practice to set up the current handler to 0 (glBindXXX(0)) after its use to avoid any unexpected use and detects bugs more easily.


1. Note that it is possible in C++ to conveniently store a buffer of data in std::vector
2. The use of a pointer instead of an unsigned integer to indicate a gap is related to the old way of using buffers in OpenGL using direct CPU pointers instead of VBOs