Computer Graphics Lab 5: Hierarchical Modeling

Ben Mitchell and Zach Pezzementi

A formation animation with random starfield background and illusory perspective:



Modules and Models

In our system, a "model" is indistinguishable from a "module." A module is the basic unit of a hierarchical model, and is what we draw onto an image.

When we create a new module, it is empty. At this point, many things can be added to it. The two primary classes of things that can be added to it are drawables and transforms. When the module is drawn, things are done in the order they were added; drawables are drawn to the image, and transforms modify the LTM, which affects the way drawables appearing after the transform are drawn. "Drawables" include graphics primitives such as points, lines, and polygons, as well as other modules (sub-modules). It is this ability to call modules as drawables from within other modules that makes the system "hierarchical."

The "data type" is, as always, a C++ class. In fact, there is a base "Module" class, and several inheriting classes for drawables and transforms. The base class has a list of pointers of type "Module," and the draw function simply goes through the list and calls the apply function of each item. The apply function is overloaded in each of the inheriting types to do the appropriate operation. Operations can be added either by hand (ie. create a module, then add it), or by GraphicImage style calls, such as Module::drawPolygon(), or Module::translate().

Drawing is accomplished by passing the module to an image; the function GraphicImage::drawModule() takes a module, and simply draws it onto the image. The GraphicImage stores a global VTM which is used to transform all the drawing primitives inside the modules. This function calls the apply function of the Module it was passed, and that apply function in turn goes through it's list of sub-modules, calling each of their apply functions. This continues recursively until the entire module has been drawn.

For this assignment, we created a starfighter module which contained a series of drawPolygon commands to draw the body of the fighter. It also contained several gun modules. The gun module was separate, due to the fact that we wanted several guns all of which would be alike. The rest of the ship offered little repetition of substructure, so it was all done as a single module (the wings could probably have been done as sub-modules, but since there were only two of them, and they would have required reflecting, this seemed a little silly).

