| WPF tutorial 3D-Animations and Textures |
| Section: WPF | Rating: Not rated yet! |
 | CodeGod submitted this resource The member's homepage is http://www.codegod.de Visit the profile here |
Attachment
Introduction
| In this article I demonstrate 3 different features of 3D graphics with WPF: Generating a Sphere, texturing the model and add animations to the scene (storyboard-animation and user-interaction). | |
Figure 1The idea
In the example a cube made of 3D-balls is rendered. We have 3 different images for adding textures to the balls. The whole model is animated by using the StoryBoard-class and the user can change the distance of the cube by moving the mouse up and down while pressing the left mouse-button.
The project
We create a WPF Application with VS 2008 Beta2 and start to define the background with XAML for the Window-class:
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="#696988" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
This will draw a nice gradient for the whole scene.
After that, I had to think a little bit. How can I draw a sphere? There are no primitives available in WPF like in OpenGL. But there is a great open-source
primitive-library available from Daniel Lehenbauer. I just used the code for the Sphere3D-class and modified it a little bit so that we can assign an image easily by code:
public class Ball : ModelVisual3D
{
public Ball()
{
this.Content = new GeometryModel3D();
(this.Content as GeometryModel3D).Geometry = Tessellate();
}
internal Point3D GetPosition(double t, double y)
{
double r = Math.Sqrt(1 - y * y);
double x = r * Math.Cos(t);
double z = r * Math.Sin(t);
return new Point3D(x, y, z);
}
private Vector3D GetNormal(double t, double y)
{
return (Vector3D)GetPosition(t, y);
}
private Point GetTextureCoordinate(double t, double y)
{
Matrix TYtoUV = new Matrix();
TYtoUV.Scale(1 / (2 * Math.PI), -0.5);
Point p = new Point(t, y);
p = p * TYtoUV;
return p;
}
public string ImageSource
{
set {
DiffuseMaterial dm = new DiffuseMaterial();
ImageSource imSrc = new
BitmapImage( new Uri( value, UriKind.RelativeOrAbsolute ) );
dm.Brush = new ImageBrush( imSrc );
(this.Content as GeometryModel3D).Material = dm;
}
}
public Point3D Offset
{
set {
this.Transform = new
TranslateTransform3D(value.X, value.Y, value.Z);
}
}
internal Geometry3D Tessellate()
{
int tDiv = 32;
int yDiv = 32;
double maxTheta = MathHelper.DegToRad(360.0);
double minY = -1.0;
double maxY = 1.0;
double dt = maxTheta / tDiv;
double dy = (maxY - minY) / yDiv;
MeshGeometry3D mesh = new MeshGeometry3D();
for (int yi = 0; yi <= yDiv; yi++)
{
double y = minY + yi * dy;
for (int ti = 0; ti <= tDiv; ti++)
{
double t = ti * dt;
mesh.Positions.Add(GetPosition(t, y));
mesh.Normals.Add(GetNormal(t, y));
mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y));
}
}
for (int yi = 0; yi < yDiv; yi++)
{
for (int ti = 0; ti < tDiv; ti++)
{
int x0 = ti;
int x1 = (ti + 1);
int y0 = yi * (tDiv + 1);
int y1 = (yi + 1) * (tDiv + 1);
mesh.TriangleIndices.Add(x0 + y0);
mesh.TriangleIndices.Add(x0 + y1);
mesh.TriangleIndices.Add(x1 + y0);
mesh.TriangleIndices.Add(x1 + y0);
mesh.TriangleIndices.Add(x0 + y1);
mesh.TriangleIndices.Add(x1 + y1);
}
}
mesh.Freeze();
return mesh;
}
}
Okay, so far for the Ball-object. But if we want to display a cube made of balls we have to do a little bit more. Here is the BallCubeBuilder-class for creating a BallCube (some hard-coded values in it, you can change this if you like):
public class BallCubeBuilder
{
private ModelVisual3D _mv3D;
public BallCubeBuilder(ModelVisual3D modelVisual3D)
{
_mv3D = modelVisual3D;
}
public void BuildSlice( string imageSrc, double offsetZ )
{
Point3D p3D = new Point3D(0, 0, offsetZ);
for (int x = 0; x < 3; x++)
{
for (int y = -1; y < 2; y++)
{
Ball ball = new Ball();
ball.ImageSource = imageSrc;
p3D.X = (x * 2.0) - 2.0;
p3D.Y = (y * 2.0);
ball.Offset = p3D;
_mv3D.Children.Add(ball);
}
}
}
}
And this is how the cube is build by code:
public Window1()
{
InitializeComponent();
BallCubeBuilder bcb = new BallCubeBuilder(visualModel);
bcb.BuildSlice("Mars.jpg", -2.0);
bcb.BuildSlice("Venus.jpg", 0.0);
bcb.BuildSlice("Uranus.jpg", 2.0);
}
You can see that the BallCubeBuilder has a member of type ModelVisual3D. All Ball-objects are added to that object as children. This is possible because a Ball-object is derived from ModelVisual3D. Here is the XAML-code for the parent-object:
<ModelVisual3D x:Name="visualModel">
<ModelVisual3D.Transform>
<Transform3DGroup x:Name="transformGroup">
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotationY"
Angle="0" Axis="0,1,0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotationX"
Angle="0" Axis="1,0,0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
</ModelVisual3D.Transform>
</ModelVisual3D>
As you can see I added a Transform3DGroup to the visualModel-object. We will need this for our animations (see next chapter).
Animations
Defining some rotations for the cube is easy: We can use the StoryBoard-class in our XAML-code that rotates the objects along the X- and Y-axis. As targets we use the AxisAngleRotation-instances rotationX and rotationY:
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded" >
<BeginStoryboard>
<Storyboard Name="myStoryBoardX">
<DoubleAnimation
Storyboard.TargetName="rotationX"
Storyboard.TargetProperty="Angle"
From="0" To="360" Duration="0:0:15" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard Name="myStoryBoardY">
<DoubleAnimation
Storyboard.TargetName="rotationY"
Storyboard.TargetProperty="Angle"
From="0" To="360" Duration="0:0:12" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
For the mouse-interaction we reuse the
MouseHelper-class from a
previous project but changed it so that we can modify the
FieldOfView-property of our
PerspectiveCamera-object:
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Media3D;
using System.Windows.Markup;
public class MouseHelper
{
private PerspectiveCamera _camera;
private FrameworkElement _eventSource;
private Point _position;
private double _diffX = 0;
public MouseHelper(PerspectiveCamera camera)
{
_camera = camera;
}
public FrameworkElement EventSource
{
get { return _eventSource; }
set
{
if (_eventSource != null)
{
_eventSource.MouseDown -= this.OnMouseDown;
_eventSource.MouseUp -= this.OnMouseUp;
_eventSource.MouseMove -= this.OnMouseMove;
}
_eventSource = value;
_eventSource.MouseDown += this.OnMouseDown;
_eventSource.MouseUp += this.OnMouseUp;
_eventSource.MouseMove += this.OnMouseMove;
}
}
private void OnMouseDown(object sender, MouseEventArgs e)
{
Mouse.Capture(EventSource, CaptureMode.Element);
_position = e.GetPosition(EventSource);
_diffX = 0;
}
private void OnMouseUp(object sender, MouseEventArgs e)
{
Mouse.Capture(EventSource, CaptureMode.None);
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
Point currentPosition = e.GetPosition(EventSource);
_diffX = 0;
if (e.LeftButton == MouseButtonState.Pressed)
{
_diffX = (_position.Y - currentPosition.Y) * 0.5;
_camera.FieldOfView -= _diffX;
}
_position = currentPosition;
}
}
The assignment can be done like this in the main window:
public Window1()
{
InitializeComponent();
MouseHelper mh = new MouseHelper(camera);
mh.EventSource = viewPort;
...
}
Try it, it's a simple but nice effect, the project is attached. Have fun!