The previous tutorial's source-code included animation, but I said that would be a topic for another day. Well, today is that day. I'm going to go through the code I added to animate the cube. 

IMPORTANT: While this tutorial is about Warp3D Nova, the animation code below is almost 100% generic. You could reuse it to perform 2D animations using the graphics library, or OpenGL.

Animating the Cube

Animation involves rendering multiple images (a.k.a., frames) per second, each with the scene frozen at a slightly different point in time. Display enough frames per second, and they'll blend together in your mind, forming a fluid motion picture (lookup "persistence of vision" for more).

The animation we're going to do will be simple, smooth, and completely independent of how fast the machine is. That is, the cube will rotate at the same speed no matter what hardware you use. So let's get started...

I recommend having the previous tutorial's code handy while you go through this tutorial. That way you can see the code changes in a wider context. Here's a direct link: W3DNovaTutorial6.lha

Animation Timing

First up, let's add code to measure time. That's how we'll make the cube rotate at the same speed regardless of how fast or slow the computer is. This code is generic, so add it to Util.c. In Util.c, add the following:

#include <stdlib.h>

Now add the getSysTime() function:

uint64 getSysTime() {
    struct timeval tp;
    struct timezone tzp;

    gettimeofday(&tp,&tzp);
    return ( (uint64) tp.tv_sec * 1000000 + (uint64) tp.tv_usec);
}

Also add a function prototype to the Util.h header so it's usable from other source files:

/** Gets the system time in microseconds (used for calculating the animation.
 */
uint64 getSysTime();

The Main Loop

Back to the main() function. There are a few changes to be made. For starters, the window needs to be refreshed continually. So, find the line that sets refreshWindow to FALSE, and change it so that refreshWindow is always TRUE:

			refreshWindow = TRUE; // Keep true because we're animating

Next, we want the main loop to execute continually instead of waiting for user input. So, the WaitPort() call needs to go. This is replaced with code that checks for user events but doesn't wait. Confusingly, the AmigaOS function to do this is called SetSignal(). SetSignal() can both set signals and get their current states, which is exactly what we want to do. Here's the updated event handling code (in full):

		// Check for an event that we need to respond to
		struct Window *window = renderContext->window;
		ULONG signalMask = (1L << window->UserPort->mp_SigBit);
		if((IExec->SetSignal(0L, signalMask) & signalMask) != 0) {
			struct IntuiMessage *msg;
			while ((msg = (struct IntuiMessage *) IExec->GetMsg (window->UserPort))) {
				switch (msg->Class)
				{
				case IDCMP_NEWSIZE:
					// A resize occurred, need to refresh the window
					resizeWindow = TRUE;
					break;
				case IDCMP_REFRESHWINDOW:
					refreshWindow = TRUE;
					break;
				case IDCMP_CLOSEWINDOW:
					// The user says quit!
					quit = TRUE;
					break;
				default:
					; // Do nothing
				}
				IExec->ReplyMsg((struct Message *) msg);
			}
		}

With that out of the way, we can finally do the actual animation. Just above the main loop (so above the while(!quit)), add two new variables for tracking time:

	uint64 timeStamp = getSysTime();
	uint64 prevTimeStamp = timeStamp;

Now scroll down to the if(refreshWindow) section, and insert the following code:

			// -- Animate the cube --
			// How long since the last frame?
			timeStamp = getSysTime();
			double elapsedTime = (double)(timeStamp - prevTimeStamp) / 1e6;
			
			// Now rotate the model
			kmMat4RotationX(&rotMat, ANG_VELOCITY * elapsedTime);
			kmMat4Multiply(&modelMat, &rotMat, &modelMat);
			
			// Prepare for the next round...
			prevTimeStamp = timeStamp;

This should be put before the code that performs the rendering (including the code that updates the shader data). The code works as follows:

  • Calculates how much time passed since the previous frame (elapsedTime)
  • Rotates the model by ANG_VELOCITY * elapsedTime. This is done by creating a rotation matrix (kmMat4RotationX()) and then multiplying the cube's model matrix with the rotation matrix (kmMat4Multiply())
    NOTE: ANG_VELOCITY is a constant defined as follows:
    // The cube's angular velocity
    #define ANG_VELOCITY 0.75f // radians/s
  • Saves the current timeStamp to prevTimeStamp in preparation for the next frame

Congratulations, the cube now moves!

Limiting the Frame-Rate

The code above will work, but it'll render hundreds or even thousands of frames a second, almost all of which you'll never see. That's a pointless waste, so let's limit the frame-rate to what your monitor can handle.

Insert the following above the rcSwapBuffers() call:

			// Limit the frame-rate to the monitor's refresh rate
			rcWaitVBlank(renderContext);

And, create the rcWaitVBlank() function in Context.c. It's simply a WaitBOVP() call, which waits for the monitor's vertical blanking period (a period between frames):

void rcWaitVBlank(RenderContext *renderContext) {
	IGraphics->WaitBOVP(&(renderContext->window->WScreen->ViewPort));
}

Remember to also add its prototype to Context.h, so that rcWaitVBlank() can be used from other source files:

/** Waits for the VBlanking period.
 *
 * @param renderContext pointer to the context to destroy
 */
void rcWaitVBlank(RenderContext *renderContext);

Conclusion

The code changes above are the basics needed for animation. The changes were:

  • Measures elapsed time
  • Updates the cube's pose by the amount it moved during that elapsed time
  • Renders the new frame with the updated pose
  • Waits for the vertical blanking interval before working on the next frame (a simple technique to limit the frame-rate to what the monitor can physically display)

That's the basics of animation. Sure, there are more complicated schemes (e.g., physics engines work better with a constant elapsed time), but they're all variations of the fundamentals we just covered.

As usual, feel free to contact me here or post to the comments section below if you have any comments or questions.