Skip to content
April 24, 2018 / gus3

Scripting an Animation

I remember watching the science programs of the 1980’s, such as Nova, National Geographic, and Cosmos. Their occasional computer animations, bringing to life some dry concept of mathematics or physics, were an endless source of awe. Partly, it was jealousy that someone had the equipment to make the animations real; also, some individual had the imagination to lay out what such an explanation should look like.

Fast-forward 30-some years, and I have the tools, the hardware and software, to do basic animations. But in typical Unix fashion, it’s up to me to put them together, to get them to work the way I want. Add to that the KISS principle, and it’s a simple matter of shell scripting!

Prerequisites

I run a full installation of Slackware 14.2 (upgraded to -current), so I already have “gnuplot” and “mencoder” installed. If you are running some other Linux distribution (or a Unix descendant), you may need to take steps to install them, along with their dependencies. If you are not the administrator for your system, this will mean groveling before your sysadmin, preferably with Chinese food or pizza.

The initial experiment: a 1-radian angle

Most of us are familiar with degrees: 30, 60, 90, 120, 180, 360 degrees. It’s convenient to use big-enough integers in daily speech. But radians? That’s a whole different thing.

A radian, as an angular unit, corresponds to the angle between a circle’s origin and the chord with the length equal to the circle’s radius. A circle’s circumference is 2πr, so a full circle is 2π radians. A semi-circle is π radians. But what about 1 radian? How to visualize that?

Well, 360 degrees is 2π radians, so 1 radian is 360/2π, or 57.2957795 degrees. A chord of  57.2957795 degrees, is as long as the circle’s radius. It’s that simple.

Why a radian?

There are two parts to this explanation. For the first, the sine function of any number has a definite value, even if we’re talking about the sine function of 377.4 ships on the Mediterranean Sea. The sine of a number is independent of its units. It doesn’t matter that it’s “ships on the Mediterranean Sea”; the sine of that number is just the sine of 377.4.

Why are radians important?

Trigonometric functions based on radians become delightfully simple to calculate. Also, given that the exponents in the calculation (a Taylor series) are whole numbers, it’s possible to calculate sines and cosines with a minimally capable computation system. The precision available using a Taylor series is arbitrary; a thousand decimal digits would take longer than ten, but would require no modification to the actual algorithm.

Some notes about frame rendering

The “mencoder” program accepts wildcard notation for files containing individual frames. This means, once all the frames are rendered, “mencoder” will pull them all into a video, using the right wildcard pattern.

Which gives rise to another point: rendering the seventh frame doesn’t depend on rendering the previous 6. At least in this case, the rendering order doesn’t matter. The first task is to render the frames; the final task is to make a video from them.

The independence of the frames means that they can be rendered sequentially, in parallel, or in any general random manner. The task is great for a multi-processing environment (note to self: create a rendering farm, using Bash?).

Alright, already. Show me the code!

Well, this takes a bit more explanation. The first function in the script does the rendering, but

  1. The function calls gnuplot to do the rendering,
  2. using a gnuplot script,
  3. with the appropriate function parameters substituted into the script,
  4. and using the function parameters to generate a filename for each rendered frame.

So simple, right? Not really; this was the beating heart of the script, and it took more than 1, but fewer than 10 attempts to make it work. Quoting, parameters, variable expansion all challenged me as I tried to get a single frame, or a few disparate frames, rendered into files.

Eventually, I got it all to work correctly, meaning the shell script rendered all frames with no reported errors, and the rendered frames got turned into a movie. Here’s the working script:

#!/bin/bash

