Codegod - The page for .NET-developers
Winforms Grid TreeList Control .NET 2.0/3.5

WPF tutorial 3D-Animations and Textures
Section: WPFRating: Not rated yet!

Add to YiGGAdd to google-bookmarksAdd to linkarenaAdd to redditAdd to del.icio.usAdd to misterwongAdd to digg


zip-attachment 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 1

The 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!




 Reader-Comments: