Useful links
1. Introduction.
Realistic motion is usually described by ordinary differential equations (ODE). Solution of ordinary differtial equations mostly requires numerical methods. Some equations assume a lot of calculations. For example motion model of artificail Eatrth's comtains model of Earth's gravity and Earth's atmosphere . Numerical methods calculate motion parameters at discrete values of time t1, t2, t3, ... The difference dt = ti+1-ti is called a step size. Reduction of step size supplies a more accurate solution of ODE. However there is the optimal value of dt0 such that there is no substantial difference between solutions given by step sizes dtopt and dt < dtopt. Value of dtopt depends on ODE. Moreveover this value is not
a constant. In case of robot pilot control value of dtopt is bigger then in case of manual control. A lot of tasks require animation. If dtopt is small then animation can be performed by following way. Computer solves ODE step by step and
simultaneously updates 3D images. This method is in fact synchronous animation which is well known long time ago. However if dtopt is not small then synchronous animation occurs considerable jumps of 3D images. There are following ways to avoid this problem:
- Reduction of step size;
- Application of asynchronous animation.
First way sometimes is not good because it requires substantial enlargement of calculation. Asynchronous animation is explained by following picture.
Real traejctory of 3D object is replaced with piecewise linear trajectory, which is close to real. However synchronous animation corresponds to piecewise constant function which is presented below.
Following movies show difference between different methods of animation.
Synchronous animation occurs substantial jumps of cube, jumps of asynchronous animation are scarcely visible.
Present day technologies support a lot of asynchronous animation methods with a powerful hardware support. In this article I have used WPF animation described here. However my soft contains abstract layer and it can be easy adapted for other types of animation.
2. Math Background
Uniform motion from 3D point p1 = (x1, y1, z1) to p2 = (x2, y2, z2) can be represented by following elementary expression
p(t) = (x1(1 - t) + x2t, y1(1 - t) + y2t, z1(1 - t) + z2t)
where t is a current progress (current progress corresponds to the Clock.CurrentProgress Property). Uniform rotational motion can be easy described by quaternions. Uniform rotation from quaternion Q1 = Q01 + Q11i + Q21j + Q11k to Q2=Q02 + Q21i + Q22j + Q12k is described by following equation
Q(t) = Q1Quniform(t)
where Quniform(t) corresponds to uniform rotation. Since uniform rotation is supported by WPF we do not need explicit calculation of Quniform(t). We need axis of rotation and rotation angle only. These parameters can be given from a following relative rotation quaternion
Qrelative = Q1*Q2
where Q1* = Q01 - Q11i - Q21j - Q11k.
Otherwise
Qrelative= cos (a/2) + rxsin(a/2)i + rysin(a/2)i + rzsin(a/2)k.
Where a is a full rotation angle, direction of vector r = (rx, ry, rz) coincides with rotation axis. If r= 0 (resp. r is close to 0) then rotation is abscent (resp. can be neglected). Above equations are implemented by Uniform6DMotion
class which is contained is source code. This class is independent from WPF or any other animation implementation.
3. WPF Implementation of the Uniform 6D Motion
Uniform 6D motion is implemented by Uniform6DTransformation
which is a custom animation class. Following code snippet explains its logics.
public class Uniform6DTransformation : AnimationTimeline
{
#region Fields
Transform3DGroup transform = new Transform3DGroup();
TranslateTransform3D translation = new TranslateTransform3D();
RotateTransform3D rotate_const = new RotateTransform3D();
RotateTransform3D rotate_uniform = new RotateTransform3D();
QuaternionRotation3D quaternionConstRotation = new QuaternionRotation3D();
AxisAngleRotation3D angle_uniform = new AxisAngleRotation3D();
Motion6D.Uniform6D.Uniform6DMotion calculator;
internal Uniform6DTransformation(AnimatableWrapper wrapper, Motion6D.ReferenceFrame frame,
bool realtime, double[] changeFrameTime, TimeSpan forecastTime)
{
transform.Children.Add(rotate_uniform); transform.Children.Add(rotate_const); transform.Children.Add(translation); rotate_const.Rotation = quaternionConstRotation; rotate_uniform.Rotation = angle_uniform;
}
private void SetProgressTime(double progressTime)
{
double angle; double x, y, z; calculator.SetTime(progressTime, out angle, out x, out y, out z); angle_uniform.Angle = (180 / Math.PI) * angle; translation.OffsetX = x;
translation.OffsetY = y;
translation.OffsetZ = z;
}
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
SetTime(animationClock.CurrentProgress.Value);
return transform;
}
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
SetProgressTime(animationClock.CurrentProgress.Value);
return transform;
}
}
Above code means that a transformatin is decomoposed to
- Uniform 3D translation;
- Constant 3D rotation;
- Uniform 3D rotation.
4. General Asynchronous Animation Algorithm
Asynchronous animation architecture contains abstract level which corresponds to following interface.
public interface IAnimationDriver
{
object StartAnimation(IComponentCollection collection, string[] reasons,
Enums.AnimationType animationType,
TimeSpan pause, double timeScale, bool realTime, bool absoluteTime);
bool SuppotrsAsynchronous
{
get;
}
}
public enum ActionType
{
Calculation,
Animation
}
public enum AnimationType
{
Synchronous,
Asynchronous
}
The StartAnimation
function returns object which implements following interface.
public interface IAsynchronousCalculation
{
void Start(double time);
Action<double> Step
{
get;
}
void Interrupt();
void Suspend();
bool IsRunning
{
get;
}
event Action OnSuspend;
event Action Finish;
event Action OnInterrupt;
}
Above interface is not intended for animation only. Following table contains different implementations of this interface.
N | Class name | Purpose |
1 | PauseAsynchronousCalculation | Synchronous animation |
2 | WpfAsynchronousAnimatedCalculation | Asynchronous animation for WPF |
3 | WpfAsynchronousRealtimeAnimatedCalculation | Asynchronous real-time animation for WPF |
The real-time mode is described below.
The PauseAsynchronousCalculation
does not depend on WPF or other animation technology. This class just performs a pause. Following code explains this circumstance.
public class PauseAsynchronousCalculation : IAsynchronousCalculation
{
#region Fields
private Action<double> pause;
public PauseAsynchronousCalculation(TimeSpan pauseSpan)
{
pause = (double time) =>
{
Thread.Sleep(pauseSpan);
};
}
Action<double> IAsynchronousCalculation.Step
{
get { return pause; }
}
}
Every step of synchronous animation assumes following operations:
- Calculation of motion parameters;
- Showing of 3D objects with new 6D positions;
- Time pause.
Asynchronous animation assumes one thread of calculation and many threads for animation. Every step of asynchronous animation calculation contains following operations:
- Calculation of motion parameters;
- Enqueuing motion parameters and current time to every animation thread.
Every animation thread performs following operations:
5. Calculation of motion parameters
Calculation of motion parameters is already described in my previous article. However I overlap some text from my previous article for convenience.
A kinematics domain contains following basic types:
- 3D Position (
IPosition
interface);
- 3D Orientation (
IOrientation
interface;
- Standard 3D Position (
Position
class which implements IPosition
interface);
- 3D Reference frame (
ReferenceFrame
class which implements both IPosition
and IOrientation
);
- Holder 3D Reference frame (
IReferenceFrame
interface) ;
- Reference frame binding (
ReferenceFrameArrow
class which implements ICategoryArrow
interface).
Source (resp. target) of ReferenceFrameArrow
is always IPosition
(resp. IReferenceFrame
). This arrow means that coordinates of IPosition
are relative with respect to IReferenceFrame
. Following code represents these
types:
public interface IPosition
{
double[] Position
{
get;
}
IReferenceFrame Parent
{
get;
set;
}
object Parameters
{
get;
set;
}
void Update();
}
public interface IOrientation
{
double[] Quaternion
{
get;
}
double[,] Matrix
{
get;
}
}
public class Position : IPosition, IChildrenObject
{
#region Fields
protected IReferenceFrame parent;
protected double[] own = new double[] { 0, 0, 0 };
protected double[] position = new double[3];
protected object parameters;
protected IAssociatedObject[] ch = new IAssociatedObject[1];
#endregion
#region Ctor
protected Position()
{
}
public Position(double[] position)
{
for (int i = 0; i < own.Length; i++)
{
own[i] = position[i];
}
}
#endregion
#region IPosition Members
double[] IPosition.Position
{
get { return position; }
}
public virtual IReferenceFrame Parent
{
get
{
return parent;
}
set
{
parent = value;
}
}
public virtual object Parameters
{
get
{
return parameters;
}
set
{
parameters = value;
if (value is IAssociatedObject)
{
IAssociatedObject ao = value as IAssociatedObject;
ch[0] = ao;
}
}
}
public virtual void Update()
{
Update(BaseFrame);
}
#endregion
}
public class ReferenceFrame : IPosition, IOrientation
{
#region Fields
protected double[] quaternion = new double[] { 1, 0, 0, 0 };
protected double[] position = new double[] { 0, 0, 0 };
protected double[,] matrix = new double[,] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
protected double[,] qq = new double[4, 4];
protected double[] p = new double[3];
protected IReferenceFrame parent;
protected object parameters;
private double[] auxPos = new double[3];
#endregion
#region Ctor
public ReferenceFrame()
{
}
private ReferenceFrame(bool b)
{
}
#endregion
#region IPosition Members
public double[] Position
{
get { return position; }
}
public virtual IReferenceFrame Parent
{
get
{
return parent;
}
set
{
parent = value;
}
}
public virtual object Parameters
{
get
{
return parameters;
}
set
{
parameters = value;
}
}
public virtual void Update()
{
ReferenceFrame p = ParentFrame;
position = p.Position;
quaternion = p.quaternion;
matrix = p.matrix;
}
#endregion
#region IOrientation Members
public double[] Quaternion
{
get { return quaternion; }
}
public double[,] Matrix
{
get { return matrix; }
}
#endregion
}
public interface IReferenceFrame : IPosition
{
ReferenceFrame Own
{
get;
}
List<IPosition> Children
{
get;
}
}
[Serializable()]
public class ReferenceFrameArrow : CategoryArrow, ISerializable, IRemovableObject
{
#region Fields
IPosition source;
IReferenceFrame target;
#endregion
#region Constructors
public ReferenceFrameArrow()
{
}
protected ReferenceFrameArrow(SerializationInfo info, StreamingContext context)
{
}
#endregion
#region ISerializable Members
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
}
#endregion
#region ICategoryArrow Members
public override ICategoryObject Source
{
get
{
return source as ICategoryObject;
}
set
{
IPosition position = value.GetSource<IPosition>();
if (position.Parent != null)
{
throw new CategoryException("Root", this);
}
source = position;
}
}
public override ICategoryObject Target
{
get
{
return target as ICategoryObject;
}
set
{
IReferenceFrame rf = value.GetTarget<IReferenceFrame>();
IAssociatedObject sa = source as IAssociatedObject;
IAssociatedObject ta = value as IAssociatedObject;
INamedComponent ns = sa.Object as INamedComponent;
INamedComponent nt = ta.Object as INamedComponent;
target = rf;
source.Parent = target;
target.Children.Add(source);
}
}
#endregion
}
Some code is omitted, a reader can find it in source files. Let us consider a following picture:
Type of the Point is Position
and the Frame implements IReferenceFrame
interface. The Link arrow means that motion of Point is relative with respect to Frame. Absolute coordinates of Point are calculated by following way:
Xa = XF + A11Xr + A12Yr + A13Zr;
Ya = YF + A21Xr + A22Yr + A23Zr;
Za = ZF + A31Xr + A32Yr + A33Zr.
where
- Xa, Ya, Za absolute coordinates of Point
- XF, YF, ZF absolute coordinates of Frame
- Xr, Yr, Zr relative coordinates of Point
- A11,..., A33 - elements of
3D rotation matrix.
6. Animated objects
There is a many to many relation of following objects:
Following movies contains 7 shapes and 5 cameras with independent motion
Helicoper contains following shapes:
- Fuselage;
- Tail rotor;
- 5 blades of main rotor.
Independent motion of blades is due to main rotor assembly
WPF allows usage of single Visual3D
in single camera only. So we need for copies of 3D shapes. Above picture contains 5 * 7 + 5 - 40 animatable (IAnimatable interface), they are 5 cameras and 5 copies for each 7 Visual3D
objects. However simulation of motion is performed for 5 + 7 = 12 objects (5 cameras and 7 3D shapes). So we need 12 objects of the ReferenceFrame type. Following interface is responsible for interoperability between animated objects and objects of 6D motion simulation.
public interface ILinear6DForecast
{
ReferenceFrame ReferenceFrame
{
get;
}
TimeSpan ForecastTime
{
get;
set;
}
double CoordinateError
{
get;
set;
}
double AngleError
{
get;
set;
}
}
Above interface does not depend on 3D graphics technology, following interface is a WPF version.
interface IAnimatedObject : ILinear6DForecast
{
void InitAnimation(AnimationType animationType);
void InitRealtime(AnimationType animationType, double[] changeFrameTime);
AnimatableWrapper[] Children
{
get;
}
event Action Change;
void StopAnimation();
event Action OnStop;
bool SupportsAnimationEvents
{
get;
set;
}
}
The Children
property contains wrappers of animatable (IAnimatable interface). In previous picture every 3D shape contains 5 children, every perspective camera has a single child. Following code contains wrapper of animatable object.
public class AnimatableWrapper : IDisposable
{
#region Fields
private IAnimatable animatable;
private DependencyProperty dependencyProperty;
private object value;
private Action change = () => { };
private IAnimatedObject animatedObject;
private Uniform6DTransformation transformation;
private Action finish = () => { };
double[] auxQuaternion = new double[4];
bool isStopped = false;
object loc = new object();
#endregion
#region Ctor
internal AnimatableWrapper(IAnimatable animatable, DependencyProperty dependencyProperty,
IAnimatedObject animatedObject, bool realtime, double[] changeFrameTime)
{
this.animatable = animatable;
this.dependencyProperty = dependencyProperty;
value = (animatable as DependencyObject).GetValue(dependencyProperty);
this.animatedObject = animatedObject;
transformation = new Uniform6DTransformation(this,animatedObject.ReferenceFrame,
realtime, changeFrameTime, animatedObject.ForecastTime);
}
#endregion
#region Public Members
public IAnimatable Animatable
{
get
{
return animatable;
}
}
public DependencyProperty DependencyProperty
{
get
{
return dependencyProperty;
}
}
#endregion
#region Internal Members
internal IAnimatedObject Animated
{
get
{
return animatedObject;
}
}
internal void Stop()
{
lock (loc)
{
transformation.Stop();
isStopped = true;
finish();
}
}
internal void StartRealtime(double time, DateTime start)
{
transformation.StartRealtime(time, start);
}
internal void Init(double[] coord, double[] quaternion)
{
transformation.Init(coord, quaternion);
}
internal void StartAnimation(double[] coord, double[] quaternion, DateTime start)
{
transformation.StartAnimation(coord, quaternion, start);
}
internal void Enqueue(Tuple<TimeSpan, double[], double[]> parameters)
{
transformation.Enqueue(parameters);
}
internal void Finish()
{
finish();
}
internal event Action OnFinish
{
add { finish += value; }
remove { finish -= value; }
}
internal Action Event
{
get
{
return transformation.Event;
}
}
#endregion
#region IDisposable Members
void IDisposable.Dispose()
{
(animatable as DependencyObject).SetValue(dependencyProperty, value);
animatedObject.Change -= change;
}
#endregion
}
Above class contains a transformation
field of the Uniform6DTransformation : AnimationTimeline type. This field is responsible for animation.
7. Real-time
Recently I wrote an article devoted to real-time. This section can be regarded as an animation extension of the real-time simulation. Above animation is explained by following picture.
However above scheme is not sufficient for real-time because animation lag behind simulation. Sufficient scheme is presented below.
This scheme implies usage of the linear prediction. Both velocity vector and angular velocity vectors supply necessary data for linear prediction. My software supports calculation of velocity and angular velocity for different engineering problems (See here and here). Now these calculations are used for the asynchronous animation. Following interfaces are responsible for calculation of velocity vectors.
public interface IVelocity
{
double[] Velocity
{
get;
}
}
public interface IAngularVelocity
{
double[] Omega
{
get;
}
}
Following code explains application of these interfaces for linear prediction.
ReferenceFrame frame;
IVelocity velocity;
IAngularVelocity angularVelocity;
public void InitializePrediction(double forecastTime)
{
if (!(frame is IVelocity))
{
throw new Exception("Frame does not support velocity");
}
if (!(frame is IAngularVelocity))
{
throw new Exception("Frame does not support angular velocity");
}
velocity = frame as IVelocity;
angularVelocity = frame as IAngularVelocity;
}
public void InitializePrediction(double forecastTime)
{
if (!(frame is IVelocity))
{
throw new Exception("Frame does not support velocity");
}
if (!(frame is IAngularVelocity))
{
throw new Exception("Frame does not support angular velocity");
}
velocity = frame as IVelocity;
angularVelocity = frame as IAngularVelocity;
}
private void LinearPrediction(double time)
{
lastTime = time;
double delta = time - changeFrameTime[0]; double[] coord = frame.Position; double[] v = velocity.Velocity; for (int i = 0; i < 3; i++)
{
auxVectorFrame[i] = coord[i] + v[i] * delta; }
double[] omega = angularVelocity.Omega; double mod = omega[0] * omega[0] + omega[1] * omega[1] + omega[2] * omega[2];
mod = Math.Sqrt(mod);
Array.Copy(omega, 0, auxQuarterInter, 1, 3); double angle = 0.5 * mod * delta;
double s = Math.Sin(angle);
double c = Math.Cos(angle);
auxQuarterInter[0] = c;
double smod = s / mod;
for (int i = 1; i < 3; i++)
{
auxQuarterInter[i] *= smod;
}
QuaternionMultiply(frame.Quaternion, auxQuarterInter, auxQuaterFrame); }
8. Examples
8.1 Animation of a Cube
This sample is rather demo than realistic. Motion of cube is described by following finite formulas.
Above formulas are used as coordinates and components of 3D orientation quaternion.
Following movie represents animation of the cube.
8.2 Animation of a Helicopter
Motion of helicopter is also rather demo and it also is defined by finite formulas. But it includes 7 3D shapes and 5 virtual cameras. Following movie represents motion of helicopter.
8.3 Animation of Artificial Earth's Satellite.
This sample is quite realistic because it uses following ingredients:
Description of math model details is here. Following movie represents this animation.
8.4 Manual Control of an Airplane
This sample is close to realistic. It uses real-time manual control of an airplane. Motion model of airplane is quite realistic because it uses realistic model atmosphere and an aerodynamics model. However manual control is not realistic. Instead a pilot wheel the "roll pitch robot pilot" is used. The ForcedEventData class is used for manual control. Object of this class performs following operations:
User can update required values of pith and roll. Any update occurs a transient state. Following chart contains two transient states.
.
Real-time simulation is based on event driven calculations. Following scheme of events is used for airplane simulation.
Our calculation is driven by Event collection object which is simultaneously a generator of events and an event handler. It is an object of the EventCollection class. The Event collection is a "sum" of Timer, Transition and Pilot Control events. The Timer raises one event per 2000 ms = 2 s. The Pilot Control raises event of user manual control. The Transition raises one event per 0.2 s during transition state. The Transition is an object of the TransientProcessEvent
class. This class is simultaneously a source of event and event handler. Following code explains how does this class works.
[Serializable()]
public class TransientProcessEvent : CategoryObject, IEvent, IEventHandler, ISerializable
{
#region Fields
TimeSpan[] spans;
private Action ev = () => { };
object loc = new object();
bool isEnabled = false;
DateTime previous = DateTime.Now;
bool isRunning = false;
#endregion
#region Ctor
public TransientProcessEvent()
{
}
#endregion
#region IEvent Members
event Action IEvent.Event
{
add { ev += value; }
remove { ev -= value; }
}
#endregion
#region IEventHandler Members
void IEventHandler.Add(IEvent ev)
{
events.Add(ev);
onAdd(ev);
}
#endregion
#region Private Members
void Enable()
{
lock (loc)
{
isRunning = false;
if (isEnabled)
{
previous = DateTime.Now;
foreach (IEvent ev in events)
{
ev.Event += EventHandler; }
return;
}
foreach (IEvent ev in events)
{
ev.Event -= EventHandler; }
}
}
void EventHandler()
{
if (isRunning)
{
previous = DateTime.Now;
return;
}
lock (loc)
{
if (isRunning)
{
return;
}
isRunning = true;
currentStep = 0;
Action act = AsyncEvent; act.BeginInvoke(null, null);
}
}
void AsyncEvent()
{
for (int i = 0; i < spans.Length; i++)
{
Thread.Sleep(spans[i]); ev(); }
}
#endregion
}
The AsyncEvent
performs cyclic sleep operation and raising of event. Following movies explains how does it works.
After user action (mouse click/move) we have 10 updates distinguished by update per 0.2 s. In passive mode we have one update per 2 s. Following movie shows this sample.
Points of Interest
I am a software developer since 1977. During development of this article I first time encoutered with deadlock.