Home About Eric Topics SourceGear

2006-08-02 15:21:02

WPF Animation

So when my woodworking app displays a join (two pieces of wood being glued or fastened together), I figured it would be nice to animate the display to make it easier to see how the join is done.  And since WPF is supposed to have cool animation features, why not try it?

Until now, my experience with WPF has been so productive as to be surreal.  With no prior experience, I can often get something new working with WPF in the amount of time it takes for my daughters to watch an episode of Sponge Bob.

However, animation wasn't quite that simple.

Frames

My first attempt was unsuccessful because I was going about things the wrong way.

Since I had two objects which I want to move back and forth, I figured I would just create ten frames by moving the objects apart a little bit further on each frame.  The moving of the objects was done using a simple 3D translation in my solid modeler.  Each time I moved the objects, I converted my solid model into a triangle mesh suitable for display.  The result is that I would end up with ten meshes, one for each frame.  Showing the animation would be a simple matter of showing each mesh in succession.

So I wrote the code to get my solid modeler to produce the ten sets of triangles, but things got difficult when I tried hooking this up to WPF's animation stuff.  Life would have been simpler if I had looked at the WPF animation samples or documentation first.  I assumed WPF would let me register a delegate or something which would get called when it's time to flip to the next frame.  In that callback, I would switch my viewport3d to the next mesh.  But that's not how WPF animation works.

The WPF animation stuff wants to tweak a property of an object.  You tell it which object and which property.  You provide it a range of values for that property and specify the amount of time it should take to get from the beginning to the end.  For example, you might say "cycle the Angle property from 0 to 360 over a 7 second interval".

The absence of the notion of frames is a feature, not a bug.  Somewhere deep in the bowels of System.Windows.Media.Animation there is some code which is making smart decisions about how many frames to use depending on how fast my hardware is.

However, this design certainly suggests that the property to be animated needs to be numeric, like a double or an int.  If I'm allowed to pass in a collection of mesh objects, I don't see how.

I tried to create a wrapper class with a FrameNumber property so I could just use an Int32Animation to flip through the frames by number.  But at this point it really seemed like I was swimming upstream.  So I decided to step back and rethink.

Transforms

WPF 3D has a really nice habit of using matrix transform objects all over the place.  Model3DGroup has a Transform object.  So does GeometryModel3D.  The cameras and lights have one too.  These things make it easier to scale, rotate and translate things without changing the actual geometry.

And they were designed to be animated.  So...

Sweet.  Everything worked perfectly, except for one silly hack.

Only One Property

The only problem here is that I don't like the way TranslateTransform3D works.  It has three properties:  OffsetX, OffsetY, and OffsetZ.  Since I happened to be animating something moving parallel to an axis, this was not really a problem.  But what if I wanted to be moving something in a straight line where X, Y and Z were all changing by different amounts?  I suppose I could create three separate DoubleAnimation objects, but that feels like a silly hack.

What I really want is a different translation class.  Instead of three separate offsets for X, Y and Z, what I want is a unit vector and scaling factor.  The unit vector determines the direction.  The scaling factor determines how far in that direction to translate.  Then I could hook up a DoubleAnimation object to the scaling factor property.

So I decided to just create the class I need.  All I have to do is subclass TranslateTransform3D and provide two new properties to do what I want.

Oops!  That class is sealed.  I wonder why?

Oh well, I guess I can just move up a level and subclass from AffineTransform3D.  It'll be a little more work, but I'll live.  So:

  public class VectorTranslateTransform3D : AffineTransform3D

Ouch!  The compiler says I have to implement CreateInstanceCore, AddRefOnChannelCore, ReleaseOnChannelCore and GetHandleCore.  What the heck is all this stuff?  The documentation is silent.  I Googled "AddRefOnChannelCore" and found this post by somebody at Microsoft named Adam Smith who explains that subclassing from these classes is a bad idea.  Bummer.  I guess I'm stuck with my silly hack for now.

(If anybody in Redmond is reading this, please accept my vote for VectorTranslateTransform3D, with two properties:  a Direction vector and a Distance double.  Thanks!)

Wait -- One Last Try

After giving up on the idea of having my animation work on one property instead of three, I couldn't sleep.  So I decided to try one last thing:  If inheritance won't work, maybe encapsulation will.

Success!  Here's the code for my VectorTranslateTransform3D class:

    /// <summary>
    /// This class encapsulates a TranslateTransform3D object but
    /// presents a different interface.  Instead of x, y, and z
    /// offsets, we use a unit vector and a scaling factor.
    ///
    /// This class was created specifically to allow a DoubleAnimation
    /// to use the Distance property as a target.  For that reason,
    /// Distance is a DependencyProperty.
    ///
    /// Intuitively, this class should derive from DependencyObject,
    /// but when I do this, the code compiles but the animation
    /// doesn't work.  If I instead just change the base class
    /// to UIElement, it works.  I'm not yet sure why.
    /// </summary>
    public class VectorTranslateTransform3D : UIElement
    {
        // This is a unit vector
        private Vector3D _direction;

        // This is the encapsulated transform object.
        private TranslateTransform3D _tt;

        public VectorTranslateTransform3D(
            TranslateTransform3D tt,
            Vector3D dir,
            double dist)
        {
            _tt = tt;
            Direction = dir;
            Distance = dist;
        }

        /// <summary>
        /// When the vector or the distance changes, we need to
        /// update the encapsulated transform object.
        /// </summary>
        private void update()
        {
            // multiply the unit vector times the distance to get
            // the full translation
            Vector3D vec = _direction * Distance;

            _tt.OffsetX = vec.X;
            _tt.OffsetY = vec.Y;
            _tt.OffsetZ = vec.Z;
        }

        public Vector3D Direction
        {
            get
            {
                return _direction;
            }
            set
            {
                _direction = value;
                _direction.Normalize();
                update();
            }
        }

        public double Distance
        {
            get
            {
                return (double)this.GetValue(DistanceProperty);
            }
            set
            {
                this.SetValue(DistanceProperty, value);
            }
        }

        private static void DistanceChangedCallback(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            VectorTranslateTransform3D v =
                (VectorTranslateTransform3D)d;

            v.update();
        }

        public static readonly DependencyProperty DistanceProperty =
            DependencyProperty.Register(
                "Distance",
                typeof(double),
                typeof(VectorTranslateTransform3D),
                new PropertyMetadata(
                new PropertyChangedCallback(DistanceChangedCallback)));
    }

But I still wish a vector-based translation class was built in to the framework.

Name Weirdness

Another "problem" is that the WPF animation stuff is a bit awkward to use from C#.  It appears to be designed to be used from XAML.

I already said that the animation objects require me to specify an object and a property on that object.  I was kind of assuming this would be simple, like maybe just passing the object into the constructor of the animation object and giving the name of the property as a string.  Instead, I had to fiddle around with classes called NameScope, DependencyProperty and PropertyPath, and I'm still not sure I understand any of them.

So first I create my translation object:

  TranslateTransform3D ttmove = new TranslateTransform3D(0, 0, 0);

And a reference to this object is placed inside all the GeometryModel3D objects which need to move around.

Then I create my encapsulation object:

  VectorTranslateTransform3D vtt = new VectorTranslateTransform3D(ttmove, new Vector3D(vec.x, vec.y, vec.z), 0);

Then I create a NameScope in my current window and register the translation object with a name.

  NameScope.SetNameScope(this, new NameScope());
  this.RegisterName("whyistherumalwaysgone", vtt);

Then when I create my Storyboard and DoubleAnimation objects, I need to hook them up through the namescope thingie:

  DoubleAnimation ia = new DoubleAnimation(blah blah blah);
  Storyboard.SetTargetName(ia, "whyistherumalwaysgone");

And to specify which property is being manipulated for the animation, I have to do this:

  Storyboard.SetTargetProperty(ia,
      new PropertyPath(VectorTranslateTransform3D.DistanceProperty)));

All this code feels a bit dorky, but I understand why it's there.

Results

In the end, the results are beautiful.  I'm not going to publish an xbap demo since everybody had trouble getting the last one to work, presumably because I'm still on WPF beta 2.  All this stuff will be easier when WPF is actually a shipping technology.

But suffice it to say that the animation works very well.  It's very smooth, with no flicker.  The CPU usage graph stays at zero while the animation is running.  Using the mouse to rotate and zoom works perfectly even while the animation is going on.  I can even print the Viewport3D while the animation is running, with the resulting page showing whatever was on the screen when I clicked the print button.