|
Wednesday, August 02, 2006
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...
- I tossed out the code to generate 10 different meshes for
10 frames and wrote code to generate two meshes. One set of triangles was
for the part of my solid model that is moving, and the other set
corresponds to the part that is to remain in place.
- Then I created a TranslateTransform3D object as a hookup
point for animation.
- Then I took the moving triangles and stuffed them in a
GeometryModel3D object and set the Transform to point to the
TranslateTransform3D instance I just created.
- Then I created a DoubleAnimation object to animate the TranslateTransform3D
object.
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.
|