Half a year ago, I designed a small language that helped me create a few animations of mechanical machinery. For some time now, I have planned to create an animation of a larger apparatus, namely the standard Austrian mechanical railway interlocking type "5007".
However, it became clear very quickly that I needed much more support for this new animation. After all, my very much simplified drawing of this device contains around 80 movable parts, and I am not inclined to draw each one separately and then write computations to move them to the required coordinates! Rather, I wanted a more streamlined process:
- I would draw the whole apparatus in a single "big diagram".
- Then, I would assign a layer to each part (the "Z order").
- Exporting each layer as PNG file, it would now be possible to extract each part from that file if its coordinates were known—which I would indicate by simple diagonal lines that would end up in a "parts description file".
- For assembling movements from these parts, I wanted an extended small language that could do about the following:
- Generate scalar and vector values (like the old animation language).
- Instead of directly calling ImageMagick's convert, simple shortcuts should define the parts' movements. For example, "@ 30" means "rotate by 30 degrees".
- A frame sequence shold be built by assembling separately defined actions than can even run in parallel. For example, one "action" could be the rotation of an electric generator, whereas in another action an electro-magnet would attract its anchor. Defining the actions separately and then running them in parallel allows me to reuse the actions in other scenarios, e.g. when the line from the generator to the magnet is interrupted: Then only the generator would turn, but the anchor would remain stationary.
- ImageMagick's convert is not very fast, especially if large pixel images are involved. Because I want to zoom in to parts in some scenes, my raw CAD diagrams usually have 8410x5194 pixels. However, this means that scenes encompassing many parts are much too fine-grained, and hence the parts computations may take long. Therefore, it is necessary to compute a part at a certain position only once, i.e., the result should be cached somewhere.
- Last but not least, the implementation of the supporting software should be as small as possible.
Just to be clear, this is still an animation engine (and a very small one),
not a physics engine. It is still my problem to assign correct coordinates to each part. And it is a purely two-dimensional animation engine.
In the following, I will describe the new animation process, at least as a reference for me, but maybe someone else is also interested in this "small language adventure." As a running example, I'll use a simple latching mechanism: A lever can push a rod with a cut. When the lever is pushed far enough, a weight will drop into the cut and lock the rod in position, while the lever is pushed back by a spring.
Here is an overview over what will be explained in this posting:
Drawing it
Here is the initial diagram I drew:
Creating the Layer PNGs
In a next step, the elements of the diagram are put into layers. The pink weight will of course be in its own lower layer. But moreover, I also put pieces that touch each other into separate layer files because I can then more easily "cut them out"—I'll show this in a minute. For this contraption, two layers L1 and L2 are sufficient:
- L2 is the lower layer—the pink block must be in it to be behind the green rod.
- L1 is the upper layer—the green rod is in it. All the other parts are put in the layers so that no two adjacent parts are in the same layer:
From this CAD drawing, I now create a PNG for each layer. For this, I use
PDFCreator with a special profile that prints 254 dots per inch. By this, one dot in the PNG corresponds to exactly 0.1mm in the CAD drawing (as 1" equals 25.4mm) so that I can line up coordinates in the PNG and in my CAD drawing more or less easily (the "more or less" comes from the fact that my CAD diagrams have their origin at the lower left corner, whereas the PNGs created by PDFCreator have their origin at the upper left corner).
Unfortunately, the PNGs coming out of
PDFCreator are vertically oriented even if printed in landscape mode—I do not know why, but rotating then by 90° them with Windows Image Viewer is easy enough:
By convention, the names of the layer PNG files also define the Z order when sorted alphabetically. Therefore, parts in layer file L1.png will be drawn above parts from layer file L2.png.
Creating the parts list
Besides the layer PNGs, the
Animate3 compiler needs as its input a parts list (or a few of them). The information collected in this list is, for each part:
- A name for the part
- From which layer file it is to be extracted
- Its coordinates in the layer file.
Actually, the syntax of the file is a little more complicated:
- A first line indicates width and height of the layer PNG files (all must have the same size); and then the coordinates of the PNG files' origin and opposite corner in CAD coordinates. Here is a typical first line:
This corresponds to the following coordinates:
Then, there are the parts lines:
- Lines starting with # and empty lines are ignored.
- A simple part line looks like this:
Groupname partname layerfile x1 y1 x2 y2
The actual part name is the groupname plus the partname, separated by an underscore. If a single dot is given as the groupname, the previous groupname is used. If a single dot is given as partname, it is omitted in the actual part name. This creates somewhat nicer part lists, IMHO.
The layerfile information can be also be replaced with a single dot. In that case, the part is not cut out form a file, does not have a layer and can therefore never be used in a frame. However, this feature is useful to define e.g. scene windows ("viewports") in the CAD drawing whose coordinates can then be used directly in the script—see examples below.
Finally, the two pairs of x and y coordinates define a rectangle which is used cut the part from the layer file. The coordinates given are CAD coordinates which are translated to pixel coordinates using the six values given in the first line of the parts file. I'll show in a moment how I get these CAD coordinates somewhat efficiently.
- Besides using a simple part line, it is alternatively also possible to define a part with a pivoted part line. It has four more coordinates x3, y3, x4, and y4, which define a pivot point in a somewhat intricate way: Two of the first four coordinates must be equal to two of the last four coordinates—and exactly that equal pair of coordinates defines the pivot point. The remaining four coordinates define the surrounding rectangle. The reason for this strange definition will become clear in the example.
Now, let us create the parts file for the demo machine. First, we write a simple text file that contains the header line and then parts' names and the layers:
8410 5941 0 594 841 0
rod . L1.png
. spring L2.png
. springblock L1.png
lever . L2.png
. washer L1.png
. spring L2.png
. springblock L1.png
. stop L1.png
weight . L2.png
view . .
The first three parts will be called "rod", "rod_spring" and "rod_springblock" when we use them in an animation.
Now, we have to add the coordinates defining the parts. For this, I add a new layer to my CAD drawing and draw diagonal
anchor lines "covering" each part, using the CAD program's crosshair. The following screenshot shows three such anchor lines in light green, and you can see that I drew them quite roughly—from somewhere "left above" the part to somewhere "below right." As long as no other part is also in the rectangle thus defined, this identifies each part uniquely:
At least for our lever, we need a definition of the pivot. For this, we draw
two anchor lines that meet at the pivot. As explained above, their common point will define the lever's pivot:
Here is a complete coverage of all parts with green anchor lines. Both springs have also gotten pivot points (at their stationary end), and I have added a long line traversing all parts which can be used to define the view (we'll see later how):
How do we get the lines' coordinates into the parts file? This depends on the CAD tool—for example, it might have an export function that can be used on the parts lines' layer. My CAD program doesn't, so I had to extract this information with a Windows script. I use
AutoHotkey with a simple script which I use as follows:
- I open the parts file with Notepad.
- One after the other (in the order in the parts file), I select the lines in the CAD drawing while having open an "object information" dialog. Hitting "windows space" starts an AutoHotkey script that selects and copies each coordinate, activates the notepad window and pastes the copied coordinate into it. For objects with pivots, I use a "go back to previous line" autohotkey script between copying the data of the two adjacent lines. After some practicing, one gets quite fast with copying over the lines' coordinates into the parts file.
Here is the resulting completed parts file, which we save in
AnimateDemoParts.txt. It is untypical that all the coordinates are integral numbers—the reason here is that the model is so simple that I only put the parts at grid points in the CAD drawing:
8410 5941 0 594 841 0
rod . L1.png 70 480 330 420
. spring L2.png 420 440 310 480 430 420 420 440
. springblock L1.png 410 480 450 420
lever . L2.png 50 550 90 270 90 270 110 240
. washer L1.png 110 390 130 320
. spring L2.png 220 350 110 390 230 330 220 350
. springblock L1.png 210 390 250 330
. stop L1.png 40 390 70 330
weight . L2.png 310 510 240 390
view . . 20 560 460 230
Writing an animation
The major feature of the new animation process is a new language whose features I have outlined above. For obscure reasons, I call it "Animate3". The following sections explains this small language with some examples. Later, I might write a "reference manual" for this small language.
An Animate3 script is compiled to a batch script (Windows .bat file or something similar) that can then be run to create the actual frames of the animation.
A short overview
The most important commands in a script emit one or more frames. A simple command might look like this:
& 50 lever @ . . !lever_A
This is read as follows:
Create (&) 50 frames by rotating (@) part lever around its pivot point (. .) by !lever_A degrees at its original position in the CAD drawing.
So that the lever actually rotates, the value
!lever_A must be defined so that it is incremented in each frame. This is done by the command
!lever_A 0 10 .
which is read as follows:
Start !lever_A at value 0 and increment it by 10 after each frame.
(Ignore the dot at the end for the moment). For the
& command to work, we need to read in a parts file with a
lever part beforehand. This can be done with
< AnimateDemoParts.txt
Finally, we need to define a view on all the parts before we start creating frames. Here is a possible definition:
= . 1000 1000 3000 2500 frame!lever_A.png 600 .
The first four coordinates define the frame window, the next one is the name of the frame file (using
!lever_A ensures that each frame file gets a different name!), and the last number is the pixel width of the resulting frame files. Because this is one fifth of the width given on the left of the frame file name, the resulting frames are scaled down from the input PNG files—i.e. the layer files—by a factor of five.
As long as the three last commands precede the frame generating
& command, this will emit the 50 frame files:
< AnimateDemoParts.txt
!lever_A 0 10 .
= 50 1000 1000 3000 2500 frame!lever_A.png 600 .
& 50 lever @ . . !lever_A
I hope this looks simple enough.
Unfortunately, this script does not work as intended: It does create 50 frames, but all are written into the same file
frame500.png! To understand why I need to explain the concept of "actions."
Actions and frames
Animate3 supports parallel actions. This means that at one time, many actions can contribute to a frame sequence—one might create a rotating wheel at the bottom of the sequence, another might insert the graphics for a swinging pendulum, a third one might create a blinking arrow. For creating these image parts, actions use & (or $) commands, as well as an = command for defining the "viewport", i.e., the extent of the created scene. Each & or $ command contributes its parts to a number of images (50, in the example above), and each = command sets the viewport for some number of frames. When an action has exhausted the frame count on a command, it proceeds to the next command—which might again be a frame creation command looping for some frames; or it might be another command, which is immediately processed: A parts file is read; or a value generator gets new parameters.
Somewhat more precisely, frame creation proceeds as follows:
- First, all active actions together create a single frame—each & and $ command create sub-images for each part to be placed, these images (or parts) are then stacked above each other according to their layer order and finally written into the file specified in the currently active = command.
- Afterwards, all actions let their value generators increment their values.
- Finally, actions that have exhausted their current frame command (& or $ or =) proceed to the next frame command. On their way, they might execute other commands that create or modify value generators; read parts files; and/or start sub-actions.
"Creating a frame" means, in Animate3, "emit a shell command (that will actually create the frame) into the output batch file." In many cases, these commands are calls to ImageMagick's
convert executable; however, one may use other commands if one so desires. The idea is to separate the script interpreter from the actual (long running) graphics work so that the script can be debugged beforehand.
Actually, there is some fine print involved in the process above: E.g., parent actions can cut off their child actions, and some actions might loop more than once, and some might even loop endlessly as long as no parallel action cuts them short. But this will be explained later.
Now, this algorithm explains why the script above does not work: The sequential action
first runs the = command 50 times, which creates no image (because there is no & or $ command running), but increments the
!lever_A value to 500 (50 increments of step size 10). Thus, the frame file name ends up at
frame500.png.
Then, the & command starts to emit frames—but they will now all end up in the same file!
Obviously, what we need is that the increments for the frame file name and the frame creation occur in parallel. We will see how this is done in the next section.
The example device starts to move
Let us create a first small animation for a single part of the example device. I will introduce here the necessary commands on the fly, without much general explanations. If you are interested in more details, you can look up the command reference (when I post it). This will allow us to concentrate on the necessary setup script—later, we will add more moving parts. The moving part in this simple scenario is the lever, which simply rotates about its pivot point. Here is the script for a simple action that creates 50 frames where the lever moves by 10 degrees from frame to frame:
Scene: A lever rotates about its pivot
!lever_A 0 10 360
& 50 lever @ . . !lever_A
In contrast to the previous script, I used a
modulus on the
!lever_A value. Thus, the value will get the values 0, 10, 20, ... 350, but the following values will be 0, 10 etc. again. This is not strictly necessary here—after all, a rotation by 360 degrees or 370 degrees etc. is perfectly legitimate—, but it allowed me to introduce this concept.
We store this text in a file called
script_scene.an3. However, we need some more things to get the whole animation: The parts file, and a view on the scene, and finally some shell command that assembles all the frames into a movie.
Here is the "camera action." Actually, in this scene (and many technical animations), the camera does not move at all—so why do we need a separate action for it? Well, as we saw in the previous section, there
is one thing that "moves" even in simple "linear" scripts, namely the frame number: We must write each frame to a different result file! Here is the action that allows the camera to capture up to 10000 frames in files
f_10000.png to
f_19999.png. The viewport of the camera (what we see from the whole drawing) is defined by using the parts coordinates of the fictious part "view"—remember that this was the diagonal line we drew over all parts exactly to define this view. After the filename, we define the pixel width of the resulting frames (600), but do not explicitly specify the height (.) which results in frames of the same aspect ratio as the view definition:
Camera definition
!Frame 0 1 9999
= . view:x view:y view:w view:h result\f_1!Frame.png 600 .
We store this text in
script_camera.an3.
Finally, we need a master script that executes the previous two scripts in parallel. Here it is, with the following steps:
- The emitted shell commands are collected in file script1.bat.
- The parts file is read.
- A shell command is emitted that creates a directory for the created frame files.
- Then, we start the two actions in parallel. The scene action (script_scene.an3) is performed exactly once (this is the number 1), whereas the camera action (script_camera.an3) is performed "as long as needed" (the dot after this action). By this, the camera is "switched off" after 50 frames, even though it could perform for 10000 frames.
- Finally, we emit an ffmpeg command that converts the frames to the animation file script1.mp4.
Script 1 - overall control
~ script1.bat
< AnimateDemoParts.txt
$ 1 if not exist script_frames mkdir script_frames
| . script_scene.an3 1 script_camera.an3 .
$ 1 ffmpeg -f image2 -r 12 -i script_frames\f_1%%04d.png \
-vcodec libx264 -pix_fmt yuv420p -y script1.mp4
Creating the first animation
We now must compile the script into a batch file and execute it. The current Animate3 compiler is a 1000-line
Gnu-AWK script. In order to run it, one must first install
Gnu-AWK in version 3.1.6 or higher. Then, under Windows, frame creation can e.g. be done with these 4 lines of code:
gawk -f animate3.awk %1.an3
if errorlevel 1 goto :eof
call %1.bat
%1.mp4
When we save this in
compileAndRun.bat and call it for our script with
compileAndRun script1, we get our first animation:
Moving the whole apparatus
Let us now create an animation of all parts where the lever is pushed to the right until the pink weight falls into the rod's cut and locks it, while the lever returns to its original position. During this, we will have to rotate (the lever), shift (the rod and the weight), and compress (the two springs). Let us structure the scene into three parallel actions (not counting the camera action):
- One action handles the lever and its spring.
- Another action handles the rod and its spring.
- A last action deals with the weight.
One can also view these actions as "models:" Each model describes some movements that are tightly linked, i.e., controlled by the same value generators. Different models, on the other hand, are controlled by different values. In our case,
- all parts in the lever action are controlled by a single value "angle of lever";
- all parts in the rod action are controlled by a single value "displacement of rod";
- and the single part in the weight action is controlled by a single value "position of weight."
Moving the lever
Let
!lever_A be the angle of the lever. Then, the lever's position can simply be described as
& . lever @ . . !lever_A
The number of frames create by
& is specified as a dot which means "infinitely many frames." Of course, the number of frames must be limited by some other means elsewhere—this will be the job of the control script. This is standard for setting up a model.
What about the "washer" (the piece between the spring and the lever)? It needs to move horizontally for a distance that is proportional to the sine of the lever's angle. There are two ways to arrive at the correct value:
The first possibility is to compute the shift via the formula
r ⋅ sin α. In this case,
r is 900. Why? The distance between the lever pivot and the pushing disc is 90mm in the CAD drawing, and as we created the layer PNG files with 10 pixels per millimeter (or 254dpi), the 90mm correspond to 900 pixels in the layer PNGs:
& . lever @ . . !lever_A \
lever_washer > { 900*sin !lever_A } 0
When a frame is created, all values are interpolated in the frame command (in this case, only
!lever_A will be replaced). Then, each formula between curly braces {...} is evaluated. Formulas are quite restricted, but they support all important operators: +, unary and binary –, * (times), / (divide) and \ (modulus), parentheses, and sin (sine) and cos (cosine).
The other method uses the angle rotation of a vector. We let an auxiliary vector
%lever_D of length 900 rotate like the lever. However, as we are only interested in relative distances, we pivot
%lever_D at the origin:
%ORIGIN 0 0 . .
%lever_D 0 900 %ORIGIN !lever_A'
The
x coordinate of this vector is now exactly the shift that we need for the washer, so we can extend our & command:
& . lever @ . . !lever_A \
lever_washer > %lever_D:x 0
In this example, I'll keep to the first version that uses the formula.
The last moving part we have to handle is the lever's spring: It must shrink when the lever is moving to the right. The shrinkage factor can be computed as (length of spring – shift of left end of spring) divided by the length of the spring. As this is the standard computation for scaling, the two values can directly be put into the scaling operator. The length of the spring must be determined in the CAD drawing (it is 100mm or 1000 pixels), whereas the shift is the same as the washer's. As we do not want any scaling in y direction, we fill in constants that do not lead to a division by zero:
& . lever @ . . !lever_A \
lever_washer > { 900*sin !lever_A } 0 \
lever_spring ^ . . 1000 { -900*sin !lever_A } 1 0
Finally, let us add the fixed blocks on the left of the lever and on the right of the spring:
# Parameters:
# !lever_A = angle of lever
& . lever @ . . !lever_A \
lever_washer > { 900*sin !lever_A } 0 \
lever_spring ^ . . 1000 { -900*sin !lever_A } 1 0 \
lever_springblock \
lever_stop
We now have created a small, but definitely non-trivial animation model. Let us store it in a file
script_lever.an3. Before we continue with other actions, we certainly want to run this script and view its result.
As in our first test script, we need some infrastructure. We copy the previous controlling script and change a few names:
Script 2 - test animation for lever
~ script2.bat
< AnimateDemoParts.txt
$ 1 if not exist script_frames mkdir script_frames
... run script_lever.an3 ...
$ 1 ffmpeg -f image2 -r 12 -i script_frames\f_1%%04d.png \
-vcodec libx264 -pix_fmt yuv420p -y script2.mp4
For actually running something, we must decide how the parts should move in our test. In this small test animation, let us first turn the lever by an angle of 30 degrees in 12 frames and then move it back to the vertical position.
There are three crucial elements missing: First, we must define the value generator
!lever_A somewhere. Then, we must create calls for the parts and, as above, for the camera. Last but not least, we must limit the number of created frames. Here is a script that has all these elements:
Script 2 - test animation for lever
~ script2.bat
< AnimateDemoParts.txt
$ 1 if not exist script_frames mkdir script_frames
!lever_A 0 2.5 .
| 12 script_lever.an3 1 script_camera.an3 .
!lever_A . -2.5 .
| 12 script_lever.an3 1 script_camera.an3 .
$ 1 ffmpeg -f image2 -r 12 -i script_frames\f_1%%04d.png \
-vcodec libx264 -pix_fmt yuv420p -y script2.mp4
Moving the rod
Moving the rod is much easier. We use a scalar value generator
!rod_S for the rod's shift distance:
# Parameters:
# !rod_S = horizontal displacement of rod
& . rod > !rod_S 0 \
rod_spring ^ . . 1000 {-!rod_S} 1 0 \
rod_springblock
When we put this into a file
script_rod.an3, we can add some corresponding movements to our test animation:
...
!rod_S 0 75 .
| 12 script_rod.an3 1 script_camera.an3 .
...
Dropping the weight
Finally, we need to drop the weight. This is of course even easier than moving the rod:
# Parameters:
# !weight_S = vertical displacement of weight
& . weight > 0 !weight_S
After storing this in
script_weight.an3, we can add some test animation, too:
...
!weight_S 0 30 .
| 12 script_weight.an3 1 script_camera.an3 .
...
Playing this test animation already gives us some good feedback:
The complete machinery at work
Now let us create the animation for the complete machine. Here is a rough script of what we want to show:
- First, the lever and the rod move to the right until the weight is free.
- Then, the weight falls. During this, both lever and rod still move a little bit to the right.
- The lever reverses its direction, and both lever and rod move backwards.
- However, the weight's red nose catches the rod, so the rod remains stationary.
- The lever continues its way back back.
- When the lever bumps at its stop, it stops, and everything remains at rest for some time.
Here is the script—it reuses the three partial models we have already created. We run all four scripts (camera, lever, rod, and weight) in parallel many times. This is a typical "scene script:" The same parallel actions are run for a number of scene parts, but in between, we change the values and increments of the controlling variables to move the parts according to our script. To avoid the repetition of the full | command, there is a short version that consists only of a bar and a framelimit—which means that the previous | command is to be repeated with the new framelimit.
In the following scene script, I found one number, namely the 1.15 degrees/frame of the lever, by trial and error (instead of measuring and solving an equation). Other numbers, namely the 1800 in the rod shift computation and the weight's 100 pixels/frame free fall, come from distances in the CAD diagram:
~ script3.bat
< AnimateDemoParts.txt
$ 1 if not exist script_frames mkdir script_frames
$ 1 del /q script_frames\f_3*.png
--------------- Lever at rest --------------
!lever_A . 0 .
!rod_S . 0 .
!weight_S 0 0 .
| 6 script_camera.an3 . script_lever.an3 1 \
script_rod.an3 1 script_weight.an3 1
--------------- Lever moves right ----------
!lever_A 0 1.15 .
!rod_S {1800*sin !lever_A}
| 12
!lever_A . 1 .
!weight_S 0 100 .
| 2
!weight_S . 0 .
| 2
--------------- Lever moves left -----------
!lever_A . -1 .
!weight_S . 0 .
| 3
!lever_A . -1.15 .
!rod_S . 0 .
| 13
--------------- Lever at rest --------------
!lever_A . 0 .
| 12
$ 1 ffmpeg -f image2 -r 12 -i script_frames\f_3%%04d.png \
-vcodec libx264 -pix_fmt yuv420p -y script3.mp4
And here is the result: