Мне нужна помощь с кодом, приведенным ниже, где я пытаюсь загрузить 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)
Действительно, я получил частичный рендеринг, указав dtype
моих данных. Я совершенно забыл это сделать после того, как перешёл на систему загрузки OBJ. Однако ключевое слово здесь является частичным. Я опубликую обновление.
@AlexeyS.Larionov Хотите оставить свой комментарий в качестве ответа? Ваше предложение на самом деле является решением. :) Спасибо!
Но почему это был частичный рендеринг?
Ну, под частичным рендерингом я на самом деле имею в виду, что некоторые части сетки (сетей) не видны. Это связано с тем, что, вероятно, не существует ни одного экспортера и загрузчика OBJ, который соответствовал бы фактическому формату. XD Не говоря уже о том, что мне еще предстоит найти экспортера, который, например, правильно триангулирует сетку и не портит ее. Я пробовал pywavefront
, pymeshlab
, несколько небольших загрузчиков проектов, и лучший на данный момент — impasse
и ObjLoader
в TreeJS (но он недоступен в Python). Так что все данные есть, просто нормали неверные.
Возможно, проверьте свои типы данных. Например. когда ты устанавливаешь
indices = np.array(mesh.faces)
этот массив должен иметь dtype == np.uint16
, потому что позже вы выполняете рендеринг с помощью
glDrawElements(..., GL_UNSIGNED_SHORT, ...)
Итак, если в вашем массиве индексов есть dtype == np.uint32
, вам нужно рисовать с помощью GL_UNSIGNED_INT
.
Проверьте также типы данных для данных вершин, это должно быть np.float32
, потому что вы определяете GL_FLOAT
при вызове glVertexAttribPointer
Возможно, проверьте свои типы данных. Например. когда вы это делаете
indices = np.array(mesh.faces)
, этот массив должен бытьnp.uint16
, потому что позже вы выполняете рендеринг сGL_UNSIGNED_SHORT
вglDrawElements
. Если этоnp.uint32
, то вам нуженGL_UNSIGNED_INT
. Проверьте то же самое для данных вершин, это должно бытьnp.float32
, потому что вы определяетеGL_FLOAT
вglVertexAttribPointer