Tags

, , , , , , , , , , , , ,

In my post Blender animation in OpenGL I created an animated 3D robot in Blender and exported it as a series of Wavefront OBJ format files. I then imported the OBJ files into my Python OpenGL application – using the Pygame OBJFileLoader script – so as to render the robot and watch in awe as its head bobbed up and down. Breathtaking.

But as I mentioned in the post, loading each frame of animation from an OBJ file when the OpenGL application starts can be slow. So in this post I am going to do some preprocessing! I am going to parse the OBJ files ahead of time, writing each OpenGL command into a Python file. Also, I am going to write just the first frame of animation in full – each subsequent frame will only contain the parts of the robot that are actually moving (namely its head). Let’s go!

Create Blender animation

Okay, so here is how the robot currently appears in Blender:

BlenderAnimationInOpenGLMarkII_RobotSphereHead

I think you’ll agree, a real looker.

But that sphere we are using for its head is a complex shape – meaning that we need a lot of data to represent it. And given that the head is the part of the robot we are animating, that’s a lot of polygons for OpenGL to load!

Instead, let’s change the robot head to a cube shape:

BlenderAnimationInOpenGLMarkII_RobotCubeHead

Granted, it looks pretty shit. But we will be adding a mouth and other such delights in a future post, so not to worry.

The robot will be animated in the same way as the previous post, with its head bobbing up and down. Here’s the animation timeline with keyframes at 0, 5 and 10:

BlenderAnimationInOpenGL_Timeline

Export Blender animation

Now, here’s the trick. If we deselect the robot’s head, we can export the robot from Blender as a single OBJ file (and supporting MTL file). After all, the robot’s body contains no animation, so why export 10 frames of it?

BlenderAnimationInOpenGLMarkII_RobotBodySelected

BlenderAnimationInOpenGLMarkII_RobotBodyFiles

Next, we will deselect the robot’s body and export only its head as 10 OBJ files (and 10 supporting MTL files). Since the head contains animation for it to bob up and down, we will need all 10 frames exported.

BlenderAnimationInOpenGLMarkII_RobotHeadSelected

BlenderAnimationInOpenGLMarkII_RobotHeadFiles

Tick ‘Selection Only’ to export only the selected parts of the robot. Tick ‘Animation’ if you want multiple OBJ and MTL files exported.

BlenderAnimationInOpenGLMarkII_ExportOptions

Preprocess OpenGL animation

Right. Now that we have our single robot body OBJ file and 10 robot head OBJ files, it’s time to do some preprocessing. Let’s use the Pygame OBJFileLoader as per the last post to import our OBJ files and generate OpenGL commands. But this time we are going to write each command into a Python file. That way, when we come to use our OpenGL application, it will read all the OpenGL commands from a Python file rather than having to slowly load and parse each OBJ file from disk.

But how do we write our OpenGL commands into a Python file? Let’s take the following piece of code from the OBJFileLoader as an example:

glVertex3fv(self.vertices[vertices[i] - 1])

All we need do is write this command into our Python file as a line of auto-generated code:

write_value('glVertex3fv({})'.format(self.vertices[vertices[i] - 1]), 1)

Easy! We just format the string value with the vertices. Here’s the underlying write_value function, which commits the line of code to disk:

def write_value(value, number_of_indents):

    # add indents
    for _ in range(number_of_indents):
        value = '    ' + value

    # add new line
    value += '\n'

    # write value to file
    with open('rocky_robot_frames.py', "a") as frames_file:
        frames_file.write(value)

Notice the number_of_indents parameter, which will allow us to format our auto-generated code correctly.

Simply repeat the approach for each OpenGL command that we want to write into the Python file. Some parts of the code generation were a little trickier than others, and required writing variables to disk along with the OpenGL commands. Here’s the start of the auto-generated code file:

### Auto-generated code ###

from OpenGL.GL import *
import PIL.Image

def rocky_robot_body_frame():

    materials = {}
    image = PIL.Image.open('images/rock.jpeg')
    image = image.tostring('raw', 'RGBX', 0, -1)
    texture_id = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, texture_id)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 502, 505, 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
    materials['Material.001'] = texture_id
    
    gl_list = glGenLists(1)
    glNewList(gl_list, GL_COMPILE)
    glFrontFace(GL_CCW)
    glBindTexture(GL_TEXTURE_2D, materials['Material.001'])
    glColor([1.0, 1.0, 1.0])
    glBegin(GL_POLYGON)
    glNormal3fv([0.0, 1.0, -0.0])
    glTexCoord2fv([0.0, 0.0])
    glVertex3fv([0.807, 0.807, -1.007])
    glNormal3fv([0.0, 1.0, -0.0])
    glTexCoord2fv([1.0, 0.0])
    glVertex3fv([0.293, 0.807, -1.007])
    glNormal3fv([0.0, 1.0, -0.0])
    glTexCoord2fv([1.0, 1.0])
    glVertex3fv([0.293, 0.807, -0.493])
    glNormal3fv([0.0, 1.0, -0.0])
    glTexCoord2fv([0.0, 1.0])
    glVertex3fv([0.807, 0.807, -0.493])
    glEnd()

There was a bit of hand-cranking after the generation of the code, to tidy up a few pieces of syntax – in time I will get the preprocessing working slick.

Render OpenGL animation

Brilliant! All that is left to do is call our auto-generated code from our OpenGL application on startup, to retrieve all the animation:

class RockyRobot(Robot):
 
    # load frames
    def load_frames(self, animation):
        self.body_frame = rocky_robot_body_frame()
        self.head_frames = rocky_robot_head_frames(animation)
        self.head_frames_length = len(self.head_frames)

You’ll notice that we are passing our auto-generated code an animation parameter for the robot head. Why? Well, our OpenGL application has a configuration value which allows robot animation to be switched off. If animation is switched off then our auto-generated code will return only a single frame for the robot head, rather than processing all frames of animation.

And that’s it. Preprocessing our Blender OBJ files into a Python file allows us to optimise the animation, so that our OpenGL application only needs to commit to memory the parts of the robot that change from one frame to another. Plus, the application can read all these commands from a handy auto-generated Python file, rather than having to slowly load each OBJ file from disk.

Time for a demo! Here’s my OpenGL application – SaltwashAR (a Python Augmented Reality application) – with an animated robot:

I have updated SaltwashAR on GitHub with the auto-generated code (and amended the documentation accordingly).

Any questions, drop me a line.

Ciao!

P.S.

I ran the code on my Windows 7 PC using Python Tools for Visual Studio.

Advertisements