您的位置:首页 > 其它

WPF 3D Primer

2014-11-02 13:57 246 查看
http://www.codeproject.com/Articles/23332/WPF-3D-Primer

Alternatively, you can
update your subscriptions.

Download source and demo project - 25.38 KB



Introduction

In the last couple of days, I had to evaluate the possibility to build a GUI displaying a solid shape to the user, allowing her or him to perform some basic operations, above all rotating and zooming the view.

I'm no expert in 3D graphics, this was my first contact with this matter, and I've never used Direct3D or OpenGL, so things had to be extremely simple.

WPF 3D seemed to be the quickest way to build a 3D interface, so I decided to create a small application that just gives the user the ability to rotate and zoom the view on a single solid object.

Although I'm really happy with the result, I'm probably doing some newbie mistakes here, so feel free to point them out.

Requirements

In order to build this application, we need:

Visual Studio 2008 Professional (Visual C# 2008 Express should be sufficient)

.NET Framework 3.5 (installed along with Visual Studio)
Basic WPF knowledge
Basic trigonometry knowledge
If you are new to WPF, I strongly suggest you read the excellent articles by
Josh Smith. You can also take a look at the
introductory MSDN article and the part focused on
3D graphics.

Step 0 - The Basics

There is not much to know on WPF 3D, except for a couple of facts:

It’s based on Direct3D, and therefore it takes advantage of graphic cards (as WPF 2D does)

Full-scene anti-aliasing is disabled by default on Windows XP and enabled by default on Windows Vista (if the graphic card’s driver is

WDDM-Compliant)
The most important thing to note, however, is the different coordinate system that WPF 3D uses, as shown in the figure below:


This difference requires that most interactions with the user (namely mouse events) perform coordinate conversions. As we'll see later on, this operation is actually quite simple.

Step 1 - Creating the Visual Studio Project

This step is easy, you just need to create a new WPF Application project in Visual Studio, targeting the .NET Framework 3.5. Take a look at the figure below:


Step 2 - Preparing the Main Window

Modify the Window1.xaml file that the Project Wizard created for you, setting the window title and dimensions to something that fits your preferences. Since this is a test application, we won't care much about object names and such, but I renamed
the main window file to MainWindow.xaml, checking that all references are correct (especially in the
App.xaml file created automatically).

Note: While writing XAML, we will add the
x:Name
attribute only to elements we want to access programmatically from the code-behind, leaving all the others unnamed.


Collapse |
Copy Code
<Window x:Class="Wpf3DTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF 3D Test"
Height="400" Width="400">
<Grid>
</Grid>
</Window>

WPF 3D is based on the
Viewport3D
UI element, which is in charge of rendering the 3D scene on the screen, so we'll surely need an instance of that object.

We also want the ability to reset the view to its original “status” after the user rotated or zoomed the scene, so we'll also need a button.

For building the window layout, we'll use a grid with 2 rows. The second row will be sized automatically by WPF to occupy as much vertical space as possible, while the first will just wrap its content. We can now add the
Viewport3D
and the
Button
we need.


Collapse |
Copy Code
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Button x:Name="button" Grid.Row="0" Content="Reset" />

<Viewport3D x:Name="viewport" Grid.Row="1">
</Viewport3D>
</Grid>

The XAML above will result in something like this (notice the black-background grid, the button at the top and the empty
viewport
filling the window):


Step 3 - The Camera

Every 3D scene in WPF should have a camera, otherwise it won't be rendered on the screen. Given that we won't need to add or remove it at runtime, we can just use a bit of XAML.


Collapse |
Copy Code
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camera" FarPlaneDistance="50"
NearPlaneDistance="0" LookDirection="0,0,-10" UpDirection="0,1,0"
Position="0,0,5" FieldOfView="45" />
</Viewport3D.Camera>

The XAML above adds a
PerspectiveCamera
(although there are
other types of cameras)

inside the
Viewport3D
element.
FarPlaneDistance
and
NearPlaneDistance
represent the range within which the camera will display elements. When an element is too far from or too close to the camera, it won't be displayed.
FieldOfView
can be safely set to
45
in most cases, giving us a natural perspective view.
LookDirection
is the point the camera “looks at”: we set it to a point with a negative Zcoordinate.
UpDirection
is the vertical axis of the camera: we set it to be coincident with the Y axis.

Step 4 - Preparing the 3D Model

The
content
of the
viewport
is described using an instance of the
ModelVisual3D
UI element (MSDN) inheriting from the abstract class
Model3D
. Inside this object, we can basically add geometries (including their materials) and lights. Given that our application might want to load 3D models from an external source, we now only add the lights using XAML, leaving the rest of the
work for the code-behind.

There are
3 types of light sources in WPF: we'll now add two types of them, as shown in the markup below:


Collapse |
Copy Code
<ModelVisual3D x:Name="model">
<ModelVisual3D.Content>
<Model3DGroup x:Name="group">
<AmbientLight Color="DarkGray" />
<DirectionalLight Color="White" Direction="-5,-5,-7" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>

The actual contentof the model is specified inside the
ModelVisual3D.Content
element. Since we have more than one item, we wrap all of them with a
Model3DGroup
element (MSDN).The XAML above adds a dark grey (i.e. a very dim light)
AmbientLight
and a white
DirectionalLight
, pointing in the point (-5, -5, -7) (directional lights don't have a position, they just “look” towards the specified direction).

The two light sources we used give the scene a good illumination, but feel free to experiment new configurations yourself.

Mesh Basics

A mesh is a 3D object built using only triangles. Each triangle has obviously three 3D vertices, combined together to form a small surface called a
facet.

In WPF 3D (and possibly Direct3D and/or OpenGL, as I said I'm not an expert) a facet has a
direction, that defines the side the facet will be visible from, as you can see in the figure below:


Although you can explicitly specify the normals of each vertex (not triangle) composing a facet (using the
Normals
property of the
MeshGeometry3D
class), WPF can automatically calculate them using the sequence used to add the vertices. If we add the vertices in counterclockwise order, the faced direction will “point towards us”, as shown
in the figure above: vertices are labelled 0, 1 and
2, and the direction of the faced is represented by the arrow labelled “+”. We add them in the order
0, 1, 2.

Adding the 3D Geometry

In our test application, we're going to add the 3D object programmatically from the code-behind C# file.

We'll create the solid shown in the figure below (a truncated pyramid) by adding its 8 vertices and then defining the 12 triangles needed to define all its faces.



We define the
BuildSolid
method in the code-behind file MainWindow.xaml.cs (or
Window1.xaml.cs if you didn't rename it). This method is then invoked in the constructor of the class, and it just adds the vertices one-by-one, in the order specified in the figure (this is not actually a requirement, but it makes things more clear).


Collapse |
Copy Code
// Define 3D mesh object
MeshGeometry3D mesh = new MeshGeometry3D();
// Front face
mesh.Positions.Add(new Point3D(-0.5, -0.5, 1));
mesh.Positions.Add(new Point3D(0.5, -0.5, 1));
mesh.Positions.Add(new Point3D(0.5, 0.5, 1));
mesh.Positions.Add(new Point3D(-0.5, 0.5, 1));
// Back face
mesh.Positions.Add(new Point3D(-1, -1, -1));
mesh.Positions.Add(new Point3D(1, -1, -1));
mesh.Positions.Add(new Point3D(1, 1, -1));
mesh.Positions.Add(new Point3D(-1, 1, -1));

Although we used the term “face”, it’s worth noting that we still don't have any real face defined. In order to do that, we have to build the triangles that compose each face of the solid from the points we just created: we add each triangle declaring its
vertices in the proper order, so that the direction of each facet points outwards the solid.


Collapse |
Copy Code
// Front face
mesh.TriangleIndices.Add(0);
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(3);
mesh.TriangleIndices.Add(0);
// Back face
mesh.TriangleIndices.Add(6);
mesh.TriangleIndices.Add(5);
mesh.TriangleIndices.Add(4);
mesh.TriangleIndices.Add(4);
mesh.TriangleIndices.Add(7);
mesh.TriangleIndices.Add(6);
// Right face
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(5);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(5);
mesh.TriangleIndices.Add(6);
mesh.TriangleIndices.Add(2);
// Other faces (see complete source code)...

As you can see, we defined 12 triangles using only 8 vertices. The only thing we have to do now is to actually add the mesh to the scene, wrapping it in an instance of
GeometryModel3D
.

For the purposes of the application, we'll need a reference to the geometry for later use, so we add a new
private
member to the class.


Collapse |
Copy Code
// Reference to the geometry for later use
private GeometryModel3D mGeometry;

Then, we can complete the
BuildSolid
method with the following code, which creates the geometry object and adds it to the
Model3DGroup
(named
group
) seen a while ago in the XAML of the main window.


Collapse |
Copy Code
// Geometry creation
mGeometry = new GeometryModel3D(mesh, new DiffuseMaterial(Brushes.YellowGreen));
mGeometry.Transform = new Transform3DGroup();
group.Children.Add(mGeometry);

As you can see, we used a simple
DiffuseMaterial
to color the solid, but you can take a look at the different options that WPF 3D offers in this field (see

MSDN). We also set the
Transform
property to a new instance of the
Transform3DGroup
class, which is a container for transform objects, as we'll see later.

Now, if we hit the F5 button, we should see something like this:


The main window now displays our test solid, front facing. Try to resize the window and see how WPF seamlessly scales the
viewport
and the solid.

Step 5 - Implementing the Zoom Functionality

For the sake of simplicity, we're going to implement the zoom functionality so that it will be accessible only using the mouse wheel. We just need to implement a handler for the
MouseWheel
event of the main
grid
element in the window. In this case,
IntelliSense is our friend:

Let the IntelliSense popup appear, and hit enter. Visual Studio should have created a handler named
Grid_MouseWheel
.

The handler has to do one thing only: move our camera on the Zaxis. Binding this movement with the wheel scroll value is quite tricky and after some trial-and-error, I found that the following code works well:


Collapse |
Copy Code
private void Grid_MouseWheel(object sender, MouseWheelEventArgs e) {
camera.Position = new Point3D(
camera.Position.X,
camera.Position.Y,
camera.Position.Z- e.Delta / 250D);
}

We don't touch the
X
and
Y
position of the camera, we just modify its position on the
Z
axis.

We also want to let the Reset button we defined in XAML reset the camera position, so we add a handler for its
Click
event (in the same way we added the handler for the
grid
’s
MouseWheel
event), and add the following code to it, which resets the Zvalue of the camera position to
5
:


Collapse |
Copy Code
private void Button_Click(object sender, RoutedEventArgs e) {
camera.Position = new Point3D(
camera.Position.X,
camera.Position.Y, 5);
}

Just in case you're curious, the XAML code for the
button
element is updated as follows.


Collapse
| Copy Code
<Button x:Name="button" Grid.Row="0" Content=" Reset"Click="Button_Click" />

You can now hit F5 again, verifying that zooming works and that the Reset button does its work properly.

Step 6 - Implementing the 3D Rotation Functionality

We want now to enable the user to rotate the solid with her mouse, very much like CAD applications. This is the toughest part of the demo, but with a bit of trigonometry we should get away quite quickly.

The difficult part of this task is to map a 2D vector (the mouse movement) to a 3D rotation of the solid. It took me a while to figure it out, but the mouse movement vector can be easily converted to a rotation angle, applied to the solid around a rotation
axis that is coplanar with the screen and perpendicular to the mouse movement vector, as shown in the figure below.


The rotation axis is pinned in the origin (0,0,0) because the solid is centered in the origin itself, and both the mouse movement vector and the rotation axis have the
Z
coordinate set to zero, so they are coplanar with the
XY
plane.

The rotation axis has a direction, indicated by the arrow in the previous figure. Angles are always calculated in clockwise direction, so if you rotate the solid with a positive angle, it will rotate leftwards:


In order to properly calculate the rotation axis, we first need to calculate the angle of the mouse movement vector. Basic trigonometry tells us that the angle,
alpha, is computed as follows:


We also need to take care of the sign of
dx
and
dy
(mouse position delta in
X
and
Y
), as we'll see directly in the code.

In order to implement the rotation function, we need three more handlers for the
MouseDown
,
MouseUp
and
MouseMove
events of the main
grid
:


Collapse
| Copy Code
<Grid Background="Black" MouseWheel="Grid_MouseWheel"
MouseDown="Grid_MouseDown" MouseUp="Grid_MouseUp"
MouseMove="Grid_MouseMove">

We also need a couple of variables in the
MainWindow
class (besides the previously defined
mGeometry
):


Collapse
| Copy Code
private GeometryModel3D mGeometry;
private bool mDown;
private Point mLastPos;

mDown
is
true
when the mouse left button is pressed,
false
otherwise.
mLastPos
contains the last position of the mouse pointer relative to the
viewport
(this is very important, as we're going to see).

The
MouseDown
and
MouseUp
event handlers are quite simple, they just toggle
mDown
.


Collapse
| Copy Code
private void Grid_MouseUp(object sender, MouseButtonEventArgs e) {
mDown = false;
}

private void Grid_MouseDown(object sender, MouseButtonEventArgs e) {
if(e.LeftButton != MouseButtonState.Pressed) return;
mDown = true;
Point pos = Mouse.GetPosition(viewport);
mLastPos = new Point(
pos.X- viewport.ActualWidth / 2,
viewport.ActualHeight / 2 - pos.Y);
}

MouseDown
actually does a little more: it stores the first position of the mouse just after its left button is pressed. The position is relative to the
viewport
(
Mouse.GetPosition
does that), and it is then converted from the 2D coordinate system to the 3D coordinate system, as mentioned at the beginning of the article: the
X
and
Z
coordinates must be adjusted using the
ActualWidth
and
ActualHeight
properties of the
viewport
element.

The
MouseMove
handler has a lot of work to do. See the complete code:


Collapse
| Copy Code
private void Grid_MouseMove(object sender, MouseEventArgs e) {
if(!mDown) return;
Point pos = Mouse.GetPosition(viewport);
Point actualPos = new Point(
pos.X- viewport.ActualWidth / 2,
viewport.ActualHeight / 2 - pos.Y);
double dx = actualPos.X- mLastPos.X;
double dy = actualPos.Y - mLastPos.Y;
double mouseAngle = 0;

if(dx != 0 && dy != 0) {
mouseAngle = Math.Asin(Math.Abs(dy) /
Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)));
if(dx < 0 && dy > 0) mouseAngle += Math.PI / 2;
else if(dx < 0 && dy < 0) mouseAngle += Math.PI;
else if(dx > 0 && dy < 0) mouseAngle += Math.PI * 1.5;
}
else if(dx == 0 && dy != 0) {
mouseAngle = Math.Sign(dy) > 0 ? Math.PI / 2 : Math.PI * 1.5;
}
else if(dx != 0 && dy == 0) {
mouseAngle = Math.Sign(dx) > 0 ? 0 : Math.PI;
}

double axisAngle = mouseAngle + Math.PI / 2;

Vector3D axis = new Vector3D(
Math.Cos(axisAngle) * 4,
Math.Sin(axisAngle) * 4, 0);

double rotation = 0.02 *
Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2));

Transform3DGroup group = mGeometry.Transform as Transform3DGroup;
QuaternionRotation3D r =
new QuaternionRotation3D(
new Quaternion(axis, rotation * 180 / Math.PI));group.Children.Add(new RotateTransform3D(r));

mLastPos = actualPos;
}

The method first checks if the mouse left button is pressed; if not, it exists. The mouse
dx
and
dy
values are then calculated using the previous position stored in
mLastPos
, converting the current position to the 3D coordinate system as shown for the
Grid_MouseDown
event handler.

The angle of the mouse movement vector is then calculated with the formula we saw before, with three different cases:

If
dx
and
dy
are both different from zero,
mouseAngle
is computed using the absolute value of
dy
, and then it’s corrected according to the sign of both
dx
and
dy
.
If
dx
is zero,
mouseAngle
must be either 90 or 270 degrees (we use the sign of
dy
).
If
dy
is zero,
mouseAngle
must be either 0 or 180 degrees (we use the sign of
dx
).
The angle of the rotation axis (
axisAngle
) is calculated adding 90 degrees to the mouse movement vector angle. Keep in mind that these angles are always referred to the
X
axis.


The rotation
axis
(instance of
Vector3D
) is then calculated from the
axisAngle
, leaving the
Z
coordinate to zero.

The rotation
angle
is calculated as the module of the mouse movement angle multiplied for a factor of
0.01
, producing a smooth rotation:


Collapse
| Copy Code
double rotation = 0.01 * Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2));

