Search code examples
pythonpython-3.xopenglrenderingpyopengl

PyOpenGL - update triangles array when some of the scene elements need to disappear


I have a scene that I need to render quickly. So I successfully generate the vertices of the triangles as numpy, upload to GPU and render it very fast. So far so good.

The problem starts when some of the elements in the scene need to disappear from the scene. What I do is traverse the entire array and filter out the vertices the scene elements that no longer need to be drawn, upload it again to GPU and then render it. This obviously takes a few seconds which is unacceptable. I'm looking for a quick way to modify the array and redraw the scene.

This a simplified version of the code I use: (Would also appreciate any comments about the code itself)

from OpenGL.GL import *
from OpenGL.GLU import *
import numpy as np
import ctypes

vertex_dtype = [("position", np.float32, 3), ("color", np.float32, 4)]
vertex_array_object = None
vertex_buffer = None
shader_program = .......

def upload_to_gpu(vertices_np):
    global vertex_array_object, vertex_buffer
    if vertex_array_object is None:
        vertex_array_object = glGenVertexArrays(1)
    if vertex_buffer is None:
        vertex_buffer = glGenBuffers(1)
    glBindVertexArray(vertex_array_object)
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer)
    glBufferData(GL_ARRAY_BUFFER, vertices_np.nbytes, vertices_np, GL_STATIC_DRAW)
    stride = vertices_np.strides[0]
    offset = ctypes.c_void_p(0)
    position_location = glGetAttribLocation(shader_program, 'a_position')
    glEnableVertexAttribArray(position_location)
    glVertexAttribPointer(position_location, 3, GL_FLOAT, False, stride, offset)

    offset = ctypes.c_void_p(vertices_np.dtype["position"].itemsize)
    color_location = glGetAttribLocation(shader_program, "a_color")
    glEnableVertexAttribArray(color_location)
    glVertexAttribPointer(color_location, 4, GL_FLOAT, False, stride, offset)

    glBindVertexArray(0)
    glBindBuffer(GL_ARRAY_BUFFER, 0)

def create_np_array(all_vertices):
    vertices_np = np.zeros(len(all_vertices), dtype=vertex_dtype)
    cur_idx = 0
    for vertex in all_vertices:        
        if ....should_ignore_triangle....:
            continue
        vertices_np[cur_idx]["position"] = [vertex[0], vertex[1], vertex[2]]
        vertices_np[cur_idx]["color"] = ......
        cur_idx += 1

    vertices_np = vertices_np[:cur_idx]
    return vertices_np

def draw_scene(vertices_np):
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glUseProgram(shader_program)
    # Set view and projection matrix    
    glBindVertexArray(vertex_array_object)
    glDrawArrays(GL_TRIANGLES, 0, vertices_np.shape[0])

def main():
    all_vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0], ......]   # long list of ~million vertices
    vertices_np = create_np_array(all_vertices)
    upload_to_gpu(vertices_np)

    # Now we are ready to start rendering
    draw_scene(vertices_np) # works fine

    # Now something changes and we need to regenerate the vertices_np and upload it again
    vertices_np = create_np_array(all_vertices)   # <-- THIS TAKES TOO MUCH TIME
    upload_to_gpu(vertices_np)
    draw_scene(vertices_np) # works fine

Solution

  • If you only want to omit vertices, but you don't want to add new ones, then I recommend just to draw the subset of vertices you want. Don't change any data on the GPU or recreate any vertex array.

    Create an array of tuples, which contains the index ranges of the vertices, to be drawn. For this find the first vertex which has to be omitted and append a "range" tuple to the tuple list. Find the next vertex which should be drawn and repeat the process till the end of the array:

    def draw_np_array(all_vertices):
    
        idx_list = []
        cur_start_idx = 0
        cur_end_idx = 0
        for vertex in all_vertices:
    
            if ....should_ignore_triangle....:
                if cur_end_idx > cur_start_idx:
                    idx_list.append( (cur_start_idx, cur_end_idx) );
                cur_end_idx += 1
                cur_start_idx = cur_end_idx
                continue
    
            cur_end_idx += 1
    
        if cur_end_idx > cur_start_idx:
            idx_list.append( (cur_start_idx, cur_end_idx) );
    
        return idx_list
    

    Draw the scene with an "intelligent" draw call:

    def draw_scene(idx_list):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glUseProgram(shader_program)
        # Set view and projection matrix
        glBindVertexArray(vertex_array_object)
    
        for idx_range in idx_list:
            start = idx_range[0]
            count = idx_range[1]-idx_range[0]
            glDrawArrays(GL_TRIANGLES, start, count)
    

    You can further improve this by using glMultiDrawArrays, which can draw multiple ranges of an array by one draw call, instead of drawing the arrays in a loop.