基于PyOpenGL读取深度缓冲区的技术问题咨询
PyOpenGL渲染模型提取深度图的技术问题
我尝试通过命令行指定路径加载模型并渲染后提取深度图(即每个像素对应GL.glVertex3dv(vertex)中顶点z坐标的插值结果),但遇到以下问题:
- 为何
glReadPixels调用返回的numpy数组形状为(width, height)而非(height, width)? - 为何返回的是与渲染模型无关的无效数据?
- 在PyOpenGL的OpenGL旧版代码中,是否存在简便方法获取z坐标?
- 获取到的数组范围是否确实为
[0;1],即zNear与zFar之间的归一化值?
测试代码
import sys import argparse import pyassimp from pyassimp.postprocess import aiProcess_JoinIdenticalVertices, aiProcess_Triangulate import numpy as np import matplotlib.pyplot as plt from collections import namedtuple from OpenGL import GL, GLUT Mesh = namedtuple('Mesh', ('vertices', 'faces')) def load_mesh(filename): scene = pyassimp.load(filename, processing=aiProcess_JoinIdenticalVertices | aiProcess_Triangulate) mesh = scene.mMeshes[0].contents def get_vector_array(vector): return [vector.x, vector.y, vector.z] def get_face_array(face): return [face.mIndices[i] for i in xrange(face.mNumIndices)] vertices = np.array([get_vector_array(mesh.mVertices[i]) for i in xrange(mesh.mNumVertices)]) faces = np.array([get_face_array(mesh.mFaces[i]) for i in xrange(mesh.mNumFaces)]) pyassimp.release(scene) return Mesh(vertices, faces) def load_ortho(): GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() GL.glOrtho(-1, 1, -1, 1, -1, 1) GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() mesh = None width, height = 1920, 1080 def draw_mesh(): global mesh, width, height GL.glClearColor(0, 0, 0, 0) GL.glClearDepth(0.5) GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) GL.glDepthMask(GL.GL_TRUE) load_ortho() for face in mesh.faces: GL.glBegin(GL.GL_POLYGON) for vertex in mesh.vertices[face]: GL.glVertex3dv(vertex) GL.glEnd() GLUT.glutSwapBuffers() d = GL.glReadPixels(0, 0, width, height, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT) plt.imshow(d) plt.show() def reshape(w, h): GL.glViewport(0, 0, w, h) GLUT.glutDisplayFunc(draw_mesh) GLUT.glutPostRedisplay() def init(width, height): GLUT.glutInit(sys.argv) GLUT.glutInitDisplayMode(GLUT.GLUT_RGBA | GLUT.GLUT_DOUBLE) GLUT.glutInitWindowSize(width, height) GLUT.glutInitWindowPosition(0, 0) GLUT.glutCreateWindow("test") # GLUT.glutDisplayFunc(draw_mesh) # GLUT.glutIdleFunc(draw_mesh) GLUT.glutReshapeFunc(reshape) GLUT.glutIdleFunc(GLUT.glutPostRedisplay) def keyPressed(self, *args): if args[0] == '\033': sys.exit() GLUT.glutKeyboardFunc(keyPressed) if __name__ == '__main__': parser = argparse.ArgumentParser("Test on extracting depth while rendering a model with PyOpenGL") parser.add_argument("model", type=str) args = parser.parse_args() global mesh mesh = load_mesh(args.model) init(width, height) draw_mesh()
逐个问题解答
问题1:glReadPixels返回数组形状为(width, height)而非(height, width)
这事儿其实和OpenGL的像素存储规则以及numpy的数组映射逻辑有关:
- OpenGL的像素坐标原点在窗口左下角,数据是按行从左到右、从下到上存储的;
- PyOpenGL的
glReadPixels会直接按照你传入的width和height顺序返回数组,所以默认是(width, height); - 但numpy和matplotlib习惯用
(height, width)的维度顺序,而且imshow默认以左上角为原点,和OpenGL正好相反。
你可以这样修正:
# 读取后调整数组形状 d = GL.glReadPixels(0, 0, width, height, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT).reshape(height, width) # 显示时指定原点为左下角,匹配OpenGL坐标 plt.imshow(d, origin='lower')
问题2:返回与模型无关的无效数据
你的代码踩了几个OpenGL渲染的常见坑,咱们一个个说:
- 没开启深度测试:你只设置了
glDepthMask(GL.GL_TRUE),但这只是允许写入深度缓冲区,必须启用深度测试才能让GPU正确更新深度值。在初始化时加一行:GL.glEnable(GL.GL_DEPTH_TEST) - 渲染流程异常:你的
init函数里没设置displayFunc,反而在reshape里设置,这会导致渲染时机混乱;而且你直接调用draw_mesh,没启动GLUT的主循环,渲染根本没正常执行。必须在init后调用GLUT.glutMainLoop()。 - 模型被裁剪:bunny.obj的坐标范围通常很小(大概在[-0.1,0.1]),但你的
glOrtho设置的是[-1,1],虽然理论上能放下,但如果模型没居中,可能部分被裁;另外你设置的glClearDepth(0.5)也不对,深度缓冲区默认清除值应该是1.0,设置成0.5会导致初始深度值异常。 - 缓冲区交换时机:
glutSwapBuffers是交换前后缓冲区,你应该在交换后读取深度数据,确保读取的是刚渲染好的内容。
问题3:旧版PyOpenGL中获取z坐标的简便方法
在固定功能管线(旧版OpenGL)里,最简便的还是读取深度缓冲区,修正前面的问题后就能正常工作。其他方法要么复杂要么效率低:
- 自己写软件光栅化计算每个像素的z值:太麻烦,GPU本来就擅长这个,没必要重复造轮子;
- 切换到可编程管线写着色器:如果你的代码必须用旧版固定管线,这个方法不适用。
所以优先把glReadPixels的使用修正到位就好。
问题4:数组范围是否为[0;1]的归一化值
是的!当你用GL.GL_DEPTH_COMPONENT和GL.GL_FLOAT读取时,返回的值是归一化到[0,1]范围的,对应你glOrtho里设置的zNear和zFar。
不过要注意:
- 正交投影下,深度值和实际z坐标是线性关系,公式是:
你的代码里z_actual = zNear + d * (zFar - zNear)zNear=-1,zFar=1,所以z_actual = -1 + d*2,反过来d=(z_actual+1)/2。 - 如果是透视投影,深度值是非线性的,这时候需要额外的转换才能得到实际z坐标。
修改后的完整测试代码
import sys import argparse import pyassimp from pyassimp.postprocess import aiProcess_JoinIdenticalVertices, aiProcess_Triangulate import numpy as np import matplotlib.pyplot as plt from collections import namedtuple from OpenGL import GL, GLUT Mesh = namedtuple('Mesh', ('vertices', 'faces')) def load_mesh(filename): scene = pyassimp.load(filename, processing=aiProcess_JoinIdenticalVertices | aiProcess_Triangulate) mesh = scene.mMeshes[0].contents def get_vector_array(vector): return [vector.x, vector.y, vector.z] def get_face_array(face): return [face.mIndices[i] for i in xrange(face.mNumIndices)] vertices = np.array([get_vector_array(mesh.mVertices[i]) for i in xrange(mesh.mNumVertices)]) faces = np.array([get_face_array(mesh.mFaces[i]) for i in xrange(mesh.mNumFaces)]) pyassimp.release(scene) return Mesh(vertices, faces) def load_ortho(): GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() # 调整正交投影范围,适配bunny模型的坐标大小 GL.glOrtho(-0.2, 0.2, -0.2, 0.2, -0.2, 0.2) GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() mesh = None width, height = 1920, 1080 def draw_mesh(): global mesh, width, height GL.glClearColor(0, 0, 0, 0) GL.glClearDepth(1.0) # 恢复默认的深度缓冲区清除值 GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) GL.glDepthMask(GL.GL_TRUE) load_ortho() # 缩放模型到合适大小,确保能在视口中显示 GL.glScalef(0.1, 0.1, 0.1) for face in mesh.faces: GL.glBegin(GL.GL_POLYGON) for vertex in mesh.vertices[face]: GL.glVertex3dv(vertex) GL.glEnd() GLUT.glutSwapBuffers() # 读取并调整深度数据形状 d = GL.glReadPixels(0, 0, width, height, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT) d = d.reshape(height, width) # 正确显示深度图 plt.imshow(d, cmap='gray', origin='lower') plt.title('Extracted Depth Map') plt.show() def reshape(w, h): global width, height width, height = w, h GL.glViewport(0, 0, w, h) GLUT.glutPostRedisplay() def init(width, height): GLUT.glutInit(sys.argv) # 添加GLUT_DEPTH启用深度缓冲区 GLUT.glutInitDisplayMode(GLUT.GLUT_RGBA | GLUT.GLUT_DOUBLE | GLUT.GLUT_DEPTH) GLUT.glutInitWindowSize(width, height) GLUT.glutInitWindowPosition(0, 0) GLUT.glutCreateWindow("Depth Extraction Test") # 启用深度测试 GL.glEnable(GL.GL_DEPTH_TEST) GLUT.glutDisplayFunc(draw_mesh) GLUT.glutReshapeFunc(reshape) GLUT.glutIdleFunc(GLUT.glutPostRedisplay) def keyPressed(self, *args): if args[0] == '\033': sys.exit() GLUT.glutKeyboardFunc(keyPressed) if __name__ == '__main__': parser = argparse.ArgumentParser("Test on extracting depth while rendering a model with PyOpenGL") parser.add_argument("model", type=str) args = parser.parse_args() mesh = load_mesh(args.model) init(width, height) # 启动GLUT主循环,这是之前代码缺失的关键部分 GLUT.glutMainLoop()
内容的提问来源于stack exchange,提问作者Andrey K.
相关产品推荐
相关产品推荐