function draw_frame() {
	ITER=$1 ; ANNOT=${2:-""}
	# first, constants: one radian X and Y
	XRAD=.54030230586813971740
	YRAD=.84147098480789650665
	# conversions
	RADDEG=57.2957795
	DEGRAD=.017453292
	# and of course
	PI=3.14159265358979

	# here's the heavy lifting
	echo "set term png size 640,640
	set output
	set xrange [-1:2*$PI*$YRAD]
	set yrange [-1:2*$PI*$YRAD]
	set object 1 circle at 0,0 size 1 arc [0:$ITER*$RADDEG]
	set object 2 circle at 0,0 size 0.95 fs solid 1.0 fc bgnd
	set object 3 polygon from 0,0 to $XRAD*$ITER,$YRAD*$ITER to 0,0
	set label \"$ITER\" at 1,1
	set label \"$ANNOT\" at 2,0
	plot -2" | gnuplot > frames/frame-$ITER.png

	# if annotated, pause for 2 seconds
	if [ ! -z "$ANNOT" ] ; then
		for i in `seq -f "%02.0f" 2 50` ; do
			ln frames/frame-$ITER.png{,-$i}
		done
	fi
}

# initializer: make or clean frames/ directory
if [ -d frames ] ; then
	rm -rf frames
fi
mkdir frames

for RAD in `seq 0 5` ; do
	( for CRAD in `seq -f "%02.0f" 0 98` ; do
		draw_frame $RAD.$CRAD
	done ) &
done
( for CRAD in `seq -f "%02.0f" 0 29` ; do
	draw_frame 6.$CRAD
done ) &
wait

# re-render a few frames, pausing for annotations
draw_frame 1.00 "chord length = radius, 1 radian"
draw_frame 3.14 "pi radians, 180 degrees"
draw_frame 6.28 "circumference = 2 * pi * radius"

# now, animate them. fps is arbitrary but shouldn't be > 30
mencoder "mf://frames/*.png*" -mf fps=25 -o output.mp4 -of lavf -ovc lavc -lavcopts vcodec=mpeg4

You can copy the above script, or download it from my Git space.

Analyzing the script

For 40 or so lines of actual code, this script accomplishes a lot, with some outside help. Let’s take a look at how the work gets done.

The function draw_frame() renders individual frames, with some extra code to insert identical frames, to create a 2-second pause, if a frame’s annotation requires some reading time. (The duplicate frames take up no extra file space; it’s a trick that allows multiple Unix filenames to point to the same file data.)

After draw_frame() comes the main script body. The first task is to clean out the old stuff from previous runs, then make sure the directory for rendered frames exists. That stuff is trivial.

But then come the high-level loops, executed in parallel, 7 loops in total. The loops run in parallel, but their internal tasks run sequentially. The high-level loops are rendering 100 frames of animation, except for the one that renders only the last 29 frames. But like I said earlier, all these frames are rendered independently; frames 600 to 628 don’t depend on frame 0, or any other rendered frame.

I decided to approach the task via “centi-radians,” using $RAD and $CRAD for loop management, then combining them into $RAD.$CRAD to represent 0.00 to 6.29 radians, passed as the first parameter to draw_frame(). The script didn’t really need to understand radians, in order to use them in unit conversions (look at the constants in the draw_frame() function’s preamble).

Gnuplot is very versatile when it comes to math operations, so the script can leave conversions between radians and degrees to Gnuplot, rather than handling them in the much-less-efficient Bash.

Once all the frames are rendered (and the “wait” command is finished), three frames are re-rendered with annotations. When the draw_frame() function receives a second parameter for annotation, it also creates enough links to the rendered frame to pause the resultant video for 2 seconds. In this case, 25 frames per second requires 49 duplicate frames, numbered from 2 to 50.

The final line of the script turns the individual frames into a single video file. The file names of the frames have been mapped appropriately, as “frame-N.NN.png” for regular frames, plus a few named “frame-N.NN.png-PP” for inserting the paused annotations. At this point, the important thing to bear in mind is that the file names sort correctly, plus they will all be picked up in the pattern matching used by “mencoder”.

There is one flaw that, so far, is beyond my control: it’s clear the the circle isn’t quite circular, even with a 1:1 image size ratio. But that doesn’t detract from proving how 40 lines of shell script can create an animation.

Very good, yes, but what’s the point?

The point is to do it! The script let me visualize something I wanted to understand. After I’d applied Occam’s Razor, the task at hand became a simple matter of Unix philosophy: put basic parts together to create some greater thing.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: