graphicsDeploy index


phase 5:
hierarchical modeling system

cs40 assignment 5 web page



overview

The purpose of this assignment was to create a hierarchical modeling environment for manipulating and drawing 2d graphics primitives -- unit lines, unit circles, and unit squares. The actual drawing routines are still in draw.h (albeit somewhat modified), where transformed unit squares are drawn with drawPolygon() or fillPolygon() and transformed unit circles are drawn with drawEllipseAngled() or fillEllipseAngled(). model.h has the actual modeling functions. After establishing a decent hierarchical system, it seemed like a good idea to add a set of keyframing features to facilitate animation, which are in keyframe.h. As always, documentation on the various libraries is available, at the api.


node hierarchy

The hierarchy is built from Node structures. Each node can be either a primitive (something you can draw), a transformation (parameters to create a transformation matrix -- translate, scale, rotate, shear), an attribute (sets parameters of the drawing state -- colors, gradients, etc.) or a model, which contains another Node.

A model is created by calling newModel(), which returns a Node, and then appending various Nodes to the model using insNode(). For instance, you might set the color to RED using an attribute node and then set a translation and rotation using transformation nodes, then specify that a square should be drawn with the current color and drawn under the given transformations using a primitive node. To build hierarchically, model nodes can be inserted using insNode().

For example, the xwing model is constructed from two wings and a body; the hierarchy looks something like:

      xwing
      / | \
     /  | mirror
    /   |      \
   / translate translate
  /          \ /
body        wing

where the body and wing models are each made up of variously transformed and attributed unit squares (its over 100 lines of code i think, so i'm not going to do out the entire graph...) In the following image, there is a squadron model node containing four xwing instances, and then three squadron instances to make up the fleet. Code for this model looks something like:

xwing_body = newModel();
insNode(xwing_body, ...

xwing_wing = newModel();
insNode(xwing_wing, ...

xwing = newModel();
insNode(xwing, scale(0.3,0.3,1.0));
insNode(xwing, scale(0.8,0.9,1.0));
insNode(xwing, xwing_body);
insNode(xwing, identity());
insNode(xwing, scale(0.3,0.3,1.0));
insNode(xwing, translate(0.7, 0.0, 0.0));
insNode(xwing, xwing_wing);
insNode(xwing, scale(-1.0, 1.0, 1.0));
insNode(xwing, xwing_wing);

squadron = newModel();
insNode(squadron, scale(ship_scale, ship_scale, 1.0));
insNode(squadron, rotate_z(-45.0));
insNode(squadron, xwing);
insNode(squadron, translate(-1.0, 2.5, 0.0));
insNode(squadron, xwing);
insNode(squadron, identity());
insNode(squadron, scale(ship_scale, ship_scale, 1.0));
insNode(squadron, rotate_z(-45.0));
insNode(squadron, translate(2.0, 0.5, 0.0));
insNode(squadron, xwing);
insNode(squadron, translate(0.5, 1.5, 0.0));
insNode(squadron, xwing);

fleet = newModel();
insNode(fleet, squadron);
insNode(fleet, rotate_z(15.0));
insNode(fleet, scale(0.8, 0.8, 1.0));
insNode(fleet, translate(-4.0, -1.0, 0.0));
insNode(fleet, squadron);
insNode(fleet, identity());
insNode(fleet, rotate_z(-15.0));
insNode(fleet, scale(0.8, 0.8, 1.0));
insNode(fleet, translate(-0.5, -4.0, 0.0));
insNode(fleet, squadron);

Additional sample modeling code is available in the api.


rendering

In general, rendering of a scene is performed by constructing the view transformation matrix (vtm) (from the specified camera parameters -- see model.h), then calling render_model(), an internal function to model.h, with its global transformation matrix (gtm) set to identity(), on the world Node.

render_model() traverses the contents of the node it is given; when it encounters an attribute, relevant drawing state variables are set; when it encounters a transformation, the appropriate changes are made to the current local transformation matrix (ltm); when it encounters a primitive, it creates a transformation matrix [tm] = [vtm] * [gtm] * [ltm] and then draws the specified primitive under that transformation; and when it encounters a model node, it calls render_model() on the model node, setting the gtm to [gtm'] = [gtm] * [ltm]

At the user level, there are three basic rendering functions -- render_scene() renders a single frame using the current parameter values at all nodes (no key framing is used); render_frame() renders a single frame at a specified time value (keyed nodes are set the appropriate values for that time); and render_batch(), which renders a range of frames and automatically saves them out to numbered files.


animation and keyframing

The basic idea behind keyframing is that, to specify a time-variant value, rather than specify the value at every instant in time, you can just specify a few "key" values at particular times and let use the computer to interpolate the intermediate values. I implemented this by creating linked lists of KeyDbl structs, which contain a double value and an associated double time. The list is kept in time order. Then, when rendering, when render_model() encounters a KEYED node, it calls resolve_keys(), which sets all keyed values in that node to what they should be at the specified time.

Currently, only transformation nodes can be keyed, and only linear interpolation is used (but stay tuned for spline-based interpolation in the hopefully not-too-distant future...) However, there is considerable flexibility built in, for example you can key all xyz translations using key_translate() or key just a single coordinate using key_translate_x(), etc., and you can go back and forth on the same node (for instance, you might model a bouncing ball by setting all keys with key_translate() at the start and end of the motion, then handle the actual bouncing by adding several keys in only the y coordinate using key_translate_y()).

All animations on this page were created using keyframing. The following code fragment was used to generate the subsequent orbit-the-sun animation:

xwing = newModel();
insNode(xwing, ...

ti = newModel();
insNode(tie, ...

scene = newModel();
insNode(scene, sun);
insNode(scene, scale(0.15, 0.15, 1.0));
insNode(scene, (tie1_t = translate(3.3, 0.0, 0.0)));
insNode(scene, (tie1_r = rotate_z(45.0)));
insNode(scene, tie);
insNode(scene, identity());
insNode(scene, scale(0.15,-0.15,1.0));
insNode(scene, translate(2.15,0.0,0.0));
insNode(scene, (xwing_r = rotate_z(-90.0)));
insNode(scene, xwing);

make_keyed(tie1_r);
key_current(tie1_r, 0.0);
key_rotate(tie1_r, 405.0, 1.0);
make_keyed(tie1_t);
key_current(tie1_t, 0.0);
key_current(tie1_t, 1.0);
key_translate_x(tie1_t, 5.0, 0.5);
make_keyed(xwing_r);
key_current(xwing_r, 0.0);
key_rotate(xwing_r, -450.0, 1.0);

One other little feature is the time_offset field in the Node structure. The accessor function will only let you set this for model nodes. It allows you to set a local time offset for an animated model. Thus, if you had a ball which bounced from time 0.0 to time 2.0, but you were putting it into your world scene and wanted it to bounce there from time 5.0 to 7.0, you could just call set_time_offset(ball, -5.0) and you'd be good to go. This is really useful for having multiple instances of the same animated model when you want their animations to happen at different times (an example of this is the tie/xwing battle below, where the two tie fighters explode at different times, even though they are both mere instances of the tie_explode model).




graphicsDeploy index