Управление несколькими экземплярами OpenGL VBO и VAO в Python

Мне нужна помощь с кодом, приведенным ниже, где я пытаюсь загрузить OBJ с помощью Impasse (вилка PyAssimp), который может содержать несколько сеток. Чтобы сделать это и помочь себе с отладкой, я сохраняю все данные в словарной структуре, что позволяет мне легко получать данные о вершинах, цветах и ​​лицах.

Оба фрагмента кода взяты из серии руководств Марио Розаско по PyOpenGL, причем первый из них был сильно модифицирован для поддержки PyGame, ImGUI (я удалил все окна, так как это бесполезно для моего вопроса), загрузки из файла OBJ и поддержки нескольких сеток. Исходный код использует один VAO (с соответствующим VBO, который смешивает данные вершин и цветов) и IBO, и он работает. По запросу могу предоставить.

Как только я начал добавлять несколько объектов буфера и массива, я попал в затруднительное положение и теперь не могу визуализировать ничего, кроме черного цвета фона моей сцены.

Структура данных, которую я использую, довольно проста — словарь с 4 ключами:

  • vbos — список словарей, каждый из которых представляет вершины сетки.
  • cbos — список словарей, каждый из которых представляет цвета вершин сетки.
  • ibos — список словарей, каждый из которых представляет индексы всех граней сетки.
  • vaos - список ссылок ВАО

Первые три имеют одинаковую структуру элементов, а именно:

{
  'buff_id' : <BUFFER OBJECT REFERENCE>,
  'data' : {
    'values' : <BUFFER OBJECT DATA (flattened)>,
    'count' : <NUMBER OF ELEMENTS IN BUFFER OBJECT DATA>,
    'stride' : <DIMENSION OF A SINGLE DATUM IN A BUFFER OBJECT DATA>
   }
}

Я предполагаю, что OBJ с несколькими сетками (я использую Blender для создания своих файлов) имеет отдельные данные вершин и т. д. для каждой сетки и что каждая сетка определяется как компонент g. Кроме того, при экспорте я использую триангуляцию (поэтому stride на данный момент не особо нужен), поэтому все мои лица являются примитивами.

Я почти на 100% уверен, что где-то пропустил привязку/отвязку, но найти место не могу. Или, возможно, в моем коде есть другая, более фундаментальная проблема.

import sys
from pathlib import Path
import shutil

import numpy as np

from OpenGL.GLU import *
from OpenGL.GL import *

import pygame

import glm
from utils import *
from math import tan, cos, sin, sqrt
from ctypes import c_void_p

from imgui.integrations.pygame import PygameRenderer
import imgui

import impasse
from impasse.constants import ProcessingPreset, MaterialPropertyKey
from impasse.structs import Scene, Camera, Node
from impasse.helper import get_bounding_box

scene_box = None

def loadObjs(f_path: Path):
    if not f_path:
        raise TypeError('f_path not of type Path')
    if not f_path.exists():
        raise ValueError('f_path not a valid filesystem path')
    if not f_path.is_file():
        raise ValueError('f_path not a file')
    if not f_path.name.endswith('.obj'):
        raise ValueError('f_path not an OBJ file')
    
    mtl_pref = 'usemtl '
    mltlib_found = False
    mtl_names = []
    obj_parent_dir = f_path.parent.absolute()
    obj_raw = None
    with open(f_path, 'r') as f_obj:
        obj_raw = f_obj.readlines()
        for line in obj_raw:
            if line == '#':
                continue
            elif line.startswith('mtllib'):
                mltlib_found = True
            elif mtl_pref in line:
                mtl_name = line.replace(mtl_pref, '')
                print('Found material "{}" in OBJ file'.format(mtl_name))
                mtl_names.append(mtl_name)

    args = {}
    args['processing'] = postprocess = ProcessingPreset.TargetRealtime_Fast
    scene = impasse.load(str(f_path), **args).copy_mutable()
    scene_box = (bb_min, bb_max) = get_bounding_box(scene)

    print(len(scene.meshes))

    return scene

