Sunday, February 05, 2012

Measuring performance of immediate mode sphere rendering

In my previous post we created a simple hello world OpenGL application. Now we move one step closer to my goal by actually rendering some spheres. And measuring rendering performance.


This program draws a bunch of spheres in immediate rendering mode. This is the slowest possible way of doing it. But it is also the simplest to program and requires the least indirection. If the performance of this approach would meet my needs, I would be happy to use it. But it does not meet my needs. I ideally want the following:
  1. Render over 10000 spheres at once
  2. In under 30 milliseconds
  3. With the most realistic rendering available

It is clear that immediate mode rendering, which is horribly old fashioned, will not come close to meeting these criteria. Let's see how far off we are from the goal.

In this experiment we vary the number of spheres shown, and the number of polygons used to define each sphere. The number of polygons is determined by the second and third arguments to glutSolidSphere(). I set both arguments to the same resolution value, either 10 (spheres look OK, but there are obvious artifacts when zoomed in) or 50 (almost as good as a tesselated sphere can look). And I also varied the number of spheres shown.

Here are the results (click to embiggen):


At each resolution the rendering time is proportional to the number of spheres drawn, as you might expect. At the lower resolution of 10 layers-per-dimension, the rendering time is about 70 microseconds per sphere. At the higher resolution of 50, the rendering time is about 315 microseconds per sphere. To meet my desired performance criteria, the performance would need to be about 0.3 microseconds (300 nanoseconds) per sphere. So we need about a 100-fold speed up from the higher quality rendering to satisfy my needs here.

But there are many approaches ahead. More next time.

The data:

method # spheres resolution frame rate
immediate 1 10 0.3 ms
immediate 1 50 0.4 ms
immediate 3 10 0.4 ms
immediate 3 50 1.1 ms
immediate 10 10 0.9 ms
immediate 10 50 3.4 ms
immediate 30 10 2.1 ms
immediate 30 50 8.9 ms
immediate 100 10 6.8 ms
immediate 100 50 29.0 ms
immediate 300 10 20.1 ms
immediate 300 50 92.3 ms
immediate 1000 10 69.8 ms
immediate 1000 50 315.1 ms

Here is the full source code for the program to run this test:


#!/usr/bin/python

# File sphere_test.py
# Investigate performance of various OpenGL sphere rendering techniques
# Requires python modules PySide and PyOpenGL

from PySide.QtGui import QMainWindow, QApplication
from PySide.QtOpenGL import QGLWidget
from PySide.QtCore import *
from PySide import QtCore
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
from random import random
from math import sin, cos
import sys


class SphereTestApp(QApplication):
"Simple application for testing OpenGL rendering"
def __init__(self):
QApplication.__init__(self, sys.argv)
self.setApplicationName("SphereTest")
self.main_window = QMainWindow()
self.gl_widget = SphereTestGLWidget()
self.main_window.setCentralWidget(self.gl_widget)
self.main_window.resize(1024, 768)
self.main_window.show()
sys.exit(self.exec_()) # Start Qt main loop


# This "override" technique is a strictly optional code-sanity-checking
# mechanism that I like to use.
def override(interface_class):
"""
Method to implement Java-like derived class method override annotation.
Courtesy of mkorpela's answer at
http://stackoverflow.com/questions/1167617/in-python-how-do-i-indicate-im-overriding-a-method
"""
def override(method):
assert(method.__name__ in dir(interface_class))
return method
return override


class SphereTestGLWidget(QGLWidget):
"Rectangular canvas for rendering spheres"
def __init__(self, parent = None):
QGLWidget.__init__(self, parent)
self.y_rot = 0.0
# units are nanometers
self.view_distance = 15.0
self.stopwatch = QTime()
self.frame_times = []
self.param_generator = enumerate_sphere_resolution_and_number()
(r, n) = self.param_generator.next()
self.set_number_of_spheres(n)
self.sphere_resolution = r

def set_number_of_spheres(self, n):
self.number_of_spheres = n
self.sphere_positions = SpherePositions(self.number_of_spheres)

def update_projection_matrix(self):
"update projection matrix, especially when aspect ratio changes"
glPushAttrib(GL_TRANSFORM_BIT) # remember current GL_MATRIX_MODE
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(40.0, # aperture angle in degrees
self.width()/float(self.height()), # aspect
self.view_distance/5.0, # near
self.view_distance * 3.0) # far
glPopAttrib() # restore GL_MATRIX_MODE

