Introduction to Object Oriented Animation
In the past 20 years, the video game industry which has seen rapid
growth from simple 2D monochromatic games to full blown high resolution,
full color and fast 3D games that are so popular today, and even modern
films are full of computer-generated images. At the same time,
object-oriented programming has made it easier to conquer the complexity
involved in creating high quality computer animation. Creating an
animation can be conveniently thought of as drawing objects currently in
view on the computer screen. For example, a monster in the game DOOM is
an object instance of the class for that kind of monster.
Simply put, an Object Oriented Animation application defines various
classes, creates the instances of these classes and performs operations
on the object instances. Below, we'll explain some of the essential
abstractions that you can use to create your own computer animations.
This article assumes that you have basic familiarity with the
principles of object-oriented programming in C++. If you don't know
C++, first check the C++ Made Easy tutorial, especially the sections on classes and inheritance.
Graphic Object abstraction
The most basic abstraction in animation is that of a Graphical
Object, which we'll represent here as class GraphicObject. Anything that
is drawn on the screen as a unit is a Graphic Object. A point, a line, a
polygon, a Bezier curve, a Bezier surface, a polygon with a texture
map, a background, a flat image, a sprite a 3D shape made of polygons
are all examples of Graphic Objects. (Don't worry if you don't know
what something like a Bezier curve or a sprite is—Bezier curves and
surfaces are just special curves and surfaces that have mathematical
properties that make them easy to work with in animation, and a sprite
is just a simple 2D image—your mouse pointer on the screen is an example
of a sprite). Simple graphic objects can be combined into more complex
graphic objects.
The most basic problem that an animation engine must solve is to be
able to draw these objects on the screen. In procedural (non-objected
oriented) programming, there may be nothing in common between the
functions to draw a cube and a sphere. But in object oriented
programming, we want to be able to have a generic solution for drawing
objects. We can think of "drawing" a graphical object as an action that
someone (the caller) can perform on an entity (the callee) i.e. the
Graphic object. The caller should not have to care about the details of
how the drawing is done, as long as the Graphic Object makes the action
available to the caller. So the class GraphicObject which should have a
method on it to draw the object, i.e. GraphicObject::draw(). Concrete
graphic objects like cubes, spheres, lines, curves, surfaces etc. can
all inherit from class GraphicObject and implement the method
GraphicObject::draw().
Designing a draw() function this way is not only good style, but has
some very important implications for animation engine design. An engine
that deals only with the abstract class GraphicObject will NOT have to
be changed on introduction of new types of Graphic Objects. As an
analogy, a hypothetical automobile engine may be designed to be able to
take in gasoline, ethanol, natural gas, or perhaps even vegetable oil!
As long as the combustion unit design is isolated from other parts of
the engine, such that it interfaces with the rest of the engine through a
piston that can drive other parts, it does not matter how the
combustion unit is built or what type of fuel it uses. To the driver of
the automobile, the engine must simply have an external interface, i.e.
the ignition system. The driver simply needs to insert the ignition key
and turn it to start the engine. The GraphicObject abstraction in the
case of an animation engine is like the combustion unit, and the method
GraphicObject::draw() is like turning on the ignition.
Thus, the method GraphicObject::draw() is the essential method for to
all Graphic Objects. Without this method, the object cannot be made
visible on the screen. By making the GraphicObject::draw() an abstract
method, we get a complete separation of interface and implementation, so
that the animation application can call this method on each Graphic
Object in a generic manner, yet each Graphic Object can be drawn in its
own unique way.
To further extend the scope of GraphicObject, we can include camera
movements as drawing operations. Thus in addition to objects that the
user can see, the GraphicObject abstraction could include
implementations of the animation engine camera—we'll talk more about
this later.
The Graphic Object abstraction has properties and methods that may or
may not be implemented by all Graphic Object classes. Most Graphic
Objects have the property of "position", which the method
GraphicObject::draw() can use as a reference point for drawing the
object. The position is like an anchor point for the graphic object.
GraphicObject::draw needs to know how to draw the object with reference
to that position.
When the graphics engine moves the graphic object, the
GraphicObject::draw() method needs to recalculate the location of all
parts of the graphic object and draw them at their new location, thus
redrawing the graphic object as a whole. The advantage of the position
property in GraphicObject is that the engine simply sets a new position
on the graphic object, and the graphic object redraws the rest of itself
in the new location. Without the position property, the engine would
have to relocate individual parts of the graphic object, thus binding
the engine design to the details of its graphic objects.
Not all user visible graphic objects need a position property. An
example is a graphic object representing the background. The drawing of
the background is not tied to any position because it is just drawn on
the entire screen. On the other hand, there may be graphic objects that
are not user visible, but do need a position. An example of such a
graphic object is the animation camera, as it does need a position to
compute the view of the rest of the visible objects.
Graphic Objects with the position property may also have operations
on them to perform rigid body movements, such as
GraphicObject::reposition(), GraphicObject::displace(), or
GraphicObject::rotate(). A "rigid body movement," by the way, is just a
mathematical term for a movement where all the points in the object stay
in the same places relative to each other after the movement—for
example, if you pick up a book and put it on the bookshelf, the cover
and all the pages are still in the same places relative to each other
afterwards, even though the book has moved. An "elastic body movement"
on the other hand is when the points move in relationship to each
other—for example, after you inflate a helium balloon, the points on the
balloon are much further from each other than before. Elastic body
movements could include GraphicObject::scale(), or
GraphicObject::shear(). These rigid and elastic movements could
potentially be made part of the abstract Graphic Object interface, as
they apply in a generic manner to a large variety of Graphic Objects.
Note that these methods are not intended to draw the graphic object.
These methods affect the target object so as to make the method
GraphicObject::draw() draw the object with the desired transformation
applied.
- GraphicObject::reposition(Vector new_position) : sets the position of the Graphic Object to the given new position. The object will get drawn around the new position by the method GraphicObject::draw().
- GraphicObject::displace(Vector displacement) : sets the position of the Graphic Object to a new position that is the sum of the current position and displacement vector. The object will get drawn around the new position by the method GraphicObject::draw().
- GraphicObject::rotate(Vector axis, double angle) : rotates the point representing the position of the body around the given axis by the given angle. The object will get drawn around the new position by the method GraphicObject::draw().
- GraphicObject::scale(double proportion) : sets a scale by which the Graphic Object is to be resized, without changing its position. The object will get drawn around the new position by the method GraphicObject::draw().
- GraphicObject::linear_shear(Vector axis, Vector shear_displacement) : repositions each point of the Graphic Object by moving it parallel to the given axis, by the given shear displacement. The object can be drawn around the new position when you call GraphicObject::draw().
- GraphicObject::planar_shear(Vector plane_axis_1, Vector plane_axis_2, Vector shear_displacement) : repositions each point of the Graphic Object by moving it parallel to the given plane defined by plane_axis_1 and plane_axis_2, by the given shear displacement. The object will be drawn around the new position when you call GraphicObject::draw().
On the other hand, "plastic body" movements—operations that change
the shape of the object in potentially arbitrary ways—probably shouldn't
be a method on the abstract Graphic Object. A good example of
performing plastic operations on graphic objects is in the game SPORE,
which allows the player to reshape a creature's body through plastic
deformations.
The problem with plastic body movements is that the function
interface required for that operation depends greatly on the specifics
of the Graphic Object. To achieve plastic transformations of a graphic
object, the graphic object must be modified using a more specific
interface, independent of the animation engine. For example, event
handling code in an animation game may map certain events to operations
on instances of graphic objects. These operations would be specific to
the type of the graphic object and the application and could perform the
plastic transformations on the graphic object using methods that are
not available on the GraphicObject itself. In fact, the animation engine
will not even know about these happenings, but will simply call
GraphicObject::draw(), which will draw the object with the new plastic
deformation.
Given that there will be properties of a Graphic Object that are
unique to its implementation, we need a GraphicObject::refresh() method
that could get called internally from methods like
GraphicObject::displace(), GraphicObject::rotate(),
GraphicObject::shear() or GraphicObject::scale() to perform calculations
common to all these methods. For example, take a flashlight Graphic
Object (class FlashLight) being rotated around an axis. The
FlashLight::rotate() method should do just the geometric transform, e.g.
changing the direction vector property of the flashlight. It shouldn't
need to worry about the direction of the light source—instead, it can
call the FlashLight::refresh() method which will change the position of
the flashlight's light source based on the new direction of the
torchlight. The refresh method saves other methods the trouble of
recalculating changes in non-geometric properties that result from a
geometric operation.
Here's a possible interface for the Graphic Object based on what we've looked at so far:
class GraphicObject { protected: virtual void refresh() = 0; public: virtual void draw() = 0; virtual void reposition(Vector position) = 0; virtual void displace(Vector position) = 0; virtual void rotate(Vector axis, double angle) = 0; virtual void scale(double proportion) = 0; virtual void linear_shear(Vector axis, Vector shear_displacement) = 0; virtual void planar_shear( Vector axis1, Vector axis2, Vector shear_displacement) = 0; }; class PositionalGraphicObject : public virtual GraphicObject { protected: Vector position; public: Vector get_position(); void set_position(Vector p); };
Composite Graphic Object Abstraction
Not all graphics objects need to be based on pixels. Simple Graphic
Objects can be used to create a Composite Graphic Object. For example, a
cube could have a list of 6 Face Graphic Objects as a field to define
the surface of the cube. This greatly simplifies operations like
rotating the cube around an axis. As long as the Face Graphic Object has
the rotate method implemented, the Cube Graphic Object simply has to
call that method on each of the Face objects in its list and the Cube
will rotate as desired. Using a generic Graphic Object greatly
simplifies the composition and implementation of more complex Graphic
Objects. Imagine a creature in a game like SPORE—it will be composed of
simpler objects representing the head, torso, arms and legs, which in
turn would be composed of polygons or Bezier surfaces. To make the
creature rotate, the abstract rotate operation with identical parameters
needs to be called on its head, torso, arms and legs, which would in
turn do the same on their list of polygons or Bezier surfaces. The
complex creature object is able to delegate much of the work to simpler
objects, rather than having to compute everything itself.
Here's pseudocode showing a possible declaration of a composite graphic object:
class CompositeGraphicObject { protected: List* graphic_object_list; public: CompositeGraphicObject(List*); void add_graphic_object(GraphicObject* graphic_object); void delete_graphic_object(GraphicObject* graphic_object); };
Camera Abstraction
The Camera is an essential abstraction for an animation engine. The
camera maps objects in an animation to the rectangular animation display
window. The camera determines what parts of the animation are visible
on the screen based on its location and other parameters. Intuitively, a
simple animation camera is a hypothetical lens that has the ability to
clearly focus any number of objects, located at arbitrary distances in
its field of view, to a single rectangular screen. Most video game
animation engines use this kind of simple camera abstraction. This kind
of camera is different from a camera lens or the human eye, which focus
on only part of a scene (as determined by the focal length). 3D animated
movies like Ratatouille or Cars, unlike games, need a camera similar to
the human eye in order to shift focus between near and far objects.
For 3D perspective view animations (most contemporary video games),
the Camera is defined by its position and orientation in the animation
(virtual) world. When a 3D Cartesian coordinate system is used, the
camera is defined by 3 vectors - position, direction of view, and upward
orientation vector. Imagine yourself in the center of a room with your
head oriented straight up and looking straight. Your position in the
room, the direction you are facing and the orientation of your head is
analogous to the camera's position, direction of view, and upward
orientation vector. Moving around the room is like changing the position
of the animation camera. Turning your head in different directions -
left, right, up, down, turning around etc. is like changing the
direction of view and upward orientation vector. To understand why a
separate view direction and upward orientation vector are needed,
imagine tilting your head to your left (without turning your neck). The
direction of view remains same in this case, but the upward orientation
is changed. Similarly turning your head to the left without tilting it
causes the direction of view to change, but not the upward orientation.
There is a one-to-one mapping from the triplet {position,
direction-of-view, upward-orientation} to the contents of the field of
view.
In a 3D game, the player's viewpoint moves around the virtual world
when the animation engine moves and orients the animation camera.
Although the camera is not drawn as an object on the screen, it is
nonetheless a worthy candidate to inherit from class GraphicObject. Its
implementation of GraphicObject::draw() will position and orient the
camera in the virtual world, allowing the rest of the visible objects to
be drawn correctly onto the screen.
Here's psuedocode that demonstrates a possible definition of class Camera:
class Camera : public virtual GraphicObject { . . . other protected members . . . Vector position; Vector direction; Vector up; public: . . . other public members . . . };
Frustum Abstraction
The frustum is a concept closely related to the camera. It defines
the clipping planes for the field of view. It is of the shape of a
square pyramid, except part of the top of the pyramid is cut off. To
understand the frustum better, imagine yourself looking at a small
rectangle in front of you, with a much larger rectangle behind this
small rectangle, such that the sides of the rectangles are parallel to
each other. If your eye were the apex of a pyramid with the larger
rectangle as its base, then the frustum is the part of this pyramid
between the small rectangle and the big rectangle. If the small
rectangle represents the video screen as a window into the animation
world, then you can only see things inside the frustum on the screen;
anything outside it is simply out of your field of view. The clipping
planes of the frustum are the planes that contain the faces of the
frustum. Diagram 1 gives a 3D representation of the frustum.
The camera defines all possible points in space that can be mapped to
the screen. The frustum defines all the points that actually do get
mapped to the screen. The process of using the frustum to define these
points is known as frustum culling. The camera is located exactly at the
apex of the pyramid described above, and its direction of view is the
vector from the tip of the pyramix to the center of the base and
perpendicular to the near and far rectangles. The camera's upward
orientation is parallel to one of the sides of the rectangle. You can
see the camera in Diagram 1.
Diagram 1: Frustum 3D Representation
The dimensions of the frustum are usually set only once at the
beginning of the animation application. However, it is one of the inputs
given to the animation engine, and therefore should be abstracted to
keep the engine platform/library independent.
A possible definition of class Frustum is given in pseudo code.
class Frustum { private: double near; // z coordinate of near rectangle with respect to camera double far; // z coordinate of far rectangle with respect to camera double right; // x coordinate of right side of near rectangle with respect to camera double left; // x coordinate of left side of near rectangle with respect to camera double top; // y coordinate of top side of near rectangle with respect to camera double bottom; //y coordinate of bottom side of near rectangle with respect to camera };
Animation Object Abstraction
An animation application needs to do more than just drawing Graphic
Objects. It needs to animate them, which means changing the Graphic
Object in discrete steps. The structure of most animations is a loop of
two steps. The first step is to make all the computations needed for
drawing a single frame of the animation. The second step is to draw the
frame by updating the video memory of the display device. These two
steps repeat until the animation stops. This is essentially the same as
the way a movie is displayed in a theater. The movie projector closes
its shutter and rolls a frame of the film in front of the light source
(step 1), then opens the shutter to let the light of the frame image out
to screen through the lens (step 2). This sequence repeats itself as
nearly 48 times in a second which gives the viewer the illusion of a
continuous movie experience.
A simple abstraction for an animation is to have a class with a
single method on it that simply does the computation for loading the
image of the frame to be displayed next. Although it would be tempting
to call this method draw(), a more appropriate name would be
get_next_frame().
If the film projector above were implemented in software, it would
have one animation object whose method AnimationObject::get_next_frame()
would load the image from the Image Graphic Object into that part of
the application's memory that gets copied to the video memory of the
display device. As another example, the abstract class "MovieClip" used
in ActionScript (a programming language used to creating Flash
applications) is a generic Animation Object abstraction.
An animation application may not want to draw just one Image Graphic
Object in each frame. Instead, it would identify various Animation
Objects and put them together in each frame. For example, the monsters
in the game DOOM each map to an Animation Object. The
AnimationObject::get_next_frame() for each of the monster's Animation
Objects performs actions on the Graphic Object associated with the
monster. These operations might include repositioning, displacing,
rotating, scaling, or even smiling and growling! The DOOM game
application creates the monsters and the DOOM animation engine goes
through the list of all monsters, calling
AnimationObject::get_next_frame() on each of them, thus giving the user
the illusion that they are all moving, turning, attacking or growling.
Let us consider why we need the Animation Object. If you see an
animated movie such as Disney's Cars, you can't help but notice that
every object in the movie is animated individually. The animation engine
that renders each frame must manipulate several objects at once for
each frame of the movie. The animation engine that creates the movie is
asking each of the objects to draw themselves in each frame. For each
frame that GraphicObject::draw() is called on an object, the object will
get drawn, potentially differently. Even though it is easy to relate to
the abstraction of Graphic Object in a single frame, it is a little
more difficult to grasp that the Graphic Object is actually extended
over a range of frames of the movie. The extension of the Graphic Object
across a range of frames is precisely the abstraction of "Animation
Object". To understand this is to take a giant leap into the world of
animation! If you ever get lost in the design of your animation/game
engine, simply read this paragraph again to return to home base.
Now we can derive some essential properties of an Animation Object.
One of the properties of the Animation Object is a Graphic Object or (a
list of Graphic Objects) that it needs to draw across its range of
frames. Because the exact Graphic Objects depends on the nature of the
Animation Object, each Animation Object subclass will define its own
member variables to hold onto the Graphics Objects it needs. To perform
the animation, the Animation Object needs to call GraphicObject::draw()
on each of its Graphics Object members during the
AnimationObject::get_next_frame() method.
There is one property that can be considered to be valid for all
Animation Objects. The idea that there is a range of frames across which
the Graphic Object will be displayed suggests that the range of frames
is an essential property of an Animation Object. For simplicity, the
animation object's first frame can be 0, while its last frame would be
equal to (nframes - 1), where nframes is the total number of frames that
the animation persists. The range will be depicted as {0, nframes - 1}.
This range of frames is used not by the animation engine, but by the
method AnimationObject::get_next_frame(), or by a module other than the
animation engine (like an event handler module) to extend or shrink the
range.
To understand the range of frames better, let's look at an example of
an Animation Object: a moving cube that begins moving at frame X and
stops at frame Y. This cube is an example of an animation object with a
range of frames { 0 , (Y - X - 1) }. For now let's assume that
something manages the animation object's placement in the animation
engine, so that the cube animation is seen on the screen only for the
range of frames specified. When the cube animation object is created,
its frame number would be 0. When this object is given to the animation
engine, the engine simply calls AnimationObject::get_next_frame() on it.
This method sees that the frame number is 0, draws the cube in its
initial position, and increments the frame number. The engine then comes
back and calls AnimationObject::get_next_frame() the second time. Since
the current frame counter has changed to 1 (frame 2), this method draws
the cube in its next position. And so on till the last frame is
reached. Thus the cube gets animated by the engine over its range of
frames. This is illustrated in pseudocode below, wherein the X position
of the cube gets changed over its range of frames.
FrameState CubeAnimationObject::get_next_frame() { if (current_frame displace( Vector( 0, deltaX ) ; } . . . cube->draw(); . . . }
It is very common to find animation objects in
movies/animations/games that repeat an animation effect continuously,
such as for a continuously rotating object, or even a continuously
repeating scene. On the other hand, some animations stop after the last
frame is displayed. In video games, there are other ways an animation
can play out, for example, on pressing a key, a blinking object stops
blinking—a continuously repeating animation that gets stopped in its
tracks. In some cases an animation can play in reverse. For example, in
the game DOOM, when monsters are killed, their bodies disintegrate into
pieces that collect in a pile at the place the monster was killed.
Sometimes, these monsters get revived. In this case, the animation of
the disintegration of the monster's body is played back exactly in
reverse sequence. The above examples illustrate the need for more
abstract operations on the Animation Object. The abstract operations on
an Animation Object can be summarized as follows:
- AnimationObject::get_next_frame() : to play the animation forward (first frame to last)
- AnimationObject::get_previous_frame() : to play the animation in reverse (last frame to first)
- AnimationObject::pause() : to pause the animation
- AnimationObject::resume() : to resume a paused animation
- AnimationObject::rewind() : to rewind the animation and start playing from first frame
One issue that must be addressed is how the animation engine
determines which of the above calls must be made next. For example,
after a call to AnimationObject::get_next_frame(), it is possible that
for a repeating animation, the last frame was reached, and the next call
must be AnimationObject::rewind(). However, if this was not the last
frame, then AnimationObject::get_next_frame() must be called next.
One obvious, but bad, solution is that the animation engine keeps
track of the frame number for each of its animation objects and somehow
knows what to do for each frame of each animation object. This would
completely break the abstract relation between animation engine and
animation object!
A much better alternative is for the animation object to track of its
frames because it knows best what to do for the next frame. In this
case, the animation object must return information to the engine about
what to do next. One way to do this is to return an enumerated value to
represent instructions to the animation engine. The enumeration
FrameState is described in pseudocode below along with a possible
declaration of the abstract class AnimationObject.
enum FrameState { // tells the caller of the AnimationObject method what to do after the call PLAY_NEXT_FRAME, // play next frame (start play or resume paused) PLAY_PREVISOUS_FRAME, // play the previous frame STOP, // stop playing the animation object PAUSE, // pause playing the animation object REPEAT // rewind the animation object }; class AnimationObject { protected: List* graphic_object_list; int start_frame = 0; int total_frames; public: virtual FrameState get_next_frame() = 0; virtual FrameState get_previous_frame() = 0; virtual void pause() = 0; virtual void resume() = 0; virtual void rewind() = 0; };
A real example of an animation engine that implements these
operations is a DVD player. This animation engine has one currently
playing Animation Object (the scene or chapter being played), that
contains one Graphic Object per frame of the animation, i.e. the image
of the scene to be displayed on the screen for that frame. Subsequent
calls to AnimationObject::get_next_frame() on the Animation Object load
the next frame's Graphic Object and call the method
GraphicObject::draw() on it. These Animation Object operations are
performed on the DVD player by pressing keys on the DVD remote control.
When you press the PLAY button on the remote control, you are really
asking the DVD player's animation engine to call
AnimationObject::get_next_frame() continuously. When you press the PAUSE
or STOP button, you are really calling AnimationObject::pause(). The
STOP button additionally stops the animation engine completely. This
additional stop step is not an operation on the Animation Object,
rather, it is an operation on the animation engine itself. The PLAY
button also maps to AnimationObject::resume(), asking the player to
continue calling AnimationObject::get_next_frame() from where it was
paused. The STEP BACK button is calling AnimationObject::rewind() to
rewind to the frame of the Animation Object i.e. the scene being played.
The STEP FORWARD button is really asking the animation engine to
replace the current Animation Object by the Animation Object for the
next scene and start calling AnimationEngine::get_next_frame() on the
new current Animation Object. The REWIND button is asking the player's
animation engine to make calls to AnimationObject::get_previous_frame()
at different frequencies (1x, 2x, 4x and so on). Similarly the FFWD
button is asking the engine to make calls to
AnimationObject::get_next_frame() at those different frequencies.
Whenever you are wondering about interface of the Animation Object
abstraction, think of your DVD player and its remote control!
As another example, the animation camera can also be animated, and
this animation can be implemented via a Camera Animation Object. To
animate the camera, an Animation Object will not have a drawable
(visible) kind of Graphic Object to manipulate, instead, it will have to
manipulate the camera triplet {position, direction, up}. Note that all
such Animation Objects need to manipulate exactly one global camera, as
one animation can have only one camera. (I honestly don't know how a
spider manages to see with 8 eyes, but if the animation engine were a
spider, it would have to do with just one eye. Further, I would
certainly not want to teach animation to a spider.)
Article 2 will how Animation Objects are contained in Layers.
Although the discussion so far implicitly assumes that the animation
engine calls the methods of AnimationObject directly, a better design is
to have the Animation Object contained in a "Layer", and its methods to
be called via the Layer container object. Article 3 will discuss how
layers, layer folders and animation objects are used in an animation
engine. It will also discuss how to break up an animation into layers
for more modularity.
Summary
A Graphic Object is an abstraction for anything that contributes to
the content of a single frame of animation. A Graphic Object can be
visible on the screen, such as objects on the screen, or it is something
invisible, but that affects the rest of the objects that are visible,
such as a Camera Graphic Object. Most Graphic Objects require a
"position" property, while some do not need a "position" such as the
background Graphic Object.
An Animation Object is an abstraction for the extension of a Graphic
Object across a range of frames. An Animation Object contains one or
more Graphic Objects. The animation application can consist of one or
more Animation Objects. The method AnimationObject::get_next_frame()
performs computations necessary to get its list of Graphic Objects drawn
to the display. Other methods on the Animation Object cause it to
pause, resume, rewind or reverse.
An animation application or game implementation can become very complex
simply due to the large number of Graphic and Animation Objects involved, even
ignoring all the game logic and drawing and interaction of each piece of the
animation/game, making an object oriented approach useful for managing the
complexity. The next
article discusses how the concepts of Frames,
Layers and Layer Folders help organize of the multitude of things that need to be drawn and animated in your game.
No comments:
Post a Comment
if You Need Any Help Then Write Us:We Come Back With Solution