## 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 PyOpenGLfrom PySide.QtGui import QMainWindow, QApplicationfrom PySide.QtOpenGL import QGLWidgetfrom PySide.QtCore import *from PySide import QtCorefrom OpenGL.GL import *from OpenGL.GLUT import *from OpenGL.GLU import *from random import randomfrom math import sin, cosimport sysclass 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 overrideclass 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"    passclass 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 moduleif __name__ == "__main__":    SphereTestApp()