The only thing we have to do now is to apply a transformation to the geometry. Since we want to rotate the solid (but it’s possible to do

many other things), we have to add an instance of
RotateTransform3D
to the
Transform3DGroup
collection we stored in the
Transform
property of the geometry (have a look at the
BuildSolid
method). We cast the collection and store it in the
group
variable.

The constructor of
RotateTransform3D
needs an instance of a class inheriting from
Rotation3D
describing the rotation to apply (see
MSDN). Since we want to apply a rotation around an axis and by a given angle, we use
QuaternionRotation3D
and we instantiate it with
axis
and the
rotation
angle (in degrees, not in radians).

So far I omitted a detail. The rotation axis is not sufficient to define the actual axis of the rotation, because it also needs a center which is, in our case, the origin. The constructor of
RotateTransform3D
accepts an optional parameter that allows to specify the center of the rotation. If omitted, the origin is used.

Finally, we can add our transform to the transform collection:


Collapse
| Copy Code
group.Children.Add(new RotateTransform3D(r));

Again, there is a fundamental detail to note. Every transform we apply to the solid is absolute. This means that if we apply two transforms in sequence, the latter overrides the first. For this reason, we use a
Transform3DGroup
(inheriting from
Transform3D
) to store subsequent transforms, one for every mouse movement (as you can imagine, this constitutes a memory leak, but for our testing purposes, it is not a problem).

The last thing to do is to store the current mouse position in the
mLastPos
variable, so that the next mouse move will compute correct delta
X
/
Y
values.

We can now update the behavior of the Reset button so that, besides resetting the position of the camera, it also removes all the transforms applied to the solid (mitigating the memory leak we mentioned above).


Collapse
| Copy Code
private void Button_Click(object sender, RoutedEventArgs e) {
camera.Position = new Point3D(
camera.Position.X,
camera.Position.Y, 5);
mGeometry.Transform = new Transform3DGroup();
}

Now you're ready to hit F5 again and see the final result. If you are running Windows Vista, you can also admire the full-scene anti-aliasing filter applied to the edges of the solid.


Conclusion

Although there is still much work to do before obtaining a full-featured 3D user interface, we explored some interesting features of WPF 3D and we are a little more aware of its advantages and problems.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: