Thursday, April 26, 2012

Movimentum - How to test an animation?

Hopefully, we'll create a first, simple animation in a short time. How are we going to check whether the output is correct? Of course, there is always the programmer's answer: Unit testing. However, with graphical objects, our eye (together with the brain) is one of the best gadgets to check for errors, as there are:
  • A "rigid" body that suddenly gets deformed;
  • A moving body that does not move;
  • A body whose image is a mirror of the intended one;
  • ...and many others.
What I want to say is: We need graphical output! We ... need ... movies ...!

This forces me to think about the design of the data flow from the frames back to the things so that we can draw them at the correct place. I scribbled a little bit on the margin of our daily newspaper and came up with the following trivial design for the main loop:

    IEnumerable<Frame> frames = script.CreateFrames();

    foreach (var f in frames) {
        ... Create drawing pane ...

        // Compute locations for each anchor of each thing.
        IDictionary<string,
                    IDictionary<string, ConstVector>>
            anchorLocations = f.SolveConstraints();
         
        foreach (var th in script.Things) {
            th.Draw(drawingPane, anchorLocations[th.Name]);
        }

        ... Save drawing pane to a new file ...
    }

Filling out the missing parts is easy, e.g.:

    var bitmap = new Bitmap(script.Config.Width, script.Config.Height);
    Graphics drawingPane = Graphics.FromImage(bitmap);
    ...
    bitmap.Save(
        string.Format("{0}{1:000000}.jpg", prefix, f.FrameNo),
        ImageFormat.Jpeg);

This requires adding two new parameters to the .config element to define the width and height of the drawing pane and a suitable update of the grammar.

Additionally, I want to test the solver with simple things. For this, I add a simple "bar" thing, which consists of a list of anchor points connected by lines.

    thingdefinition returns [Thing result]
      : IDENT
        ':'          {{ var defs = new List<ConstAnchor>(); }}
        ( FILENAME
          anchordefinitions[defs]
                     { result = new ImageThing($IDENT.Text,
                                ImageFromFile($FILENAME.Text),
                                defs);
                     }
        | BAR
          anchordefinitions[defs]
                     { result = new BarThing($IDENT.Text, defs); }
        )

        ';'
      ;

As you might notice, I have replaced the dictionary for the anchors with a list of the new class ConstAnchor. The reason is that I want the code to remember the order of anchors from the input, so that it will draw the lines always in the same order (in Java, I'd just have to replace Hashtable with LinkedHashtable. In .Net, this is not - yet? - possible).

The Bar thing must be able to draw itself (do you remember your first book on object orientation where they told you that in these modern times, objects are drawing themselves? Now you finally know why you learned this!):

    public class BarThing : Thing {
        public BarThing(string name, IEnumerable<ConstAnchor> anchors)
            : base(name, anchors) {
        }

        public override void Draw(Graphics drawingPane,
                IDictionary<string, ConstVector> anchorLocations) {
            float height = drawingPane.VisibleClipBounds.Height;
            Point[] points = Anchors
                .Select(a => new Point(
                    (int)Math.Round(anchorLocations[a.Name].X),
                    (int)(height - Math.Round(anchorLocations[a.Name].Y))))
                .ToArray();
            drawingPane.DrawLines(new Pen(Color.Orange, 3), points);
        }
    }

Now I can create a small crowbar or hockey stick:

    .config (20, 200, 200);
    B : .bar P = [0,0] Q = [5,5] R = [5,30];

I'd also like to move it so that I can test the three lines of graphics programming from above. For this, we need a few constraints, e.g. to move it diagonally:

    @0  B.P = [60 + .t, 60 + .t];
        B.Q = [65 + .t, 65 + .t];
        B.R = [65 + .t, 90 + .t];
    @10

Creating the frames requires a minimal ability of "constraint solving". I implement this ability with a rather hardwired assignment of values - which still requires an astonishing number of lines:

    public IDictionary<string, IDictionary<string, ConstVector>> SolveConstraints() {
        var anchorLocations =
                        new Dictionary<string, IDictionary<string, ConstVector>>();
        #region ------- TEMPORARY CODE FOR TRYING OUT THE DRAWING MACHINE!! -----
        foreach (var c in _constraints) {
            var constraint = c as VectorEqualityConstraint;
            if (constraint != null) {
                Anchor anchor = constraint.Anchor;
                Vector vector = (Vector)constraint.Rhs;
                double x = Get(vector.X as BinaryScalarExpr);
                double y = Get(vector.Y as BinaryScalarExpr);
                var resultVector = new ConstVector(x, y);
                IDictionary<string, ConstVector> anchorLocationsForThing;
                if (!anchorLocations.TryGetValue(anchor.Thing,
                                                    out anchorLocationsForThing)) {
                    anchorLocationsForThing = new Dictionary<string, ConstVector>();
                    anchorLocations.Add(anchor.Thing, anchorLocationsForThing);
                }
                anchorLocationsForThing.Add(anchor.Name, resultVector);
            } else {
                // We ignore the rigid body and 2d constraints for our testing.
            }
        }
        #endregion ---- TEMPORARY CODE FOR TRYING OUT THE DRAWING MACHINE!! --------
        return anchorLocations;
    }

    #region ----------- TEMPORARY CODE FOR TRYING OUT THE DRAWING MACHINE!! --------
    private double Get(BinaryScalarExpr expr) {
        // Expression MUST be (c + .t), with constant c.
        var lhs = (Constant)expr.Lhs;
        return lhs.Value + _relativeTime;
    }
    #endregion -------- TEMPORARY CODE FOR TRYING OUT THE DRAWING MACHINE!! --------

A little script helps us to run the chain of programs to create the animation. Test.mvm contains the Movimentum script, ffmpeg can be found here:

    bin\debug\Movimentum.exe test.mvm f_
    \ffmpeg\bin\ffmpeg -y -f image2 -i f_%%06d.jpg test.mpg
    test.mpg

And here it is - my first Movimentum animation!



Of course, the important part of the animation computation is pure fake: The coordinates of the anchors are directly assigned, and the rigid body constraints as well as the 2d constraints are completely ignored by this "solver."

But - we are finally at the rim of the canyon.

------------------------------------------------------------------------------------

P.S. For those of who who look closely: I have changed the names of the expression classes because they were very ad-hoc. They are still somewhat strange, but at least each class ending in "ScalarExpr" is actually a ScalarExpr, and each class ending in "VectorExpr" is now a VectorExpr. Likewise, "Unary" and "Binary" are applied consistently.

P.P.S. I am quite unsatisfied with the expression grammar because it is almost impossible to add the five multiplication operators that should be there:
  • Scalar multiplication of scalars;
  • Scalar multiplication of vectors ("inner product");
  • Left and right multiplication of scalar and vector;
  • Outer product of 3d vectors.
I guess that I should have thought about an eighth option in that posting with the severe grammar problem: using semantic predicates. I'll deal with these problems after the canyon ... eh, the constraint solver. So those of you who are interested in language design, stay tuned.

P.P.P.S. There's more to do in the grammar - for example, right now you cannot call an anchor "link" (try it). "pink", however, works perfectly. Not what one would expect.

No comments:

Post a Comment