@override(QGLWidget)
def initializeGL(self):
"runs once, after OpenGL context is created"
glEnable(GL_DEPTH_TEST)
glClearColor(1,1,1,0) # white background
glShadeModel(GL_SMOOTH)
glEnable(GL_COLOR_MATERIAL)
glMaterialfv(GL_FRONT, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
glMaterialfv(GL_FRONT, GL_SHININESS, [50.0])
glLightfv(GL_LIGHT0, GL_POSITION, [1.0, 1.0, 1.0, 0.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 1.0, 1.0, 1.0])
glLightfv(GL_LIGHT0, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [1.0, 1.0, 1.0, 0.0])
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
self.update_projection_matrix()
gluLookAt(0, 0, -self.view_distance, # camera
0, 0, 0, # focus
0, 1, 0) # up vector
# Start animation
timer = QTimer(self)
timer.setInterval(10)
timer.setSingleShot(False)
timer.timeout.connect(self.rotate_view_a_bit)
timer.start()
self.stopwatch.restart()
print "RENDER_MODE\tSPHERES\tRES\tFRAME_RATE"

@override(QGLWidget)
def resizeGL(self, w, h):
"runs every time the window changes size"
glViewport(0, 0, w, h)
self.update_projection_matrix()

@override(QGLWidget)
def paintGL(self):
"runs every time an image update is needed"
self.stopwatch.restart()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glColor3f(255.0/300, 160.0/300, 46.0/300) # set object color
self.paint_immediate_spheres(self.sphere_resolution)
self.frame_times.append(self.stopwatch.elapsed())
# Report on frame rate after enough frames have been rendered
if 200 <= len(self.frame_times):
n = len(self.frame_times)
total = 0.0
for t in self.frame_times:
total += t
mean = total / n
print "immediate\t%d\t%d\t%.1f ms" % (self.number_of_spheres, self.sphere_resolution, mean)
# print "mean frame time = %f milliseconds" % (mean)
# Reset state
self.frame_times = [] # Reset list of frame times
try:
(r, n) = self.param_generator.next()
self.set_number_of_spheres(n)
self.sphere_resolution = r
except StopIteration:
exit(0)
# self.set_number_of_spheres(self.number_of_spheres * 2)
self.stopwatch.restart()

def paint_immediate_spheres(self, resolution):
glMatrixMode(GL_MODELVIEW)
for pos in self.sphere_positions:
glPushMatrix()
glTranslatef(pos.x, pos.y, pos.z)
glColor3f(pos.color[0], pos.color[1], pos.color[2])
glutSolidSphere(pos.radius, resolution, resolution)
glPopMatrix()

def paint_teapot(self):
glPushAttrib(GL_POLYGON_BIT) # remember current GL_FRONT_FACE indictor
glFrontFace(GL_CW) # teapot polygon vertex order is opposite to modern convention
glutSolidTeapot(2.0) # thank you GLUT tool kit
glPopAttrib() # restore GL_FRONT_FACE

@QtCore.Slot(float)
def rotate_view_a_bit(self):
self.y_rot += 0.005
x = self.view_distance * sin(self.y_rot)
z = -self.view_distance * cos(self.y_rot)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(x, 0, z, # camera
0, 0, 0, # focus
0, 1, 0) # up vector
self.update()


class SpherePosition():
"Simple python container for sphere information"
pass


class SpherePositions(list):
"Collection of SpherePosition objects"
def __init__(self, sphere_num):
for s in range(sphere_num):
pos = SpherePosition()
# units are nanometers
pos.x = random() * 10.0 - 5.0
pos.y = random() * 10.0 - 5.0
pos.z = random() * 10.0 - 5.0
pos.color = [0.2, 0.3, 1.0]
pos.radius = 0.16
self.append(pos)
assert(len(self) == sphere_num)


def enumerate_sphere_resolution_and_number():
for n in [1, 3, 10, 30, 100, 300, 1000]:
for r in [10, 50]:
yield [r, n]


# Automatically execute if run as program, but not if loaded as a module
if __name__ == "__main__":
SphereTestApp()

No comments: