1

Basically, I'm trying to extract a depth map (by this I mean a matrix with z corresponding to z-coordinates for vertices in GL.glVertex3dv(vertex) call - obviously, interpolated for plane pixels) after rendering a model (script is loading the model from file with path specified as the first command line argument).

There are several questions which come to mind:

  • why glReadPixels call returns a numpy array with shape (width, shape), instead of (height, width)?

  • why it returns some trash, not connected to the rendered model?

  • is there an easy way to get z-coordinates on OpenGL legacy code with PyOpenGL framework?

  • is it correct that maximum I can get here is some array with range [0; 1], basically is some fraction between zNear and zFar (and normalized by glReadPixels, for whatever reason)?

The code itself:

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()

The model file I personally used for testing: bunny.obj The snippet's result is here

Andrey K.
  • 35
  • 1
  • 7
  • 1
    At orthographic projection the Z-coorinate is linaer mapped to the depth value (see [this question](https://stackoverflow.com/questions/7777913/how-to-render-depth-linearly-in-modern-opengl-with-gl-fragcoord-z-in-fragment-sh/45710371#45710371)). The depth value is in range [0.0, 1.0]. So the Z-coordinate in view space is `z = -near - depth*(far-near)`. In your case it is `z = 1 - 2*depth;`. Because you have not set any model view matrix (`GL_MODELVIEW`), the view space is equal the world space. – Rabbid76 Jan 10 '18 at 15:01
  • Also possibly silly issue: it looks like you aren't calling `glClear` anywhere to clear the color and depth buffers. `glClearColor` and `glClearDepth` just set the values for what clearing should do, but do not actually perform clearing of the buffers themselves. This might explain the "trash" you are seeing. – CodeSurgeon Jan 12 '18 at 14:45
  • 1
    @CodeSurgeon This didn't help. Updated the snippet accordingly. Also added snippet's output. – Andrey K. Jan 12 '18 at 16:20

1 Answers1

1

Running your code gave me some "Invalid Operation Error: 1282" messages for the glReadPixels call. Instead, here is a simple demo I just wrote that shows how to obtain the color and the depth buffer from OpenGL for a rendered triangle. What I do here is bind an FBO (framebuffer object) to the screen with the desired texture attachments (for receiving the color and depth data). I then read out the data from the GPU using glGetTexImage. Using textures might not be the fastest approach, but this is pretty simple and should work nicely. Let me know if anything in this is unclear and I will elaborate on it.

from OpenGL.GL import *
from OpenGL.GLUT import *
import numpy as np
import sys

def draw_scene():
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_TRIANGLES)
    glColor3f(1, 0, 0)
    glVertex3f(-1, -1, 0)
    glColor3f(0, 1, 0)
    glVertex3f(0, 1, 0)
    glColor3f(0, 0, 1)
    glVertex3f(1, -1, 0)
    glEnd()

def draw_texture():
    global color_texture
    glColor3f(1, 1, 1)
    glBindTexture(GL_TEXTURE_2D, color_texture)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    glTexCoord2f(0, 0)
    glVertex3f(-1, -1, 0)
    glTexCoord2f(0, 1)
    glVertex3f(-1, 1, 0)
    glTexCoord2f(1, 1)
    glVertex3f(1, 1, 0)
    glTexCoord2f(1, 0)
    glVertex3f(1, -1, 0)
    glEnd()
    glBindTexture(GL_TEXTURE_2D, 0)

def update_display():
    global fbo, color_texture, depth_texture

    #Render the scene to an offscreen FBO
    glBindFramebuffer(GL_FRAMEBUFFER, fbo)
    draw_scene()
    glBindFramebuffer(GL_FRAMEBUFFER, 0)

    #Then render the results of the color texture attached to the FBO to the screen
    draw_texture()

    #Obtain the color data in a numpy array
    glBindTexture(GL_TEXTURE_2D, color_texture)
    color_str = glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE)
    glBindTexture(GL_TEXTURE_2D, 0)
    color_data = np.fromstring(color_str, dtype=np.uint8)

    #Obtain the depth data in a numpy array
    glBindTexture(GL_TEXTURE_2D, depth_texture)
    depth_str = glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, GL_FLOAT)
    glBindTexture(GL_TEXTURE_2D, 0)
    depth_data = np.fromstring(depth_str, dtype=np.float32)
    print(np.min(depth_data), np.max(depth_data))#This is just to show the normalized range of depth values obtained

    glutSwapBuffers()

width, height = 800, 600
fbo = None
color_texture = None
depth_texture = None

if __name__ == '__main__':
    glutInit([])
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE)
    glutInitWindowSize(width, height)
    glutInitWindowPosition(0, 0)
    glutCreateWindow("Triangle Test")

    glEnable(GL_TEXTURE_2D)#not needed if using shaders...
    glEnable(GL_DEPTH_TEST)

    color_texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, color_texture)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
    glBindTexture(GL_TEXTURE_2D, 0)

    depth_texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, depth_texture)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, None)
    glBindTexture(GL_TEXTURE_2D, 0)

    fbo = glGenFramebuffers(1)
    glBindFramebuffer(GL_FRAMEBUFFER, fbo)
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_texture, 0)
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texture, 0)
    glBindFramebuffer(GL_FRAMEBUFFER, 0)

    glutDisplayFunc(update_display)
    glutIdleFunc(glutPostRedisplay)
    glutMainLoop()
CodeSurgeon
  • 2,435
  • 2
  • 15
  • 36
  • I think your answer is not applicable: 1) drawing is 2D only, no depth range present. 2) did you try to render model in such a way? Sorry, didn't have time to check your approach thoroughly yet (and apply it to my task). Another question: what platform did you run my snippet on (that it failed with runtime error)? – Andrey K. Jan 17 '18 at 10:37
  • @AndreyKravtsun 1) Even though the drawing is 2D, it still should be relevant. If you look at the contents of the `depth_data`, you can see both the background depth and the triangle depth. I just chose a 2D triangle since that was a simple shape to render in a very minimal amount of lines. Feel free to change the z-indices of the triangle and see the values in the depth texture. 2) Model should work just fine too, just have not installed the `Assimp` library on my laptop. 3) Using LinuxMint 18.3 on my laptop. – CodeSurgeon Jan 17 '18 at 20:56
  • 1
    But one of the questions remains, how does that happen that pixel reading functions return wrong dimensions (width and height are swapped?). For example, in your code snippet `depth_str = glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, GL_FLOAT)` depth_str.shape == (800, 600), according to debug output. – Andrey K. Feb 10 '18 at 10:20
  • @AndreyKravtsun That is a great point! From the manual docstring associated with pyopengl's glGetTexImage (see [here](http://pyopengl.sourceforge.net/documentation/manual-3.0/glGetTexImage.html)), it appears that it treats `GL_UNSIGNED_BYTE` data differently (by leaving it alone as a byte array) from other kinds of data (which it processes behind the scenes into a numpy array). I suspect that they "flip" the data so that indexing by `depth_str[x_coordinate, y_coordinate]` is "intuitive". The `OpenGL/images.py` source code on the pyopengl github is not easy to grok. – CodeSurgeon Feb 10 '18 at 14:44