OO Python Character Animation Design Study

From CSSEMediaWiki
(Difference between revisions)
Jump to: navigation, search
Line 1: Line 1:
 +
=== Background ===
 +
 +
My honours project required me to develop some software to run, analyze, and subjectively test compression algorithms on motion capture data. Originally I developed an OpenGL application in Python, since Python is easier to work with than C, but didn't make much use of the object-oriented features of Python, because python is traditionally used procedurally, and because OpenGL relies on procedural code also. 
 +
 +
My goal was to convert this program to a more fully object-oriented python program for easier modification and extensibility.
 +
 +
==== Starting Point ====
 +
 +
The original program featured a 3D scene displaying a motion. The user could press tab to cycle through the joints of the animated figure, and graphs of the motion for that joint would be displayed below. 
 +
 +
Here's the original class design for that program:
 +
 +
[[Image:Rle41_original_design.jpg]]
 +
 +
I refactored this functionality into an OO design, and then added a subjective test feature. For this feature, two 3D scenes are shown simultaneously, along with quiz instructions, and users are asked to guess which of the scenes shows a compressed figure, and which shows the original.
 +
 +
The final class diagram looked like this.
 +
 +
 +
 +
 +
 +
 +
 +
 +
 
=== Design Maxims Used ===
 
=== Design Maxims Used ===
  
Line 52: Line 78:
  
 
The visitor pattern is inevitably a trade-off between flexibility of the visitors and "visitees" (subjects). Here it is clear that flexibility of algorithm implementation is more useful, so the visitor pattern is clearly of benefit.  
 
The visitor pattern is inevitably a trade-off between flexibility of the visitors and "visitees" (subjects). Here it is clear that flexibility of algorithm implementation is more useful, so the visitor pattern is clearly of benefit.  
 +
 +
Currently only one compression algorithm is completed. Be warned that running this algorithm as the program is currently configured for is EXTREMELY slow (the output is saved so the algorithm need only be run once, and no effort has been put into optimization of the algorithm running time at this stage).
 +
  
  

Revision as of 02:54, 1 October 2008

Contents

Background

My honours project required me to develop some software to run, analyze, and subjectively test compression algorithms on motion capture data. Originally I developed an OpenGL application in Python, since Python is easier to work with than C, but didn't make much use of the object-oriented features of Python, because python is traditionally used procedurally, and because OpenGL relies on procedural code also.

My goal was to convert this program to a more fully object-oriented python program for easier modification and extensibility.

Starting Point

The original program featured a 3D scene displaying a motion. The user could press tab to cycle through the joints of the animated figure, and graphs of the motion for that joint would be displayed below.

Here's the original class design for that program:

Rle41 original design.jpg

I refactored this functionality into an OO design, and then added a subjective test feature. For this feature, two 3D scenes are shown simultaneously, along with quiz instructions, and users are asked to guess which of the scenes shows a compressed figure, and which shows the original.

The final class diagram looked like this.





Design Maxims Used

Law of Demeter

I made the Law of Demeter a big focus in this final redesign. I've found this to be a particularly helpful guide in structuring my code.

In particular, following the Law of Demeter stops one from "cheating" with one's interface definition. In early designs, I created a nice tidy interface for a class, but would do all the dirty work associated with that class by grabbing an object "through" the interface and then manipulating it. Obviously, this is counter-productive, and result in unnecessary coupling. The apparently "clean" interface only results in more dirt elsewhere.

Following the Law does add to the size of your interface, and results in some cascading getter methods, but provides a more accurate picture of the realities of what one is exposing. It also encourages the programmer to expose only the exact data necessary, rather than whole objects. This is good for reducing coupling.


Tell, don't ask

In earlier designs, the Window code would set up the size it required for each of its viewports. I refactored the code to be more in accordance with this maxim by giving each viewport a default size, and writing a rudimentary layout manager to change or override sizes if required.

This change made the code much more extensible. After this change I wrote a subclass of Window called QuizWindow, which runs a user evaluation program. I was able to change the program from a single figure view with graphs, to a double figure view with quiz instructions, very simply and intuitively.

I simply constructed two FigureViewport objects, wrote a small QuizMaster object to run the quiz, along with a QuizViewport to display instructions, and it Just Worked. Because I adopted the Tell don't Ask philosophy, I could just tell the program to make me two FigureViewports, rather than having to faff about specifying their sizes manually.


Observer

In this program, viewports have to work quite closely together. For instance, a GraphViewport needs to update every time the animation frame changes or the highlighted joint changes, so that it can update the Graph display appropriately.

In my initial design, the constructor for a FigureViewport had a parameter whereby one would pass in the GraphViewports that needed notifying. This seemed unclean as it resulted in unnecessary coupling, so I refactored the code such that arbitary observers could be attached to a Figure Viewport. Any object that registered as an observer of a FigureViewport merely needed to implement one method (effectively implementing an interface, although Python doesn't have explicitly defined interfaces).

The resulted in greater extensibility for the Quiz program modification. The quiz layout features no graphs, and thus there are no listeners for the Figure Viewports in this situation. Instead of having to worry about passing in a null parameter or similar, I was able to completely ignore the listener functionality, which felt much cleaner and simpler.


Composite

The hierarchical joint structure of an animated figure is well suited to the composite pattern, since we have Joints which contain other Joints. Thus we have a Joint superclass with a primitive subclass (LeafJoint) and a composite subclass (InternalJoint). There is also a subclass of InternalJoint, RootJoint to add extra functionality to the first joint. For instance, a RootJoint has positional information whereas other joints have only angular orientation relative to their parent joints.


Template Method

Joints are drawn in the same way, but InternalJoints and RootJoints must define extra code before and after drawing. Ideally, we shouldn't have to redefine the basic drawing code in each subclass. The the display() method of Joint is not subclassed, but two associated methods, predisplay() and postdisplay(), are. Note that the restriction on subclassing display is merely my own convention. There is no provision in Python for defining access restrictions.


Visitor / Strategy

The joint objects remain relatively fixed, but the addition of an approximation algorithm requires new operations to be defined over the hierarchy. Adding this code directly to the AnimatedFigure class adds clutter, particularly for complex algorithms such as point elimination based on endpoint differences.

Separating these algorithms into their own classes makes the code much cleaner, and makes it much more straightforward to add additional algorithms.

Python's first-class functions mean that the strategy pattern is overkill by itself (an algorithm can be changed dynamically simply by storing a function in a variable, and modifying that variable as required, so separate objects are not necessary) but the different visitor objects for each algorithm are effectively strategy implementations also.

The visitor pattern is inevitably a trade-off between flexibility of the visitors and "visitees" (subjects). Here it is clear that flexibility of algorithm implementation is more useful, so the visitor pattern is clearly of benefit.

Currently only one compression algorithm is completed. Be warned that running this algorithm as the program is currently configured for is EXTREMELY slow (the output is saved so the algorithm need only be run once, and no effort has been put into optimization of the algorithm running time at this stage).


OpenGL Abstraction

At the low level, the program still relies on the functional OpenGL library calls. I saw little use for a Graphics object (eg graphics.draw_point(x,y) ), which would require additional learning effort from programmers with little resulting benefit. Instead, I focussed on abstracting the less relevant parts of the OpenGL library such that the difficult details of the OpenGL system are largely hidden. The hope is to allow the developer to create a graphics application more easily by dividing the graphics creation workload into the implementation of a display method for each program object.

Complex graphics can then be built up from a number of smaller primitive objects. Also, because each object rendered is an actual program object, integration with object-oriented application logic is far more seamless.

Boilerplate

GlutMachine and GlutApplication provide an abstraction for the mandatory parts (the "Hello World" parts) of an OpenGL application. Dividing the boilerplate into two classes means that the largely fixed parts of the boilerplate (for instance, the call to glutMainLoop() ) are kept separate from the more configurable parts of the boilerplate (such as the glClear call, which can take various parameters).

The existing implementation of GlutApplication is quite straightforward, allowing easy extension through modification or subclassing if a multi-windowing, additional library calls, or other complexity is required.

Event Handling

Another component of the OpenGL library I have abstracted away is the Keyboard event handling. Instead of writing a large switch statement to deal with keypresses, any object can simply wire up event handlers by calling a static method. For instance, a FigureViewport can have its next_joint method called when the tab key is pressed simply by adding the following line:

Keyboard.attach_key_listener("\t", self.next_joint)

It made sense to make Keyboard static as there is only one keyboard.


Accomodations for Python Language Style

Python has some rough edges for its OO support. Properties are simple to define (this syntax is PropertyName = property(getter_method_name, setter_method_name), and really help tidy up one's code, but due to a bug/flaw the getters and setters cannot be subclassed unless one explicitly redefines the property in the subclass. Hopefully this is fixed in a later version.

There was also a bug with default parameter values: If default parameters are defined both in a superclass and a subclass, then the superclasses default values take precedence. Possibly I am misunderstanding something but in any case, the values can fairly easily be set explicitly in the constructor whenever one runs into this problem.

Python is not a pure OO language, so I considered it overkill to make objects for absolutely everything. Python relies heavily on lists. I did not think it sensible to wrap a collection object around these lists, since they are a well-established and tested language concept. Hence I have retained lists (and occasionally tuples) as the primary means of storing collections.

In particular, there are no 3D primitive objects. This is partially because a 3-item list serves perfectly adequately, and also because the focus of this program involves splitting 3d points and angles into separate axes, regarding each axis largely separately. Thus a Point class (or similar) was not warranted.

Personal tools