An image of our starfighter module; click for a larger version
(if it looks ugly in firefox, click on it to make sure it's not getting scaled down)

We then created several formations of these starfighters, which we animated independently so that they started out behaving as a single large echelon formation, and then split off and went three different directions (see animation below).

In terms of extensions, we created animations, though we ran into some problems with the limitations on animated gifs; as it turns out, large images and/or sequences with high framerates do not work out well. Ben wrote a class to do Xlib stuff, and tried to make a better animation program, but due to the limitations of X, there's really no way to get a pretty animation without loading every frame into main memory before you start, which also limits how large and/or high frame-rate a sequence you can create.

We also created an animation class, which contains one of several time-parameterized transformation classes. When associated with a module, this allows us to create interesting animations without manually specifying the motion for every frame. Our main loop for generating the animated gif below, for example, looks something like:

    ...
    ...
    rightAnimation.setModule(echelonrightModule);
    rightAnimation.addFunction(&turnRight);
    rightAnimation.addFunction(&translateUp);
    rightAnimation.addFunction(&translateUpAndRight);

    for(int i=0; i<=frames; i++){
      double time;
      Module current;
      im.init(xsize, ysize, samples);
      im.setVTM(vtm);

      time = (double)i/(double)frames;
      current = centerAnimation.getModule(time);
      im.drawModule(*current);
      current = rightAnimation.getModule(time);
      im.drawModule(*current);
      current = leftAnimation.getModule(time);
      im.drawModule(*current);

      sprintf(buf, "%03d.ppm", i);
      name = "fighters";
      name += buf;
      cout<<"Writing frame "<<i<<"...\n";
      im.write(name);
    }

    

While this implementation is not quite what we think Bruce was talking about in his description of the extensions, it does in fact give runtime-parametarizable animation support. We plan to move these animations into our Module class later, so that they can be nested; right now, a module must be associated with an animation, rather than the other way around, so animating sub-modules, while possible, is a little bit messy and counter-intuitive.

Also, we decided that aliasing in our animations was really ugly, so we implemented full image super-sampling anti-aliasing. It's a run-time parametarizable setting for GraphicImages, though it can only be set when initializing (or re-initalizing) an image, since switching behavior is poorly defined when we have an existing image. Filled Polygons, lines, circles, and ellipses work nicely in this mode. The only thing that doesn't work quite right is the behavior of polylines, and outline circles and ellipses when alpha blending is being used (we just haven't bothered to fix it, because we're not really using those primitives anymore). You can see the anti-aliasing in action in all of our images except the next two, one of which is un-anti-aliased for comparison.

A formation animation without anti-aliasing:

A formation animation with anti-aliasing:



API

Here are the headders for our Module class and the classes which inherit from it:

    enum moduleType {mod, xformMod, lineMod, polygonMod, pointMod};

    class Module {
      public:
        Module();

        virtual ~Module();

        // function to put the module into an image;
        // really, you should call GraphicImage::drawModule() instead
        void apply(GraphicImage &im);
        // add a module (or inheriting class) to the list of things to do
        void add(Module *m);

        // These work basically like the GraphicImage versions,
        // from a user's perspective, but rather than drawing to an image,
        // they queue up the commands in the module so they all get done
        // when the module is drawn.
        // Note also that these take Vects instead of Points, because
        // we're working in math space rather than screen space.
        void plot(const Vect &p, const GraphicContent &gc);
        void drawLine(const Vect &p1, const Vect &p2, const GraphicContent &gc);
        void drawPolygon(const VectList &pl, const GraphicContent &gc);
        void fillPolygon(const VectList &pl, const GraphicContent &gc);
        void xform(const Xform &xf);

        // overloaded operators; just shortcuts for add() and copy
        void operator +=(Module *m);
        void operator =(Module &m);

        // lots of xforms; just wrappers that call Xform::<whatever>
        // and then add() the result
        void I();
        void rotateX(double theta);
        void rotateY(double theta);
        void rotateZ(double theta);
        void rotateAt2D(const Vect &c, double theta);
        void rotate(Vect vx, Vect vy, Vect vz);
        void reflectX();
        void reflectY();
        void reflectZ();
        void translate(double tx, double ty, double tz);
        void scale(double sx, double sy, double sz);
        void scaleAt2D(const Vect &c, double sx, double sy);
        void scaleRotateAt2D(const Vect &c, double sx, double sy, double theta);
        inline void translate(double tx, double ty){ translate(tx, ty, 0); }
        inline void scale(double sx, double sy){ scale(sx, sy, 1); }


      protected:
        // we need to keep track of this for copying, among other things
        moduleType mytype; 
        list<Module *> mylist;
        Xform ltm; //local transform matrix
        Xform gtm; //global transform matrix

        // This is what gets overloaded by all the inheriting classes
        // It's private because we don't want the user to call it on
        // anything but a top-level module, since the results are
        // ill-defined
        virtual void doApply(Module *caller, GraphicImage &im);

        // This is just for Modules; since they might recurse, we need
        // to treat them slightly differently
        void myApply(GraphicImage &im);

        // classes which might need to alter our GTM
        friend class XformMod;
        friend class Anim;

    };


    // The classes for point and line are almost identical, with somewhat
    // simpler protected data and arguments to the set() function.
    // They have not been included here for the sake of brevity and clarity
    class PolygonMod : public Module {
      public:
        PolygonMod();
        ~PolygonMod(){
        }

        void set(const VectList &p, const GraphicContent &gc, bool filled);
        void apply();

      protected:
        VectList pl;
        GraphicContent mygc;
        bool fill;

        void doApply(Module *caller, GraphicImage &im);
        friend class Module;
    };


    // The interface is just like the others, but what it does is quite
    // different; XformMod must actually alter the LTM of it's parent,
    // which is why the Module *caller argument is there.  Every time a
    // module calls doApply() on something in it's list, it passes it's own
    // *this pointer as the caller argument for exactly this reason.
    // This is also the reason XformMod must be a friend class of Module.
    class XformMod : public Module {
      public:
        XformMod();
        ~XformMod(){}

        void set(const Xform &xf);

      protected:
        Xform myxform;

        void doApply(Module *caller, GraphicImage &im);
        friend class Module;
    };

    

Our object-oriented approach makes things easy to use. First, a Module is created, either on the stack, or with the 'new' command. It is then filled with drawables and Xforms to draw whatever the user wants, with all coordinates being given in model space. At this point, other modules can be created which can optionally include this first module as a sub-structure. The code for interacting with a module to add drawables is easy to write, because the same drawing primitives that can be used with a GraphicImage can be used with a Module, as can the primitives used with an Xform. This makes the creation of Modules both concise and clear.

Once we have created a module, which may contain any number of other modules as part of it's substructure, we can then draw it to an image with the same sort of command as would be used for drawing anything else to an image, with the exception that a View Transform Matrix (VTM) must have first been defined for that image; this tells us how to go from model space (where our modules still exist) to screen space, so that everything displays correctly. GraphicImage::drawModule() does all the work for us, hiding all the ugliness and recursion safely away where the user doesn't need to look at it.