def createBuffers(scene: impasse.core.Scene):
    vbos = []
    cbos = []
    ibos = []
    vaos = []

    for mesh in scene.meshes:
        print('Processing mesh "{}"'.format(mesh.name))
        color_rnd = np.array([*np.random.uniform(0.0, 1.0, 3), 1.0], dtype='float32')

        vbo = glGenBuffers(1)
        vertices = np.array(mesh.vertices)
        vbos.append({
            'buff_id' : vbo,
            'data' : {
                'values' : vertices.flatten(),
                'count' : len(vertices),
                'stride' : len(vertices[0])
            }
        })
        glBindBuffer(GL_ARRAY_BUFFER, vbos[-1]['buff_id'])
        glBufferData(
            # PyOpenGL allows for the omission of the size parameter
            GL_ARRAY_BUFFER,
            vertices,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ARRAY_BUFFER, 0)

        cbo = glGenBuffers(1)
        print('Random color: {}'.format(color_rnd))
        colors = np.full((len(vertices), 4), color_rnd)
        cbos.append({
            'buff_id' : cbo,
            'data' : {
                'values' : colors.flatten(),
                'count' : len(colors),
                'stride' : len(colors[0])
            }
        })
        glBindBuffer(GL_ARRAY_BUFFER, cbos[-1]['buff_id'])
        glBufferData(
            GL_ARRAY_BUFFER,
            colors,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ARRAY_BUFFER, 0)
        
        ibo = glGenBuffers(1)
        indices = np.array(mesh.faces)
        ibos.append({
            'buff_id' : ibo,
            'data' : {
                'values' : indices,
                'count' : len(indices),
                'stride' : len(indices[0])
            }
        })
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibos[-1]['buff_id'])
        glBufferData(
            GL_ELEMENT_ARRAY_BUFFER,
            indices,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)

    # Generate the VAOs. Technically, for same VBO data, a single VAO would suffice
    for mesh_idx in range(len(ibos)):
        vao = glGenVertexArrays(1)
        vaos.append(vao)

        glBindVertexArray(vaos[-1])
        
        vertex_dim = vbos[mesh_idx]['data']['stride']
        print('Mesh vertex dim: {}'.format(vertex_dim))
        glBindBuffer(GL_ARRAY_BUFFER, vbos[mesh_idx]['buff_id'])
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, vertex_dim, GL_FLOAT, GL_FALSE, 0, None)

        color_dim = cbos[mesh_idx]['data']['stride']
        print('Mesh color dim: {}'.format(color_dim))
        glBindBuffer(GL_ARRAY_BUFFER, cbos[mesh_idx]['buff_id'])
        glEnableVertexAttribArray(1)
        glVertexAttribPointer(1, color_dim, GL_FLOAT, GL_FALSE, 0, None)
        
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibos[mesh_idx]['buff_id'])
        
        glBindVertexArray(0)

    return {
        'vbos' : vbos,
        'cbos' : cbos,
        'ibos' : ibos,
        'vaos' : vaos
        }

wireframe_enabled = False
transform_selected = 0

# Helper function to calculate the frustum scale. 
# Accepts a field of view (in degrees) and returns the scale factor
def calcFrustumScale(fFovDeg):
    degToRad = 3.14159 * 2.0 / 360.0
    fFovRad = fFovDeg * degToRad
    return 1.0 / tan(fFovRad / 2.0)

# Global variable to represent the compiled shader program, written in GLSL
program = None

# Global variables to store the location of the shader's uniform variables
modelToCameraMatrixUnif = None
cameraToClipMatrixUnif = None

# Global display variables
cameraToClipMatrix = np.zeros((4,4), dtype='float32')
fFrustumScale = calcFrustumScale(45.0)

# Set up the list of shaders, and call functions to compile them
def initializeProgram():
    shaderList = []
    
    shaderList.append(loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"))
    shaderList.append(loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"))
    
    global program 
    program = createProgram(shaderList)
    
    for shader in shaderList:
        glDeleteShader(shader)
    
    global modelToCameraMatrixUnif, cameraToClipMatrixUnif
    modelToCameraMatrixUnif = glGetUniformLocation(program, "modelToCameraMatrix")
    cameraToClipMatrixUnif = glGetUniformLocation(program, "cameraToClipMatrix")
    
    fzNear = 1.0
    fzFar = 61.0
    
    global cameraToClipMatrix
    # Note that this and the transformation matrix below are both
    # ROW-MAJOR ordered. Thus, it is necessary to pass a transpose
    # of the matrix to the glUniform assignment function.
    cameraToClipMatrix[0][0] = fFrustumScale
    cameraToClipMatrix[1][1] = fFrustumScale
    cameraToClipMatrix[2][2] = (fzFar + fzNear) / (fzNear - fzFar)
    cameraToClipMatrix[2][3] = -1.0
    cameraToClipMatrix[3][2] = (2 * fzFar * fzNear) / (fzNear - fzFar)
    
    glUseProgram(program)
    glUniformMatrix4fv(cameraToClipMatrixUnif, 1, GL_FALSE, cameraToClipMatrix.transpose())
    glUseProgram(0)


def initializeBuffers():
    global vbo, cbo, ibo

    vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(
        # PyOpenGL allows for the omission of the size parameter
        GL_ARRAY_BUFFER,
        obj['vertices'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ARRAY_BUFFER, 0)

    cbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, cbo)
    glBufferData(
        GL_ARRAY_BUFFER,
        obj['colors'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    
    ibo = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
    glBufferData(
        GL_ELEMENT_ARRAY_BUFFER,
        obj['faces'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
    

# Helper functions to return various types of transformation arrays
def calcLerpFactor(fElapsedTime, fLoopDuration):
    fValue = (fElapsedTime % fLoopDuration) / fLoopDuration
    if fValue > 0.5:
        fValue = 1.0 - fValue
    return fValue * 2.0
    

def computeAngleRad(fElapsedTime, fLoopDuration):
    fScale = 3.14159 * 2.0 / fLoopDuration
    fCurrTimeThroughLoop = fElapsedTime % fLoopDuration
    return fCurrTimeThroughLoop * fScale


def rotateY(fElapsedTime, **mouse):
    fAngRad = computeAngleRad(fElapsedTime, 2.0)
    fCos = cos(fAngRad)
    fSin = sin(fAngRad)
    
    newTransform = np.identity(4, dtype='float32')
    newTransform[0][0] = fCos
    newTransform[2][0] = fSin
    newTransform[0][2] = -fSin
    newTransform[2][2] = fCos
    # offset 
    newTransform[0][3] = 0.0 #-5.0
    newTransform[1][3] = 0.0 #5.0
    newTransform[2][3] = mouse['wheel']
    return newTransform

# A list of the helper offset functions.
# Note that this does not require a structure def in python.
# Each function is written to return the complete transform matrix.
g_instanceList = [
    rotateY,
]


# Initialize the OpenGL environment
def init(w, h):

    initializeProgram()

    glEnable(GL_CULL_FACE)
    glCullFace(GL_BACK)
    glFrontFace(GL_CW)
    
    glEnable(GL_DEPTH_TEST)
    glDepthMask(GL_TRUE)
    glDepthFunc(GL_LEQUAL)
    glDepthRange(0.0, 1.0)

    glMatrixMode(GL_PROJECTION)
    print('Scene bounding box:', scene_box)
    gluPerspective(45, (w/h), 0.1, 500)
    #glTranslatef(0, 0, -100)

   
def render(time, imgui_impl, mouse, mem, buffers):
    global wireframe_enabled, transform_selected

    #print(mouse['wheel'])
    
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    if wireframe_enabled:
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
    else:
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

    glUseProgram(program)
    
    fElapsedTime = pygame.time.get_ticks() / 1000.0
    transformMatrix = g_instanceList[transform_selected](fElapsedTime, **mouse)

    glUniformMatrix4fv(modelToCameraMatrixUnif, 1, GL_FALSE, transformMatrix.transpose())
    #glDrawElements(GL_TRIANGLES, len(obj['faces']), GL_UNSIGNED_SHORT, None)
    for vao_idx, vao in enumerate(buffers['vaos']):
        print('Rendering VAO {}'.format(vao_idx))
        print('SHAPE VBO', buffers['vbos'][vao_idx]['data']['values'].shape)
        print('SHAPE CBO', buffers['cbos'][vao_idx]['data']['values'].shape)
        print('SHAPE IBO', buffers['ibos'][vao_idx]['data']['values'].shape)

        print('''Address:
    VBO:\t{}
    CBO:\t{}
    IBO:\t{}
    VAO:\t{}
'''.format(id(buffers['vbos'][vao_idx]), id(buffers['cbos'][vao_idx]), id(buffers['ibos'][vao_idx]), id(vao)))

        glBindVertexArray(vao)
        index_dim = buffers['ibos'][vao_idx]['data']['stride']
        index_count = buffers['ibos'][vao_idx]['data']['count']
        glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_SHORT, None)
        glBindVertexArray(0)

    glUseProgram(0)

    imgui.new_frame()
    # Draw windows
    imgui.end_frame()
    imgui.render()
    imgui_impl.render(imgui.get_draw_data())

            
# Called whenever the window's size changes (including once when the program starts)
def reshape(w, h):
    global cameraToClipMatrix
    cameraToClipMatrix[0][0] = fFrustumScale * (h / float(w))
    cameraToClipMatrix[1][1] = fFrustumScale

    glUseProgram(program)
    glUniformMatrix4fv(cameraToClipMatrixUnif, 1, GL_FALSE, cameraToClipMatrix.transpose())
    glUseProgram(0)
    
    glViewport(0, 0, w, h)
    
def main():
    width = 800
    height = 800

    display = (width, height)

    pygame.init()
    pygame.display.set_caption('OpenGL VAO with pygame')
    pygame.display.set_mode(display, pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 4)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 1)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE)

    imgui.create_context()
    impl = PygameRenderer()
    io = imgui.get_io()
    #io.set_WantCaptureMouse(True)
    io.display_size = width, height

    #scene = loadObjs(Path('assets/sample0.obj'))
    scene = loadObjs(Path('assets/cubes.obj'))
    #scene = loadObjs(Path('assets/shapes.obj'))
    #scene = loadObjs(Path('assets/wooden_watch_tower/wooden watch tower2.obj'))
    buffers = createBuffers(scene)

    init(width, height)

    wheel_factor = 0.3
    mouse = {
        'pressed' : False,
        'motion' : {
            'curr' : np.array([0, 0]),
            'prev' : np.array([0, 0])
        },
        'pos' : {
            'curr' : np.array([0, 0]),
            'prev' : np.array([0, 0])
        },
        'wheel' : -10
    }
    
    clock = pygame.time.Clock()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type ==  pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
                pygame.quit()
                quit()
            impl.process_event(event)

            if event.type == pygame.MOUSEMOTION:
                if mouse['pressed']:
                    #glRotatef(event.rel[1], 1, 0, 0)
                    #glRotatef(event.rel[0], 0, 1, 0)
                    mouse['motion']['curr'] = [event.rel[1], event.rel[0]]
                    mouse['pos']['curr'] = event.pos
            if event.type == pygame.MOUSEWHEEL:
                mouse['wheel'] += event.y * wheel_factor

        for event in pygame.mouse.get_pressed():
            mouse['pressed'] = pygame.mouse.get_pressed()[0] == 1

        render(time=clock, imgui_impl=impl, mouse=mouse, mem=None, buffers=buffers)

        mouse['motion']['prev'] = mouse['motion']['curr']
        mouse['pos']['prev'] = mouse['pos']['curr']

        pygame.display.flip()
        pygame.time.wait(10)

if __name__ == '__main__':
    main()

с модулем utils, определяемым как

from OpenGL.GLU import *
from OpenGL.GL import *
import os
import sys

# Function that creates and compiles shaders according to the given type (a GL enum value) and 
# shader program (a file containing a GLSL program).
def loadShader(shaderType, shaderFile):
    # check if file exists, get full path name
    strFilename = findFileOrThrow(shaderFile)
    shaderData = None
    with open(strFilename, 'r') as f:
        shaderData = f.read()
    
    shader = glCreateShader(shaderType)
    glShaderSource(shader, shaderData) # note that this is a simpler function call than in C
    
    # This shader compilation is more explicit than the one used in
    # framework.cpp, which relies on a glutil wrapper function.
    # This is made explicit here mainly to decrease dependence on pyOpenGL
    # utilities and wrappers, which docs caution may change in future versions.
    glCompileShader(shader)
    
    status = glGetShaderiv(shader, GL_COMPILE_STATUS)
    if status == GL_FALSE:
        # Note that getting the error log is much simpler in Python than in C/C++
        # and does not require explicit handling of the string buffer
        strInfoLog = glGetShaderInfoLog(shader)
        strShaderType = ""
        if shaderType is GL_VERTEX_SHADER:
            strShaderType = "vertex"
        elif shaderType is GL_GEOMETRY_SHADER:
            strShaderType = "geometry"
        elif shaderType is GL_FRAGMENT_SHADER:
            strShaderType = "fragment"
        
        print("Compilation failure for " + strShaderType + " shader:\n" + strInfoLog)
    
    return shader

# Function that accepts a list of shaders, compiles them, and returns a handle to the compiled program
def createProgram(shaderList):
    program = glCreateProgram()
    
    for shader in shaderList:
        glAttachShader(program, shader)
        
    glLinkProgram(program)
    
    status = glGetProgramiv(program, GL_LINK_STATUS)
    if status == GL_FALSE:
        # Note that getting the error log is much simpler in Python than in C/C++
        # and does not require explicit handling of the string buffer
        strInfoLog = glGetProgramInfoLog(program)
        print("Linker failure: \n" + strInfoLog)
        
    for shader in shaderList:
        glDetachShader(program, shader)
        
    return program
    
    
# Helper function to locate and open the target file (passed in as a string).
# Returns the full path to the file as a string.
def findFileOrThrow(strBasename):
    # Keep constant names in C-style convention, for readability
    # when comparing to C(/C++) code.
    LOCAL_FILE_DIR = "data" + os.sep
    GLOBAL_FILE_DIR = ".." + os.sep + "data" + os.sep
    
    strFilename = LOCAL_FILE_DIR + strBasename
    if os.path.isfile(strFilename):
        return strFilename
        
    strFilename = GLOBAL_FILE_DIR + strBasename
    if os.path.isfile(strFilename):
        return strFilename
        
    raise IOError('Could not find target file ' + strBasename)

Возможно, проверьте свои типы данных. Например. когда вы это делаете indices = np.array(mesh.faces), этот массив должен быть np.uint16, потому что позже вы выполняете рендеринг с GL_UNSIGNED_SHORT в glDrawElements. Если это np.uint32, то вам нужен GL_UNSIGNED_INT. Проверьте то же самое для данных вершин, это должно быть np.float32, потому что вы определяете GL_FLOAT в glVertexAttribPointer

Alexey S. Larionov 03.05.2024 10:16

Действительно, я получил частичный рендеринг, указав dtype моих данных. Я совершенно забыл это сделать после того, как перешёл на систему загрузки OBJ. Однако ключевое слово здесь является частичным. Я опубликую обновление.

rbaleksandar 03.05.2024 14:51

@AlexeyS.Larionov Хотите оставить свой комментарий в качестве ответа? Ваше предложение на самом деле является решением. :) Спасибо!

rbaleksandar 03.05.2024 15:20

Но почему это был частичный рендеринг?

Alexey S. Larionov 03.05.2024 15:37

Ну, под частичным рендерингом я на самом деле имею в виду, что некоторые части сетки (сетей) не видны. Это связано с тем, что, вероятно, не существует ни одного экспортера и загрузчика OBJ, который соответствовал бы фактическому формату. XD Не говоря уже о том, что мне еще предстоит найти экспортера, который, например, правильно триангулирует сетку и не портит ее. Я пробовал pywavefront, pymeshlab, несколько небольших загрузчиков проектов, и лучший на данный момент — impasse и ObjLoader в TreeJS (но он недоступен в Python). Так что все данные есть, просто нормали неверные.

rbaleksandar 04.05.2024 05:54
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
5
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Возможно, проверьте свои типы данных. Например. когда ты устанавливаешь

indices = np.array(mesh.faces)

этот массив должен иметь dtype == np.uint16, потому что позже вы выполняете рендеринг с помощью

glDrawElements(..., GL_UNSIGNED_SHORT, ...)

Итак, если в вашем массиве индексов есть dtype == np.uint32, вам нужно рисовать с помощью GL_UNSIGNED_INT.

Проверьте также типы данных для данных вершин, это должно быть np.float32, потому что вы определяете GL_FLOAT при вызове glVertexAttribPointer

Другие вопросы по теме