Headless Rendered OpenGL Scenes
Headless Rendered OpenGL Scenes

Download:

Running the renderer

Output:
Output of the headless renderer. The stanford rabbit.

Recent versions of PyOpenGL (>3.0.2) have support for software rendering using OSMesa. This makes rendering with OpenGL on a headless node much easier!

The dependencies for these examples (you may need to download pyopengl 3.0.2 directly):

$ sudo apt-get install python-opengl python-opencv libosmesa6

I plan to use this system for a Light Field simulator, and decided to load in and render .obj files. First up: loading in obj files:

from OpenGL import GL as gl

def MTL(filename):
  '''
  source: pygame.org/wiki/OBJFileLoader
  '''
  contents = {}
  mtl = None
  for line in open(filename, "r"):
    if line.startswith('#'): continue
    values = line.split()
    if not values: continue
    if values[0] == 'newmtl':
      mtl = contents[values[1]] = {}
    elif mtl is None:
      raise ValueError, "mtl file doesn't start with newmtl stmt"
    elif values[0] == 'map_d':
      pass
    elif values[0] == 'map_Ks':
      pass
    elif values[0] == 'map_Bump':
      pass
    elif values[0] == 'map_Kd':
      # load the texture referred to by this declaration
      '''
      mtl[values[0]] = values[1]
      surf = pygame.image.load(mtl['map_Kd'])
      image = pygame.image.tostring(surf, 'RGBA', 1)
      ix, iy = surf.get_rect().size
      texid = mtl['texture_Kd'] = gl.glGenTextures(1)
      gl.glBindTexture(GL_TEXTURE_2D, texid)
      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
        GL_LINEAR)
      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
        GL_LINEAR)
      gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ix, iy, 0, GL_RGBA,
        GL_UNSIGNED_BYTE, image)
      '''
    else:
      mtl[values[0]] = map(float, values[1:])
  return contents

class OBJ:
  '''
  source: pygame.org/wiki/OBJFileLoader
  '''
  def __init__(self, filename, swapyz=False):
    """Loads a Wavefront OBJ file. """
    self.vertices = []
    self.normals = []
    self.texcoords = []
    self.faces = []

    material = None
    for line in open(filename, "r"):
      if line.startswith('#'): continue
      values = line.split()
      if not values: continue
      if values[0] == 'v':
        v = map(float, values[1:4])
        if swapyz:
          v = v[0], v[2], v[1]
        self.vertices.append(v)
      elif values[0] == 'vn':
        v = map(float, values[1:4])
        if swapyz:
          v = v[0], v[2], v[1]
        self.normals.append(v)
      elif values[0] == 'vt':
        self.texcoords.append(map(float, values[1:3]))
      elif values[0] in ('usemtl', 'usemat'):
        material = values[1]
      elif values[0] == 'mtllib':
        self.mtl = MTL(values[1])
      elif values[0] == 'f':
        face = []
        texcoords = []
        norms = []
        for v in values[1:]:
          w = v.split('/')
          face.append(int(w[0]))
          if len(w) >= 2 and len(w[1]) > 0:
            texcoords.append(int(w[1]))
          else:
            texcoords.append(0)
          if len(w) >= 3 and len(w[2]) > 0:
            norms.append(int(w[2]))
          else:
            norms.append(0)
        self.faces.append((face, norms, texcoords, material))

    self.gl_list = gl.glGenLists(1)
    gl.glNewList(self.gl_list, gl.GL_COMPILE)
    gl.glEnable(gl.GL_TEXTURE_2D)
    gl.glFrontFace(gl.GL_CCW)
    for face in self.faces:
      vertices, normals, texture_coords, material = face
      if material:
        mtl = self.mtl[material]
        if 'texture_Kd' in mtl: # use diffuse texmap
          gl.glBindTexture(GL_TEXTURE_2D, mtl['texture_Kd'])
        else: # just use diffuse colour
          gl.glColor(*mtl['Kd'])

      gl.glBegin(gl.GL_POLYGON)
      for i in range(len(vertices)):
        if normals[i] > 0:
          gl.glNormal3fv(self.normals[normals[i] - 1])
        if texture_coords[i] > 0:
          gl.glTexCoord2fv(self.texcoords[texture_coords[i] - 1])
        gl.glVertex3fv(self.vertices[vertices[i] - 1])

      gl.glEnd()
    gl.glDisable(gl.GL_TEXTURE_2D)
    gl.glEndList()

model = OBJ('/path/to/scene.obj')

Great! Now let’s make sure our OpenGL calls from Python are good (this will only work on a machine with graphics enabled, if your machine is headless just skip to the next code listing):

from OpenGL import GL as gl, GLU as glu, GLUT as glut
import sys
'''
Setup etc. repurposed from: http://code.activestate.com/recipes/325391-open-a-glut-window-and-draw-a-sphere-using-pythono/
'''
name = 'obj_viewer'
model = None
width, height = 2816/4, 2112/4

def main():
  global model

  if len(sys.argv) < 4:
    raise BaseException("usage: %s cam_x cam_y cam_z file.obj" % sys.argv[0])
  glut.glutInit(sys.argv)
  glut.glutInitDisplayMode(glut.GLUT_DOUBLE | glut.GLUT_RGB | glut.GLUT_DEPTH)
  glut.glutInitWindowSize(width, height)
  glut.glutCreateWindow(name)
  model = OBJ(sys.argv[4])
  gl.glClearColor(0.,0.,0.,1.)
  gl.glShadeModel(gl.GL_SMOOTH)
  gl.glEnable(gl.GL_CULL_FACE)
  gl.glEnable(gl.GL_DEPTH_TEST)
  gl.glEnable(gl.GL_LIGHTING)
  lightZeroPosition = [40.,4.,40.,1.]
  lightZeroColor = [0.8,1.0,0.8,1.0] # green tinged
  gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, lightZeroPosition)
  gl.glLightfv(gl.GL_LIGHT0, gl.GL_DIFFUSE, lightZeroColor)
  gl.glLightf(gl.GL_LIGHT0, gl.GL_CONSTANT_ATTENUATION, 0.1)
  gl.glLightf(gl.GL_LIGHT0, gl.GL_LINEAR_ATTENUATION, 0.05)
  gl.glEnable(gl.GL_LIGHT0)
  glut.glutDisplayFunc(display)
  gl.glMatrixMode(gl.GL_PROJECTION)
  fov_y = 55.
  aspect = float(width / height)
  near = 1.
  far = 300.
  glu.gluPerspective(fov_y, aspect, near, far)
  gl.glTranslate(
      float(sys.argv[1]),
      float(sys.argv[2]),
      float(sys.argv[3])
    )
  gl.glMatrixMode(gl.GL_MODELVIEW)
  glu.gluLookAt(
      0, 0, 0,
      0, 0, 0,
      0, 1, 0
    )
  gl.glPushMatrix()
  glut.glutMainLoop()
  return

def display():
  gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
  gl.glPushMatrix()
  gl.glLoadIdentity()
  gl.glCallList(model.gl_list)
  gl.glPopMatrix()
  glut.glutSwapBuffers()
  return

if __name__ == '__main__': main()

Note: for this last example you will need to run the code with the environment variable PYOPENGL_PLATFORM set to "osmesa":

$ PYOPENGL_PLATFORM=osmesa python

...and finally we can render without needing a graphics card:

import cv, numpy
from OpenGL import GL as gl, GLUT as glut, GLU as glu, arrays, platform
import sys
'''
Evoke like:
$ PYOPENGL_PLATFORM=osmesa python this.py

Matti Kariluoma matti@kariluo.ma Sep 2013
'''
width, height = 2816/2, 2112/2
model = None

def setup(model_obj_filename):
  global model
  ctx = platform.OSMesaCreateContext(gl.GL_RGBA, None) # requires PYOPENGL_PLATFORM=osmesa
  buf = arrays.GLubyteArray.zeros((height, width, 4))
  p = arrays.ArrayDatatype.dataPointer(buf)
  assert(platform.OSMesaMakeCurrent(ctx, p, gl.GL_UNSIGNED_BYTE, width, height))
  assert(platform.CurrentContextIsValid())
  model = OBJ(model_obj_filename)
  color = (
        int('b0', 16) / 255.0,
        int('f0', 16) / 255.0,
        int('f0', 16) / 255.0,
        1.0
      )
  gl.glClearColor(*color)
  gl.glShadeModel(gl.GL_SMOOTH)
  gl.glEnable(gl.GL_CULL_FACE)
  gl.glEnable(gl.GL_DEPTH_TEST)
  gl.glEnable(gl.GL_LIGHTING)
  return ctx, buf

def teardown(ctx):
  platform.OSMesaDestroyContext(ctx)

def lights():
  lightZeroPosition = [10., 4., 10., 1.]
  lightZeroColor = [1.0, 1.0, 0.8, 1.0] # yellow tinged
  gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, lightZeroPosition)
  gl.glLightfv(gl.GL_LIGHT0, gl.GL_DIFFUSE, lightZeroColor)
  gl.glLightf(gl.GL_LIGHT0, gl.GL_CONSTANT_ATTENUATION, 0.1)
  gl.glLightf(gl.GL_LIGHT0, gl.GL_LINEAR_ATTENUATION, 0.05)
  gl.glEnable(gl.GL_LIGHT0)

def camera(dx=0, dy=0, dz=0):
  gl.glMatrixMode(gl.GL_PROJECTION)
  gl.glLoadIdentity()
  fov_y = 55.
  aspect = float(width / height)
  near = 1.
  far = 300.
  glu.gluPerspective(fov_y, aspect, near, far)
  gl.glTranslate(0. + dx, 0. + dy, 0. + dz)
  gl.glMatrixMode(gl.GL_MODELVIEW)
  glu.gluLookAt(
      0,0,0,
      0,0,0,
      0,1,0
    )

def render():
  def scene():
    gl.glPushMatrix()
    gl.glLoadIdentity()
    gl.glCallList(model.gl_list)
    gl.glPopMatrix()

  gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
  scene()
  gl.glFinish()

def save_render(filename):
  ia = gl.glReadPixels(0, 0, width, height, gl.GL_BGR, gl.GL_UNSIGNED_BYTE)
  im = cv.CreateImageHeader((width, height), cv.IPL_DEPTH_8U, 3)
  cv.SetData(im, ia)
  cv.Flip(im)
  cv.SaveImage(filename, im)

def main():
  if len(sys.argv) < 4:
    raise BaseException("usage: %s cam_x cam_y cam_z file.obj" % sys.argv[0])
  gl_context, offscreen_buffer = setup(sys.argv[4])
  lights()
  camera(
      float(sys.argv[1]),
      float(sys.argv[2]),
      float(sys.argv[3])
    )
  save_render("snap.jpg")
  teardown(gl_context)

if __name__ == '__main__': main()

Leave a Comment?

Send me an email, then I'll place our discussion on this page (with your permission).


Return | About/Contact