## Introduction

Recently, I have been working with Delaunay triangulation/Voronoi meshes a lot, and one day, I got the idea I could also do something fun with it. So I thought, what if I take a random set of 3D points, triangulate it, and then animate the result. I think that what I got looked pretty cool, so I decided to share it.

This article covers:

- Calling a library to calculate the Delaunay triangulation (well, tetrahedralization, but that word is so much pain to write/read/pronounce) on a random set of 3D points.
- Representing the result as a WPF
`Visual3D`

object. - Generating several types of animations - expand, random expand, collapse, and pulse/collapse.

This article does not cover:

- Using MVVM and other fancy stuff that has the word pattern associated with it.

## Background

Although the topic of 3D Delaunay triangulation is not trivial per se, for the purposes of this article, it is enough to know it is a set of tetrahedrons. Additionally, some basic knowledge of linear algebra and familiarity with WPF is always welcome.

In this article, I use the following code written by other people:

## First, the "hard work"...

Before we can start having fun, we need to do some heavy lifting. In this case, it is generating the data, computing the triangulation, and representing the result.

#### Generating the data

We will use the class `Vertex`

to represent our random 3D points. Choosing a wrapper instead of a more straightforward `Point3D`

is required because MIConvexHull encourages the input data to implement the `IVertexConvHull`

interface, which is a little inconvenient, but we will have to live with that:

class Vertex : IVertexConvHull
{
public double[] coordinates { get; set; }
public Point3D Position
{
get
{
return new Point3D(coordinates[0], coordinates[1], coordinates[2]);
}
}
}

Given the number of points to generate and the radius of the points, generating the random data is straightforward (`List`

is used because MIConvexHull uses it):

var rnd = new Random();
Func<double> nextRandom = () => 2 * radius * rnd.NextDouble() - radius;
var vertices = Enumerable.Range(0, count)
.Select(_ => new Vertex(nextRandom(), nextRandom(), nextRandom())
.ToList();

#### Triangulation

Random data generated - check. MIConvexHull represents the tetrahedrons of the triangulation by a type that implements the `IFaceConvHull`

interface:

class Tetrahedron : IFaceConvHull
{
public IVertexConvHull[] vertices { get; set; }
public double[] normal { get; set; }
Point3D GetPosition(int i) { return ((Vertex)vertices[i]).Position; }
}

As a side node, the interface is called "`FaceConvHull`

" because the triangulation is computed by finding a convex hull of a 4D object (which sounds fancy, but the idea is remarkably simple). Later, we will expand the `Tetrahedron`

type with the code to generate its WPF model and animations.

The triangulation itself is done by these two lines of code:

var convexHull = new ConvexHull(vertices);
var tetrahedrons = convexHull.FindDelaunayTriangulation(
typeof(Tetrahedron)).Cast<Tetrahedron>().ToArray();

The cast is present because `FindDelaunayTriangulation`

returns a list of `IFaceConvHull`

.

#### Representing the data

The type `Tetrahedron`

contains a function called `CreateModel`

which returns a `Model3D`

object. The models of all the tetrahedrons are later grouped using `Model3DGroup`

and finally wrapped in a `ModelVisual3D`

which can be displayed in a `Viewport3D`

.

So, the tetrahedron model is an instance of the `GeometryModel3D`

(which is derived from `Model3D`

) class. To successfully create an instance of `GeometryModel3D`

, we need to specify its geometry (triangles representing the object) and the material.

There are two ways to specify the `MeshGeometry3D`

:

- Add three points for each triangle. For triangles that share an edge, the points need to be duplicated. In this case, flat shading is used and the object gets a "faceted" look.
- Add
`n`

points and `3n`

indices. Each consecutive triplet of indices then represents a triangle. In this case, smooth shading is used because the system can compute the adjacency of individual triangles and therefore interpolate the normal for each vertex.

Furthermore, the order in which the points in the first case and the indices in the second case are added determines whether the triangle is facing front or back. This determines the lighting and visibility of the triangle. I will not go into more detail about this and point the reader for an example here.

In this article, the second approach is used. The function that takes care of correctly ordering the indices of triangles is called `MakeFace`

and I leave it as an exercise for the reader to figure how it works.

The material used has two components: the `DiffuseMaterial`

component which is the randomly generated color of each tetrahedron and the `SpecularMaterial`

that makes the tetrahedron "shine".

Additionally, a `TranslateTransform3D`

is applied to each model which will later be animated.

The whole model is represented by the `RandomTriangulation`

class which is derived from WPF's `ModelVisual3D`

.

#### The User Interface (or the lack thereof)

The user interface of this application is rather simple. It only contains a `Viewport3D`

, a bunch of buttons, a text box, and two labels. There are many articles about WPF layout and the sorts, so I will not focus on it here.

However, there are some points of interest. The first is setting up the camera and lighting and the other is rotating and zooming the scene.

<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight x:Name="light" Color="#FF808080" Direction="0 -2 -1" />
<AmbientLight Color="LightYellow" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0 0 80" UpDirection="0 1 0"
LookDirection="0 0 -1" FieldOfView="45" />
</Viewport3D.Camera>

The `DirectionalLight`

is named because we will want to apply a transform to it depending on the position of the camera.

For rotation and zooming, the `Trackball`

class is used, which is very well described here. The `Viewport3D`

is wrapped in a `Grid`

to capture the mouse event for the `Trackball`

. Also, I have extended the trackball code and made the rotation component of the transform public to be used for rotating the light direction:

var trackball = new Wpf3DTools.Trackball();
trackball.EventSource = background;
viewport.Camera.Transform = trackball.Transform;
light.Transform = trackball.RotateTransform;

## ... and then some fun

The fun part is the animation. To animate a property of an object, the `BeginAnimation`

method of the type `Animatable`

is used (pretty much everything in WPF derives from this type). This method takes two arguments: a `DependencyProperty`

to animate and an `AnimationTimeline`

object:

- The
`DependencyProperty`

is, for example, the `OffsetXProperty`

of the `TranslateTransform3D`

type. - The
`AnimationTimeline`

is an object that wraps how the value of the property changes over time. For example:

AnimationTimeline timeline =
new DoubleAnimation { From = 2, To = 10, Duration = TimeSpan.FromSeconds(2) };

represents the gradual change of the value from 2 to 10 over the course of 2 seconds. In general, WPF animations have several properties that can be changed. For example, not specifying the `From`

field means the current value of the animated property is used as the start value when the animation starts. `IsAdditive`

is another interesting property, which when set to `true`

adds the animated value to the original value of the animated property - at the end of the animation, the value is `original + to`

.

By default, the animated value is de facto a linear function of the time parameter (I say de facto because there is some interpolation going on, but I hope you get the idea). This is often pretty boring. One way around this "problem" is using the so called easing functions. By utilizing an easing function, the animated value is now a function of the time parameter. There are several predefined easing functions such as `CircleEase`

and `ElasticEase`

. These classes implement the `IEasingFunction`

interface which contains only one function: `double Ease(double normalizedTime)`

. The `normalizedTime`

argument is a value from 0 to 1 and is interpolated from the actual duration of the animation.

The following example shows an implementation of the circle ease out function - in this case, the value changes much more rapidly at the start of the animation and then almost ceases to change towards the end. Additionally, the red line shows the "original" linear function.

class MyCircleEaseOut : IEasingFunction
{
public double Ease(double normalizedTime)
{
double t = 1 - normalizedTime;
return Math.Sqrt(1 - t * t);
}
}

As a side note, when implementing custom animations, a better idea would be to derive from the `EasingFunctionBase`

class which has some basic functionality already built in (such as ease in/out support). Also, there is a nice gallery of easing functions at MSDN. Furthermore, other types of animations exist such as `Point3DAnimation`

or `ColorAnimation`

, but in the article, we will only use the `DoubleAnimation`

.

#### Expand animation

The idea behind the expand animation (and almost all other animations presented here for that matter) is translating the tetrahedron towards the direction defined by the geometrical center of the tetrahedron and the origin (0 vector). The geometrical center is simply the arithmetic average of the tetrahedron vertices' positions, and can be computed for example by some simple LINQ:

var center = points.Aggregate(new Vector3D(),
(a, c) => a + (Vector3D)c) / (double)points.Count;

Furthermore, we would like to expand the object ad infinitum. This is where the `IsAdditive`

property comes in. Now, each time the user starts the expand animation, the object will expand more and more.

The `TranslateTransform3D`

, which will be used as the means for moving the tetrahedron, has three properties: `OffsetX`

, `OffsetY`

, and `OffsetZ`

. Therefore, we will need to animate each property independently. Nevertheless, animation of each translation will be very similar, so we will write a function that creates the animation:

AnimationTimeline CreateExpandAnimation(double to)
{
return new DoubleAnimation
{
From = 0,
To = to,
Duration = TimeSpan.FromSeconds(1),
EasingFunction = expandEasing,
IsAdditive = true
};
}

Notice the use of `circleOutEasing`

. This ensures that the expansion will start fast and then gradually slows down.

Now, nothing stands in our way of creating the animations:

expandX = CreateExpandAnimation(2 * center.X);
expandY = CreateExpandAnimation(2 * center.Y);
expandZ = CreateExpandAnimation(2 * center.Z);

It is also convenient to define another helper function to begin the desired animation and specify the handoff behavior, which specifies how the animation should behave if two animations overlap:

void Animate(AnimationTimeline x, AnimationTimeline y, AnimationTimeline z)
{
translation.BeginAnimation(TranslateTransform3D.OffsetXProperty,
x, HandoffBehavior.SnapshotAndReplace);
translation.BeginAnimation(TranslateTransform3D.OffsetYProperty,
y, HandoffBehavior.SnapshotAndReplace);
translation.BeginAnimation(TranslateTransform3D.OffsetZProperty,
z, HandoffBehavior.SnapshotAndReplace);
}

Finally, the function `Expand`

does the job of moving the tetrahedron:

void Expand()
{
Animate(expandX, expandY, expandZ);
}

Alternatively, a `Storyboard`

type could be used to apply the animation to the components of the translation, but I will not discuss that here.

To animate all the tetrahedrons, we simply call `Expand`

on each of them:

foreach (var t in tetrahedrons) t.Expand()

#### Randomized expand

To make the expand animation a little more interesting, here's a randomized version. Each tetrahedron is randomly moved along the X, Y, or Z axis, either in positive or negative direction:

public void ExpandRandom()
{
switch (rnd.Next(6))
{
case 0: translation.BeginAnimation(TranslateTransform3D.OffsetXProperty,
movePositive, HandoffBehavior.SnapshotAndReplace); break;
case 1: translation.BeginAnimation(TranslateTransform3D.OffsetXProperty,
moveNegative, HandoffBehavior.SnapshotAndReplace); break;
case 2: translation.BeginAnimation(TranslateTransform3D.OffsetYProperty,
movePositive, HandoffBehavior.SnapshotAndReplace); break;
case 3: translation.BeginAnimation(TranslateTransform3D.OffsetYProperty,
moveNegative, HandoffBehavior.SnapshotAndReplace); break;
case 4: translation.BeginAnimation(TranslateTransform3D.OffsetZProperty,
movePositive, HandoffBehavior.SnapshotAndReplace); break;
case 5: translation.BeginAnimation(TranslateTransform3D.OffsetZProperty,
moveNegative, HandoffBehavior.SnapshotAndReplace); break;
default: break;
}
}

where the move animations are defined as `movePositive = CreateExpandAnimation(radius / 2)`

and `moveNegative = CreateExpandAnimation(-radius / 2)`

(`radius`

is an input parameter for generating the random points).

#### Collapse animation

As opposed to the additive nature of expand, we would like to collapse the object to its original configuration from any state. Furthermore, we want the object to start collapsing slowly and then gradually speed up the process. This is achieved by not specifying the `From`

value and using the `CircleEasing`

with `EaseIn`

:

var collapse = new DoubleAnimation
{
To = 0,
Duration = TimeSpan.FromSeconds(1),
EasingFunction = collapseEasing
};
void Collapse()
{
Animate(collapse, collapse, collapse);
}

#### Pulse and collapse animation

Save the best for last. The idea here is to have the object pulse for a while and then suddenly collapse. Now, the pulse part can be done using the `ElasticEase`

shown below on the left. The collapse part is the circle ease in function. The goal is to combine these two together, as shown bellow on the right.

To achieve this, we could define a custom easing function. However, WPF gives us another option - the keyframe animation:

AnimationTimeline CreatePulseAnimation(double to)
{
DoubleAnimationUsingKeyFrames pulseAndCollapse = new DoubleAnimationUsingKeyFrames
{
Duration = new Duration(TimeSpan.FromSeconds(3.5)),
KeyFrames = new DoubleKeyFrameCollection
{
new EasingDoubleKeyFrame(to, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(3)),
pulseEasing),
new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(3.5)),
collapseEasing)
}
};
return pulseAndCollapse;
}

where `pulseEasing = new ElasticEase { Springiness = 1, EasingMode = EasingMode.EaseOut, Oscillations = 8 }`

.

## Conclusion

Well, that's it. I hope you will have some fun playing with/improving the code. I did not cover some aspects such as freezing the objects, defining the animations in XAML (and say, using storyboards), or calculating the triangulation asynchronously, because I did not consider it important for the purpose of this application/article.

## References

## History

- March 18
^{th} 2010 - Initial version.