wordbotch.com

Blog


Custom OpenGL Screensaver

First post in a looong time. A couple of months ago at my work (Tyro Payments) we had a 'codeblitz' where developers could work on projects of their own design. For want of anything better, I made a custom screensaver with an OpenGL 3D model of the company logo.

At work we use Ubuntu, which comes with gnome-screensaver by default. This is an application that just dims the screen after a timeout. Ubuntu has decided this is the way to go, since it's more efficient and no-one uses CRT monitors any more.

Another Linux-y screensaver is xscreensaver, which has been around for ages. Created by Jamie Zawinski, it gave birth to a lot of familiar classics, like 'pipes' and 'flying toasters'. I gather that older versions of Ubuntu came with xscreensaver, and some other *nix flavours still do.

So, I made my own xscreensaver module and this is how. I won't go into too many details here, I just want to paint in broad strokes the approximate path I took and problems I came across. First, uninstall gnome-screensaver and install the xscreensaver source.

> sudo apt-get uninstall gnome-screensaver -y
> sudo apt-get source xscreensaver -y
> sudo apt-get build-dep xscreensaver -y --fix-missing
> chown -R [user] xscreensaver-5.15
Then build xscreensaver.
> cd xscreensaver-5.15
> ./configure
> make

There are a bunch of existing Open GL screensaver hacks in xscreensaver-5.15/hacks/gl. I based mine on the 'bouncing cow' module, also by Jamie Zawinski. It's about 500 lines of C. Unsurprisingly, it renders a 3D cow that bounces up and down to hilarious effect. In terms of actual code changes, a lot of it was removing unnecessary stuff that I didn't need (like, um, the jumping).

To avoid hacking the existing Makefile I made a separate build script, by adapting the output of a Make for 'bouncing cow'.

cd xscreensaver-5.15/hacks/glx
rm *cow*.o
make bouncingcow >> mybuild.sh
chmod +x mybuild.sh

The model used by bouncing cow consists of several objects stored as C float arrays, in an interleaved OpenGL format identified by the constant GL_N3F_V3F. As I understand it, the first three floats in an array identify a normal and the next three floats identify the vertex for that normal. Then normal, vertex, normal, vertex, etc. The length of the final array is thus a multiple of 6. I had a bit of trouble working out how to transform my model to this format, but got there in the end.

I made my model in Blender and exported it as a .obj file. The export settings are important though. I found that in Blender, I had to check the following checkboxes (only) in the bottom-left of the export screen.

  • normals
  • no materials
  • triangulate
Having exported to an .obj, I used obj2opengl, a Perl script by Heiko Behrens to transform that to an OpenGL-friendly format. The resulting file contains two arrays, one for vertices and one for normals.

Then I wrote some python to transform this file to the GL_N3F_V3F format required. This was just a matter of interleaving the two float arrays produced by the obj2opengl script.

def main():
 src_name = sys.argv[1]
 f_src = open(src_name, 'r')
 vertices = []
 normals = []
 state = None
 for line in f_src:
  if re.match('.*,.*,.*,.*', line):
   if state == 'normals':
    normals.append(line.strip())
   elif state == 'vertices':
    vertices.append(line.strip())
  elif re.match('float .*Verts \[\] = {', line):
   state = 'vertices'
  elif re.match('float .*Normals \[\] = {', line):
   state = 'normals'
 f_src.close()

 if len(vertices) != len(normals):
  print "error: vertex/normal counts do not align: (v=%s, n=%s)" \
   % (len(vertices), len(normals))
  sys.exit()
 vertex_count = len(vertices)

 dest_name = os.path.splitext(src_name)[0] + '.c'
 f_dest = open(dest_name, 'w')
 f_dest.write("#include \"gllist.h\"\n")
 f_dest.write("static const float data[]={\n")
 for i in range(0, len(vertices)):
  f_dest.write(normals[i] + ' ' + vertices[i] + '\n');
 f_dest.write("};\n")
 f_dest.write("static const struct gllist frame=" + \
  "{GL_N3F_V3F,GL_TRIANGLES,%s,data,NULL};\n" % vertex_count)
 f_dest.write("const struct gllist *%s=&frame;\n" % MODEL_NAME)
 f_dest.close()

if __name__ == '__main__':
 main()

Done. All in all, I was pretty happy with the end result.