Best way to store and draw multiple objects

Hi guys!

I have to draw several objects (300/400 max) whose each one consists in:

  • 4 vertices (coordinates)
  • a 512*512 pixels texture

To display all my objects, currently, I first build 5 VBOs per object (vertices, colors, index, uv_coordinates and texture). Then, in my display function, I loop on all objects to draw them but, due to multiple calls to some functions such as bindBuffer() or glVertexAttribPointer(), I don’t believe it is the optimal approach.

I am also wondering how to cleverly store such objects in memory (VAO, VBO,…).

My shaders:


vertex_code = """
    #version 440 core

    uniform float scale;
    uniform mat4 Model;
    uniform mat4 View;
    uniform mat4 Projection;    
    
    in vec2 position;
    in vec4 color;
    
    out vec4 v_color;
    
    void main()
    {
        gl_Position =  Projection * View * Model * vec4(scale*position, 0.0, 1.0);
        v_color = color;
    } """
    
    

fragment_code = """
    #version 440 core

    in vec4 v_color;
    
    
    void main()
    {
        gl_FragColor = v_color;
    } """
    

The initialization of my objects:

# Generate buffers for each tile
buffer_vertices = gl.glGenBuffers(row_grid.size)
buffer_colors = gl.glGenBuffers(row_grid.size)
buffer_index= gl.glGenBuffers(row_grid.size)
# To simplify, textures are not used here
buffer_uv_coordinates = gl.glGenBuffers(row_grid.size)
buffer_texture = gl.glGenBuffers(row_grid.size)
   
    
for iTile, (iRow, iCol) in enumerate(zip(np.nditer(row_grid), np.nditer(col_grid))):
    
    print('iRow=%i, iCol=%i'%(iRow, iCol))
    # compute current object bounding box
    tileBeamStart = iRow*blockSize*2**wtfLevel
    tileBeamEnd = (iRow*blockSize+511)*2**wtfLevel+1    
    tileSampleStart = iCol*blockSize*2**wtfLevel
    tileSampleEnd = (iCol*blockSize+511)*2**wtfLevel+1 
    
    # array filled with its 4 corners
    mat_vertices = np.array([[tileSampleStart, tileBeamStart],
                             [tileSampleStart, tileBeamEnd],
                             [tileSampleEnd, tileBeamStart],
                             [tileSampleEnd, tileBeamEnd]])
      
     # Delaunay triangulation to generate index                  
    delaunay_tri = Delaunay(mat_vertices)
    
    # read current tile to further be used as texture
    tile = ...
    
    vertices = np.ascontiguousarray(mat_vertices.flatten(), dtype=np.float32)
    index = np.ascontiguousarray(delaunay_tri.simplices.flatten(), dtype=np.uint32)
    
    # generate random colors
    tmp = np.random.rand(mat_vertices.shape[0],3)
    #tmp = np.ones(shape=(mat_vertices.shape[0],3))
    tmp2 = np.ones(shape=(mat_vertices.shape[0],1))
    tmp = np.hstack((tmp, tmp2 ))

    colors = np.ascontiguousarray(tmp.flatten().astype(np.float32))
    
    
    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_vertices[iTile])
    gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_DYNAMIC_DRAW)
    
    
    # COLORS
    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_colors[iTile])
    gl.glBufferData(gl.GL_ARRAY_BUFFER, colors.nbytes, colors, gl.GL_DYNAMIC_DRAW)
    
    # INDEX ARRAY
    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, buffer_index[iTile])
    gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, index.nbytes, index, gl.GL_STATIC_DRAW)
    

And my display function:

def display():
    
    gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
    #gl.glDrawArrays(gl.GL_TRIANGLES, 0, 12)
    
    gl.glEnable(gl.GL_DEPTH_TEST)
    gl.glDepthFunc(gl.GL_LESS)
    
    # loop on all objects
    for iTile in range(row_grid.size):        
            
        gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, buffer_index[iTile])  
        
        
        loc = gl.glGetAttribLocation(program, "position")
        gl.glEnableVertexAttribArray(loc)
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_vertices[iTile])
        gl.glVertexAttribPointer(loc, 2, gl.GL_FLOAT, False, 0,  ctypes.c_void_p(0))
        
        loc = gl.glGetAttribLocation(program, "color")
        gl.glEnableVertexAttribArray(loc)
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_colors[iTile])
        gl.glVertexAttribPointer(loc, 4, gl.GL_FLOAT, False, 0,  ctypes.c_void_p(0))
        

        gl.glDrawElements(gl.GL_TRIANGLES, len(index), gl.GL_UNSIGNED_INT, ctypes.c_void_p(0))
        
        
        
    glut.glutSwapBuffers()

[QUOTE=neon29;1280699]Hi guys!

I have to draw several objects (300/400 max) whose each one consists in:

  • 4 vertices (coordinates)
  • a 512*512 pixels texture

To display all my objects, currently, I first build 5 VBOs per object (vertices, colors, index, uv_coordinates and texture). Then, in my display function, I loop on all objects to draw them but, due to multiple calls to some functions such as bindBuffer() or glVertexAttribPointer(), I don’t believe it is the optimal approach.
[/QUOTE]
It certainly isn’t.

The optimal approach is to store all of the data in a single set of VBOs (or even a single VBO) and render everything with a single glDrawElements() call.

OpenGL doesn’t have “objects”, it has vertices and primitives. IMHO, it’s better to think of this as “stuff” rather than “things”. You have a set of geometry which needs to be drawn, and you should optimise the storage of the data based upon what OpenGL wants, not what that data “means”. If your program needs to deal with discrete “objects”, it should do so by keeping track of which portions of OpenGL’s data corresponds to the object, not by trying to make the data “belong” to the object. Everything you learn in Object-Oriented Design 101 is wrong here.

A common reason for splitting up rendering into multiple glDraw* calls is to be able to change uniforms, texture, etc between calls. There are various solutions to that, but that’s not actually happening in your code.

Whether to use a VBO per attribute or a single VBO for all data depends upon several factors, the main one being how often the attributes are changed. E.g. if you update the vertex positions regularly but the other attributes never change, then it may be worth using one VBO for the vertex positions and another for the other attributes. If you use a single VBO for several attributes, there’s a choice between interleaving the attributes (so that all attributes for a single vertex are adjacent) or storing each attribute in a separate region. The former is more efficient in terms of the GPU reading the data, the latter is more efficient when the CPU needs to update a specific attribute while leaving the others unchanged.

The position of my vertices (i.e. the model matrix) does not change. The only thing which changes is the lookat matrix. However, depending on the z coordinate of my “eye”, I can decide (later in the project) to load another resolution levels to be used as texture.

Do you have an example of how to store all my vertices in a given VBOs, all my textures in another VBO,… (I use numpy) ?

[QUOTE=neon29;1280713]
Do you have an example of how to store all my vertices in a given VBOs, all my textures in another VBO,… (I use numpy) ?[/QUOTE]
First, all of the glGenBuffers() calls should have an argument of one, i.e. buffer_vertices and buffer_colors should each be a single buffer name rather than a list.

Then, declare vertices, colors and index as arrays before the loop, e.g.


vertices = np.empty(row_grid.shape + (4,2), dtype=np.float32)
colors  = np.empty(row_grid.shape + (4,4), dtype=np.float32)
index = np.empty(row_grid.shape + (6,), dtype=np.uint32)

Inside the loop, copy the data into those arrays, e.g.


vertices[iRow,iCol,...] = mat_vertices
indexi[Row,iCol,...] = delaunay_tri.simplices + 6*iTile
...
colors[iRow,iCol,...] = tmp

After the loop, upload those arrays to the buffers.

Except … you can probably get rid of the loop altogether, just using row_grid and col_grid in place of iRow and iCol so that all of the variables are arrays. Also, using Delaunay triangulation on a rectangle is overkill. The indices are just [0,1,2,2,1,3].

Ok, I will try this ASAP. However, in my display() function, if the loop disappears, how could I draw my tiles? How many calls do I need (glVertexAttribPointer for example)?

If, now I add a texture for each (iRow, iCol) stuff, can I also define a single VBO to store such textures and link each texture to the
vertices?

For example, there is no problem for position, uv coordinates or colors:

loc = gl.glGetAttribLocation(program, "position")
gl.glEnableVertexAttribArray(loc)
gl.glBindBuffer(gl.GL_TEXTURE_2D, buffer_vertices)
gl.glVertexAttribPointer(loc, 2, gl.GL_FLOAT, False, 2*np.float32.nbytes,  ctypes.c_void_p(0))

loc = gl.glGetAttribLocation(program, "texcoord")
gl.glEnableVertexAttribArray(loc)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_uv_coordinates)
gl.glVertexAttribPointer(loc, 2, gl.GL_FLOAT, False, 2*np.float32.nbytes,  ctypes.c_void_p(0))

Thus, could I add:

 in sampler2D texSonar; 

in my fragment shader ?

and access it like this:

loc = gl.glGetAttribLocation(program, "texSonar")
gl.glEnableVertexAttribArray(loc)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buffer_texture)
// another function similar to glVertexAttribPointer but for a texture 

A last question:

Why did you write:

index = np.empty(row_grid.shape + (6,), dtype=np.uint32)
index[Row,iCol,...] = delaunay_tri.simplices + 6*iTile


?
I would have say:

index = np.empty(row_grid.shape + (4,), dtype=np.uint32)
index[Row,iCol,...] = delaunay_tri.simplices + 4*iTile

as I have 4 vertices per (iRow, iCol) unless OpenGL implicitly define 3 vertices per triangle.

[QUOTE=neon29;1280723]Ok, I will try this ASAP. However, in my display() function, if the loop disappears, how could I draw my tiles? How many calls do I need (glVertexAttribPointer for example)?
[/QUOTE]
Just remove the loop and the array subscripts. You now have two attribute buffers and one index buffer, which between them contain all of the tiles.

First, check whether you can fit all of the texture data into a single texture (possibly a 2D array texture if it won’t fit into a single 2D texture, although current-generation hardware typically allows 16384x16384 textures). If it can, you just need to adjust the texture coordinates for reach tile. Otherwise, you can bind multiple textures and access them via an array of sampler objects in the fragment shader. If that still isn’t enough, you’ll need to break up the drawing into multiple calls so that you can change textures in between.

[QUOTE=neon29;1280723]
Why did you write:

index = np.empty(row_grid.shape + (6,), dtype=np.uint32)
index[Row,iCol,...] = delaunay_tri.simplices + 6*iTile


?
I would have say:

index = np.empty(row_grid.shape + (4,), dtype=np.uint32)
index[Row,iCol,...] = delaunay_tri.simplices + 4*iTile

as I have 4 vertices per (iRow, iCol) unless OpenGL implicitly define 3 vertices per triangle.[/QUOTE]
You are correct, it should be 4* (each tile is 6 indices but only 4 distinct vertices).