3D graphics programming is often a daunting task and can get quite hairy with all those vectors, matrices, and quaternion math, not to mention hit testing and texture mapping. Luckily, WPF provides a rich set of classes to help simplify and speed up the use of 3D in your applications. On the downside, though, you may still need to get your hands dirty with polygons to define the 3D content you want to display, such as a 3D model.
This chapter will be of most value to those who have at least a basic understanding of 3D coordinate spaces and who are familiar with mathematical concepts such as points (a position in space given as an offset from the coordinate system's origin), vectors (a direction and a magnitude), and matrices (a table of values).
Although 3D can take your user interfaces to the next level, it should be used sparingly and only where it will add value to your application. Using too much may also slow your application down on some older machines.
It is important to note that the WPF 3D graphics engine does not work like a ray-tracer where light values are calculated on a per-pixel basis, because this is very costly. Instead, the light is calculated for each vertex of a triangle and then interpolated to color the remainder of the triangle's surface. This means that although the output will have a realistic look, it won't be able to achieve the same level of realism possible with a ray-tracer. Despite this, you are still able to quickly and easily build fully interactive, 3D content right in to your application.
The recipes in this chapter describe how to:
Use 3D content in your application (recipe 10-1)
Use cameras to view your 3D models (recipe 10-2)
Render a 3D model (recipe 10-3)
Add lighting to your 3D scenes (recipe 10-4)
Deal with materials and textures of objects (recipes 10-5 and 10-6)
Interact with 3D objects, responding to user input and more (recipe 10-7)
Use existing 2D content in a 3D scene (recipe 10-8)
You need to display some 3D content in your application, be it a simple control or a complex 3D model.
Use a System.Windows.Controls.Viewport3D
control to display 3D content in your 2D application.
The Viewport3D
control is a 2D control that hosts 3D content, rendering (or projecting) the content on to its 2D surface, much like 3D objects around us are projected onto the 2D surface of a camera's viewfinder. Like the viewfinder on a standard camera displays whatever the camera is looking at, the content displayed in a Viewport3D
control is directed by a System.Windows. Media.Media3D.Camera
implementation (see recipe 10-2 for more information about the Camera
class).
The content of a Viewport3D
is set through its Children
property, a System.Windows.Media. Media3D.Visual3DCollection
. The Visual3DCollection
is a collection of objects implementing the abstract System.Windows.Media.Media3D.Visual3D
class, which currently includes System. Windows.Media.Media3D.ModelVisual3D, System.Windows.Media.Media3D.Viewport2DVisual3D
(see recipe 10-8), and System.Windows.UIElement
objects.
Viewport3D
derives from System.Windows.FrameworkElement
so provides support for user input and focus, as well as methods for performing hit tests within the control, a very useful feature indeed. If something a little lighter is needed, the System.Windows.Media.Media3D. Viewport3DVisual
is available. This derives from System.Windows.Visual
, as opposed to Viewport3D
, which derives from FrameworkElement
. When using a Viewport3DVisual
, you still get support for hit testing, but you lose built-in user input handling. A Viewport3DVisual
is good to use when displaying 3D content within a 2D control and when the view is likely to be printed.
The following XAML demonstrates how to use the Viewport3D
element in a simple scenario of displaying a single polygon (see Figure 10-1):
<Window x:Class="Recipe_10_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_01" Height="400" Width="600"> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera
LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="-1,-1,0 1,-1,0 1,1,0" TriangleIndices="0 1 2" /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial Brush="Firebrick" /> </GeometryModel3D.Material> </GeometryModel3D> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Window>
You need to be able to control and alter the characteristics of the view in a System.Windows. Controls.Viewport3D
, as well as choose the type of projection method used to render your 3D scene.
Use an implementation of the System.Windows.Media.Media3D.Camera
class, defining the location, view direction, field of view, and so on, for the camera.
Cameras are an important part of 3D graphics and control how the scene appears to the viewer when it is projected on to the image plane of the camera. This is just like the camera on a movie set that defines what the user sees on the screen in the theater.
The area of 3D space that is visible to the camera is inferred from the camera's configuration such as its location, direction it is looking, orientation, field of view, focal length near plane distance, and far plane distance. This area of space is known as the frustum; see http://en.wikipedia.org/wiki/Viewing_frustum
for more information about a view frustum.
The camera's other function is to create a view matrix, a matrix that defines how objects in the world should be transformed so that the scene appears as expected from the given view. This view matrix also contains a projection matrix, which defines how points should be transformed so that they appear according to the camera's projection type, either perspective or orthographic. WPF provides both support for both of these types of camera in the System. Windows.Media.Media3D.PerspectiveCamera
and System.Windows.Media.Media3D.OrthographicProjection
camera..
The choice of projection method will depend on how you want your 3D scene to appear. When using a perspective projection camera, parallel lines will converge giving the perception of depth, or perspective. This type of projection gives more realistic projections, with objects appearing as they do in real life. When using orthographic projection, lines that are parallel remain parallel and never converge. This type of projection is ideally suited to computer-aided design (CAD) packages, where measurements need to be accurate. Figure 10-2 shows the difference between a view rendered using a PerspectiveCamera
and an OrthographicProjection
camera.
There is a third type of camera, the MatrixCamera
, that allows a great deal of control over the way the camera constructs its view matrix. It does also mean that you need to do a great deal of work to get the camera functioning properly. By specifying a view matrix for the camera, you can define how your objects appear. For example, you may want to create a camera that simulates a fish-eye lens.
The following XAML demonstrates how to use Camera
objects and how they can affect the way in which rendered objects appear. Of the two Viewport3D
controls defined in the code, the first uses an OrthographicCamera
, and the second uses a PerspectiveCamera
. Both Viewport3D
controls contain three System.Windows.Media.Media3D.ModelVisual3D objects
, each defining a square of a different orientation and color (see Figure 10-2).
<Window x:Class="Recipe_10_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_02" Height="300" Width="300"> <Window.Resources> <!-- Front, left square --> <MeshGeometry3D x:Key="squareMeshFrontLeft" Positions="-1,0,1 1,0,1 1,1,1 −1,1,1" TriangleIndices="0 1 2 0 2 3" /> <!-- Front, right square --> <MeshGeometry3D x:Key="squareMeshFrontRight" Positions="1,0,1 1,0,-1 1,1,-1 1,1,1" TriangleIndices="0 1 2 0 2 3" /> <!-- Top square --> <MeshGeometry3D x:Key="squareMeshTop" Positions="-1,1,1 1,1,1 1,1,-1 −1,1,-1" TriangleIndices="0 1 2 0 2 3" /> <DiffuseMaterial x:Key="diffuseFrontLeft" Brush="Firebrick" /> <DiffuseMaterial x:Key="diffuseFrontRight" Brush="CornflowerBlue" /> <DiffuseMaterial x:Key="diffuseTop" Brush="OrangeRed" /> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*"/> <ColumnDefinition Width="0.5*"/> </Grid.ColumnDefinitions> <DockPanel> <TextBlock Text="Orthographic Projection" DockPanel.Dock="Bottom" HorizontalAlignment="Center" /> <Viewport3D x:Name="OrthographicView"> <Viewport3D.Camera> <OrthographicCamera Width="4" Position="10,10,10" LookDirection="-1,-1,-1" UpDirection="0,1,0" /> </Viewport3D.Camera>
<!--Front left side--> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontLeft}" Material="{StaticResource diffuseFrontLeft}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Front right side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontRight}" Material="{StaticResource diffuseFrontRight}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Top side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshTop}" Material="{StaticResource diffuseTop}" /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </DockPanel> <DockPanel Grid.Column="1"> <TextBlock Text="Perspective Projection" DockPanel.Dock="Bottom" HorizontalAlignment="Center" /> <Viewport3D x:Name="PerpesctiveView" Grid.Column="1"> <Viewport3D.Camera> <PerspectiveCamera Position="3,3,3" LookDirection="-1,-1,-1" /> </Viewport3D.Camera> <!--Front left side--> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontLeft}"
Material="{StaticResource diffuseFrontLeft}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Front right side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontRight}" Material="{StaticResource diffuseFrontRight}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Top side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshTop}" Material="{StaticResource diffuseTop}" /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </DockPanel> </Grid> </Window>
Use a System.Windows.Media.Media3D.ModelVisual3D
or a System.Windows.Media.Media3D. ModelUIElement3D
object, providing a System.Windows.Media.Media3D.GeometryModel3D
object as its content. The GeometryModel3D
will contain the data representing the model through its Geometry
property, storing a System.Windows.Media.Media3D.MeshGeometry3D
.
In 3D graphics, shapes are generally broken down into triangles; a triangle is the simplest closed shape that can be rendered in three dimensions. It might come as a surprise to find that a line is not the simplest shape, but a line is a one-dimensional object, having only a length. In 3D, a line is a series of squares or cubes, each of which is made up of several triangles. There are myriad reasons as to why a triangle was selected to be the fundamental object in 3D graphics, but it's a discussion that is beyond the scope of this recipe. For more information, see http://en.wikipedia.org/wiki/Polygon_(computer_graphics)
.
Knowing this, it will come as no surprise to learn that the data for defining a mesh (remember, a collection of tessellated triangles) is based around triangles. The way in which the data for a mesh is defined may seem odd at first but will soon seem more logical as you become accustomed to the 3D world. The first stage in defining a mesh is to provide a System.Windows.Media. Media3D.Point3DCollection
object for the mesh's Positions
property, detailing the points for each vertex of each triangle in the mesh. In XAML, the values can be defined in a list, with or without separating commas. It is important to ensure the number of 3D points defined (X, Y, and Z coordinates) is a multiple of 3. The order in which the points are defined is not important, as long as each individual point is kept together.
The second stage is to define a System.Windows.Media.Int32Collection
object for the mesh's TriangleIndicies
property, detailing the order in which the points defined in the mesh's Positions
property are to be used. Again, these values can be defined as a string of space-separated values when defined in XAML. The order of the values in this case is important and is used when determining the surface normal for the triangle. When the points of a triangle are specified in counterclockwise order, the triangle is rendered facing toward the camera, whereas triangles that are defined in clockwise order are rendered such that they are facing away from the camera.
The two preceding stages will give you a model that is ready for rendering. Once the model is given a material (see recipe 10-5), it will be visible in your 3D viewport. An additional stage of configuration is to define a list of vertex normals for the triangles you have defined. A vertex normal is given for each vertex (a point at the corner of the model where several triangles meet and is used when lighting a model. A vertex normal is defined as a normalized vector and is calculated as being the average of the surface normals of each triangle that shares a given vertex. The surface normal of a triangle is a vector that is perpendicular (at a right angle to) the face of the triangle.
If no value is defined for the Normals
property of a MeshGeometry3D
object, WPF will determine the values for you, based on the winding order of your triangles. Should you want to override the inferred values or specify your own vertex normals to achieve a desired effect or smooth out an artifact, you can provide a System.Windows.Media.Media3D.Vector3DCollection
containing the vector that describes each vertex normal. The order that vertex normals are supplied should match the order in which the positions of each vertex are given, in the Positions
property.
The following XAML demonstrates rendering some simple models within a Viewport3D
control. Four triangles are created and displayed in the Viewport3D
, with each triangle having the same color and a different rotation as the others.
<Window Background="Black" x:Class="Reipce_10_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" Loaded="Window11_Loaded"> <Window.Resources> <MeshGeometry3D x:Key="triangleMesh" Positions="-1,-1,0 1,-2,-1 1,1,0" TriangleIndices="0 1 2" /> </Window.Resources> <Viewport3D x:Name="vp"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <PointLight Position="0,-1,1" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Window>
The following code defines the content for the previous markup's code-behind file. The code defines a handler for the System.Windows.Window.Loaded
event, which is added in the previous markup. When the method is invoked, it creates four triangles and rotates them before adding them to the Viewport3D
. Figure 10-3 demonstrates the scene.
using System.Windows; using System.Windows.Media; using System.Windows.Media.Media3D;
namespace Reipce_10_03 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void Window11_Loaded(object sender, RoutedEventArgs e) { //Get a reference to the triangleMesh defined in markup MeshGeometry3D triangleMesh = (MeshGeometry3D)TryFindResource("triangleMesh"); //Create four triangles for (int i = 0; i < 4; i++) { //Create a new model and geometry object ModelVisual3D modelVisual3D = new ModelVisual3D(); GeometryModel3D geometryModel3D = new GeometryModel3D(); //Set the GeometryModel3D's Geometry to the triangleMesh geometryModel3D.Geometry = triangleMesh; //Give the model a material geometryModel3D.Material = new DiffuseMaterial(Brushes.Firebrick); //Set the content of the ModelVisual3D modelVisual3D.Content = geometryModel3D; //We want to rotate each triangle so that they overlap //and intersect RotateTransform3D rotateTransform = new RotateTransform3D(); rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0, 0, −1), i * 90); //Apply the transformation modelVisual3D.Transform = rotateTransform; //Add the new model to the Viewport3D's children vp.Children.Add(modelVisual3D); } } } }
You need to be able to set the lighting within a scene, either using a natural ambient light or replicating other types of light sources.
Use an implementation of the abstract System.Windows.Media.Media3D.Light
class to add point or directional lighting to your System.Windows.Controls.Viewport3D
. WPF provides support for the following light source types:
Ambient light (System.Windows.Media.Media3D.AmbientLight
)
Directional light (System.Windows.Media.Media3D.DirectionalLight
)
Point light (System.Windows.Media.Media3D.PointLight
)
Spotlight (System.Windows.Media.Media3D.SpotLight
)
The way a 3D scene is lit can have a huge impact on how realistic it appears to the user. All 3D scenes require some level of lighting; otherwise, you wouldn't be able to see anything. It would be in the dark. The actual type and number of lights required will depend on what you are trying to achieve. If you wanted to create a simple carousel control, you would not need anything more than some bright ambient lighting, whereas if you were creating a program for real-estate agents to provide virtual walk-throughs, lighting would be very important.
Each type of light has a Color
dependency property, enabling you to set the color of your light source to any System.Windows.Media.Color
value. It may be useful to note that because lights are actually models themselves, you are able to transform and animate them in the same way you would transform or animate other models.
Of the different types of lighting, ambient light is the simplest and can be thought of as daylight, a uniform level of light that is present in all parts of a scene. It doesn't cast any shadows but provides the most basic form of illumination for your 3D objects.
Directional lighting is a step on from ambient lighting and adds a direction, as a System. Windows.Media.Media3D.Vector3D
, into the mix. Directional light travels in the given direction with uniform coverage.
The final two types of light, PointLight
and SpotLight
, derive from System.Windows. Media.Media3D.PointLightBase
, which itself inherits from Light
. A PointLight
can be thought of as a positional light, a light source from a point in space. PointLight
objects have a Position
dependency property, of type System.Windows.Media.Media3D.Point3D
, defining the location of the light in space, from which light of the specified color is emitted uniformly in all directions.
The PointLight
and SpotLight
objects also support attenuation factors, a value that indicates the distance at which the brightness (or luminosity) of the light begins to fade. This is handy if you want to model low-power light sources that don't have an infinite range such as candles or lightbulbs.
You can also create a light source by giving a model a System.Windows.Media.Media3D.EmissiveMaterial
. An emissive material is effectively a light source where you can specify the size and shape, and it is taken into account during any lighting calculations.
The following XAML uses a series of Viewport3D
controls, each with a single polygon and different type of lighting, to demonstrate the effect that lighting has on your 3D scenes (see Figure 10-4).
<Window x:Class="Recipe_10_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_04" Height="300" Width="300" Loaded="Window1_Loaded"> <Window.Resources>
<MeshGeometry3D x:Key="triangleMesh" Positions="-1,-1,0 1,-1,-2 1,1,0" TriangleIndices="0 1 2" /> </Window.Resources> <UniformGrid> <!-- Ambient light --> <Viewport3D x:Name="vp1"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Point light --> <Viewport3D x:Name="vp2"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <PointLight Position="0,-1,1" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Directional light --> <Viewport3D x:Name="vp3"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <DirectionalLight Direction="-1,-1,-1" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Spotlight --> <Viewport3D x:Name="vp4"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <SpotLight
Range="10" Direction="0,0,-1" OuterConeAngle="25" InnerConeAngle="20" Position="0,0,9" LinearAttenuation="0.1" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </UniformGrid> </Window>
The following code defines the content of the Window1.xaml.cs
file. This code contains the handler for the System.Windows.Window.Loaded
event that was added in markup. The handler creates four triangles in each of the Viewport3D
controls defined in markup. Although the same effect could have been achieved in markup, performing the triangle generation in code keeps the markup less cluttered, drawing focus to the lighting objects.
using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Media3D; namespace Recipe_10_04 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } //Handler for the Window1.Loaded event private void Window1_Loaded(object sender, RoutedEventArgs e) { //Get a reference to the triangleMesh defined in markup MeshGeometry3D triangleMesh = (MeshGeometry3D)TryFindResource("triangleMesh"); //Create our pattern of triangles for each Viewport3D CreateTriangles(vp1, 4, triangleMesh); CreateTriangles(vp2, 4, triangleMesh); CreateTriangles(vp3, 4, triangleMesh); CreateTriangles(vp4, 4, triangleMesh); } private void CreateTriangles(Viewport3D viewport3D, int triangleCount, MeshGeometry3D triangleMesh)
{ //Create four triangles for (int i = 0; i < 4; i++) { //Create a new model and geometry object ModelVisual3D modelVisual3D = new ModelVisual3D(); GeometryModel3D geometryModel3D = new GeometryModel3D(); //Set the GeometryModel3D's Geometry to the triangleMesh geometryModel3D.Geometry = triangleMesh; //Give the model a material geometryModel3D.Material = new DiffuseMaterial(Brushes.Firebrick); //Set the content of the ModelVisual3D modelVisual3D.Content = geometryModel3D; //We want to rotate each triangle so that they overlap //and intersect RotateTransform3D rotateTransform = new RotateTransform3D(); rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0, 0, −1), i * 90); //Apply the transformation modelVisual3D.Transform = rotateTransform; //Add the new model to the Viewport3D's children viewport3D.Children.Add(modelVisual3D); } } } }
You need to be able to specify the type and characteristics of the material applied to a System.Windows.Media.Media3D.GemoetryModel3D
or System.Windows.Media.Media3D. Viewport2DVisual3D
.
Use an implementation of the abstract class System.Windows.Media.Media3D.Material
to specify the type of material to be used and characteristics such as color.
The type of material used when creating a 3D object will affect the way light interacts with the object, as well as the final color of the object and any light that is reflected. WPF provides support for three categories of material: diffuse, specular, and emissive. Each material type has a Brush
dependency property that is used to specify the color/visual used to paint the material.
The most basic and commonly used material is the diffuse material, implemented with the System.Windows.Media.Media3D.DiffuseMaterial
class. A diffuse material is one that has a very uneven surface, causing reflected light rays that strike its surface to be scattered in all directions. This scattered light uniformly spreads out over a hemisphere around the point of incident and will appear the same, regardless of the camera's position. When lighting a diffuse material, gradients are often seen where the intensity of the reflected light drops off as you move out from the point of incident, giving some very pleasing and realistic effects. Diffuse materials are used when modeling a matte surface.
A specular material is quite different from a diffuse material and is used when modeling hard, glossy objects like some plastics or metals, because specular material will show highlights where light is reflected. The amount by which a highlight is spread over the surface of the material surrounding a point of incident is configured using the SpecularPower
property of a System.Windows.Media.Media3D.SpecularMaterial
. A lower value will result in a larger spread, and a higher value will give smaller, more concentrated highlights. Specular materials also differ from diffuse materials in the way their color contributes to the overall value. Generally, these values are averaged and combined, but for a specular material, the values are additive and will add to the value of light at that point. If there is a great deal of light being reflected, the value may exceed 100 percent, in which case the material will be colored white in this area. For this reason, a specular material is almost always defined within a System.Windows.Media. Media3D.MaterialGroup
, over the top of some DiffuseMaterial
, adding any highlights that may be present.
The third type of material is an emissive material. These materials are different again from the other two materials in that objects with an emissive material will emit light evenly across its surface. Despite this, an object with an emissive material is not classed as a light source, and its contribution to the final color of a light ray is calculated differently. Like SpecularMaterial
objects, an EmissiveMaterial
is almost always used in a MaterialGroup
.
You may also notice that objects that have a Material
property also have a BackMaterial
property. During the rendering process of a 3D scene, any polygons that are facing away from the camera, that is, when the angle between the polygon's surface normal and the view direction is greater than 90 degrees, are removed because they are not visible. This process is known as back-face culling; in other words, polygons facing their backs toward the camera are culled.
This is not a problem when you have a closed, solid 3D shape, but it can be problematic when dealing with lamina objects, composed only of a single layer of polygons, such as a flag. In this instance, you may want to see the BackMaterial
property to some material that will be displayed when the model is facing away from the camera. The back material could be as simple as a mirror image of the front material or something different altogether.
The following XAML demonstrates how to use the different types of materials outlined earlier. The example contains three Viewport3D
controls, each with a single polygon, rendered with one of the material types listed earlier. This example illustrates how the material of a 3D object can affect the way it appears when rendered (see Figure 10-5).
<Window x:Class="Recipe_10_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_05" Height="300" Width="800" Loaded="Window1_Loaded"> <Window.Resources> <MeshGeometry3D x:Key="triangleMesh" Positions="-1,-1,0 1,-1,-2 1,1,0" TriangleIndices="0 1 2" /> <DiffuseMaterial x:Key="diffuseMaterial" Brush="Firebrick" /> <MaterialGroup x:Key="specularMaterial"> <StaticResource ResourceKey="diffuseMaterial" /> <SpecularMaterial Brush="White" SpecularPower="5" /> </MaterialGroup> <MaterialGroup x:Key="emissiveMaterial"> <StaticResource ResourceKey="diffuseMaterial" /> <EmissiveMaterial Color="Yellow" /> </MaterialGroup> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="20" /> </Grid.RowDefinitions> <!-- Diffuse Material --> <Viewport3D x:Name="vp1"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <PointLight Position="0,-1,2" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Specular Material --> <Viewport3D x:Name="vp2" Grid.Column="1"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <PointLight Position="0,-1,2" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Emissive Material --> <Viewport3D x:Name="vp3" Grid.Column="2"> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <PointLight Position="0,-1,2" Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <!-- Labels --> <TextBlock Text="Diffuse Material" Grid.Row="1" HorizontalAlignment="Center" /> <TextBlock Text="Specular Material" Grid.Row="1"
Grid.Column="1" HorizontalAlignment="Center" /> <TextBlock Text="Emissive Material" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center" /> </Grid> </Window>
The following code defines the content of the Window1.xaml.cs
file. This code contains the handler for the System.Windows.Window.Loaded
event that was added in markup. The handler creates four triangles in each of the Viewport3D
controls defined in markup. Although the same effect could have been achieved in markup, performing the triangle generation in code keeps the markup less cluttered, drawing focus to the lighting objects.
using System.Windows; using System.Windows.Controls; using System.Windows.Media.Media3D; namespace Recipe_10_05 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } //Handler for the Window1.Loaded event private void Window1_Loaded(object sender, RoutedEventArgs e) { //Get a reference to the triangleMesh defined in markup MeshGeometry3D triangleMesh = (MeshGeometry3D)TryFindResource("triangleMesh"); //Create our pattern of triangles for each Viewport3D CreateTriangles(vp1, 4, triangleMesh, (Material)TryFindResource("diffuseMaterial")); CreateTriangles(vp2, 4, triangleMesh, (Material)TryFindResource("specularMaterial")); CreateTriangles(vp3, 4, triangleMesh, (Material)TryFindResource("emissiveMaterial")); }
private void CreateTriangles(Viewport3D viewport3D, int triangleCount, MeshGeometry3D triangleMesh, Material material) { //Create four triangles for (int i = 0; i < 4; i++) { //Create a new model and geometry object ModelVisual3D modelVisual3D = new ModelVisual3D(); GeometryModel3D geometryModel3D = new GeometryModel3D(); //Set the GeometryModel3D's Geometry to the triangleMesh geometryModel3D.Geometry = triangleMesh; //Give the model a material geometryModel3D.Material = material; //Set the content of the ModelVisual3D modelVisual3D.Content = geometryModel3D; //We want to rotate each triangle so that they overlap //and intersect RotateTransform3D rotateTransform = new RotateTransform3D(); rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0, 0, −1), i * 90); //Apply the transformation modelVisual3D.Transform = rotateTransform; //Add the new model to the Viewport3D's children viewport3D.Children.Add(modelVisual3D); } } } }
You have a 3D model that you want to apply a texture to, giving it a rich and possibly realistic appearance.
When defining a 3D model, supply the TextureCoordinates
property of a System.Windows. Media.Media3D.MeshGeometry3D
with a System.Windows.Media.PointCollection
detailing the texture coordinates when mapping a texture on to the object. Then supply the desired texture as a System.Windows.Media.Brush
, that is, a System.Windows.Media.ImageBrush
.
Texture mapping is an age-old technique in computer graphics and is the process of applying some image or texture to a rendered object. This allows you to wrap your 3D objects in lush images, increasing the richness of the application and providing a realistic image to the viewer. Performing texture mapping is often a perilous task and involves you mapping values between coordinate systems, thereby transforming the points to fit the profile of the object they are being mapped to. Luckily, WPF does a huge amount of work for you, leaving little more than for you to specify the texture coordinates for each vertex in your model and what you want to use to paint the object.
When defining a MeshGeometry3D
object, you have the option of supplying texture coordinates as a PointCollection
. The idea is that for each vertex in the model, you specify the coordinate it maps to on the source texture. This is done by listing the 2D texture coordinates in the same order as the vertices were defined; for example, the first texture coordinate you specify in your PointCollection
will be used when texture mapping the first triangle in the model.
The texture coordinates are specified as a value between 0 and 1, inclusive, where x = 0 maps to the left of the texture image and x = 1 maps to the right of the texture image. Similarly for y, 0 maps to the top of the source image, and 1 maps to the bottom. Think of them as a ratio, describing how far across or down the source image a point should map to.
So, now that you know how to specify your texture coordinates, you need to specify the texture! In true WPF style, this process is fairly painless and is carried out using a System. Windows.Media.Media3D.Material
object. Because the Brush
property of a Material
object is a System.Windows.Media.Brush
, objects such as a System.Windows.Media.ImageBrush
and System. Windows.Media.VisualBrush
can be used. To use an image file from disk as a texture, you would create a System.Windows.Media.Media3D.DiffuseMaterial
and specify an ImageBrush
as the value for its Brush
property, setting the ImageSource
property of the ImageBrush
to the path of the image you want to display.
Should you use a transparent image or control as the brush for your model, you may want to place the texture in a System.Windows.Media.Media3D.MaterialGroup
and place a soft-colored material underneath the texture.
The following XAML demonstrates how to use Ellipse, Rectangle
, or Polygon
elements to draw simple shapes in a System.Windows.Controls.UniformGrid
(see Figure 10-6).
<Window x:Class="Recipe_10_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="Thistle" Height="400" Width="400" Title="Recipe_10_06"> <Window.Resources> <!-- Front, left square --> <MeshGeometry3D x:Key="squareMeshFrontLeft" Positions="-1,-1,1 1,-1,1 1,1,1 −1,1,1" TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <!-- Front, right square --> <MeshGeometry3D x:Key="squareMeshFrontRight" Positions="1,-1,1 1,-1,-1 1,1,-1 1,1,1" TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <!-- Top square --> <MeshGeometry3D x:Key="squareMeshTop" Positions="-1,1,1 1,1,1 1,1,-1 −1,1,-1" TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <DiffuseMaterial x:Key="textureFrontLeft"> <DiffuseMaterial.Brush> <ImageBrush ImageSource="weesam.jpg" /> </DiffuseMaterial.Brush> </DiffuseMaterial> <DiffuseMaterial x:Key="textureFrontRight"> <DiffuseMaterial.Brush> <ImageBrush ImageSource="weejayne.jpg" /> </DiffuseMaterial.Brush> </DiffuseMaterial> <MaterialGroup x:Key="textureTop"> <DiffuseMaterial Brush="Olive" /> <DiffuseMaterial> <DiffuseMaterial.Brush> <VisualBrush Stretch="Uniform"> <VisualBrush.Visual>
<Border Margin="50,0" BorderThickness="1" CornerRadius="5" BorderBrush="Firebrick"> <Border.RenderTransform> <RotateTransform Angle="-45" /> </Border.RenderTransform> <TextBlock Text="I am a VisualBrush!" /> </Border> </VisualBrush.Visual> </VisualBrush> </DiffuseMaterial.Brush> </DiffuseMaterial> </MaterialGroup> </Window.Resources> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera Position="4,3.5,4" LookDirection="-1,-0.7,-1" /> </Viewport3D.Camera> <!--Front left side--> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontLeft}" Material="{StaticResource textureFrontLeft}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Front right side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshFrontRight}" Material="{StaticResource textureFrontRight}" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Top side --> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D Geometry="{StaticResource squareMeshTop}" Material="{StaticResource textureTop}" /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content>
<AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Window>
You need to detect when your 3D objects are clicked with the mouse or when the mouse is placed over the object. This includes clicking objects that overlap but at different distances from the camera.
Build your 3D models as System.Windows.Media.Media3D.ModelUIElement3D
objects in a System.Windows.Controls.Viewport3D
. The ModelUIElement3D
object provides support for input, focus, and the associated events.
The ModelUIElement3D
object is very similar to the ModelVisual3D
object, with both classes descending from System.Windows.Media.Media3D.Visual3D
, although ModelUIElement
provides the added richness that is user input and focus handling. This extra functionality isn't quite free, though, because it will add overhead to your 3D scene. If performance is key to your application, you may want to implement your own user input handling, implementing only the functionality you require.
Harnessing the extra functionality is as simple as adding event handlers to the required events and executing your custom code. This enables you to add things like tool tips or apply animations to your models, something not possible in XAML because ModelVisual3D
objects and its descendents do not support triggers.
When handling user input in a scene with more than one model, the distance that an object is from the camera will be taken into consideration when determining which object was clicked. This means that if you have two objects that overlap each other but are positioned at different depths, the object closest to the camera, and only that object, will receive the event.
If two or more objects overlap and the mouse click event is at a point where both objects are at the same depth, you will encounter z-fighting! This is where two pixels at the same depth may be selected at random in a nondeterministic fashion and will be particularly noticeable in animation.
The following XAML demonstrates how to use handling user input events on layered objects in a 3D scene. A single Viewport3D
control contains three polygons as ModelUIElement3D
objects, with handlers on each of the polygon's MouseDown
events. Observe how it doesn't matter where on the foremost triangle you click; the polygon1_MouseDown
method is invoked as polygon1
is closer to the camera than the other two polygons in the scene (see Figure 10-7).
<Window x:Class="Recipe_10_07.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_07" Height="300" Width="300"> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> <!-- Polygon 1 --> <ModelUIElement3D MouseDown="polygon1_MouseDown"> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="-1,-1,1 1,-1,1 1,1,1"
TriangleIndices="0 1 2" /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial Brush="Firebrick" /> </GeometryModel3D.Material> </GeometryModel3D> </ModelUIElement3D> <!-- Polygon 2 --> <ModelUIElement3D MouseDown="polygon2_MouseDown"> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="1,-1,0 1,1,0 −1,1,0" TriangleIndices="0 1 2" /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial Brush="CornflowerBlue" /> </GeometryModel3D.Material> </GeometryModel3D> </ModelUIElement3D> <!-- Polygon 3 --> <ModelUIElement3D MouseDown="polygon3_MouseDown"> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions="1,0,0 1,1,0 0,1,0" TriangleIndices="0 1 2" /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial Brush="OrangeRed"/> </GeometryModel3D.Material> </GeometryModel3D> </ModelUIElement3D> </Viewport3D> </Window>
The following code defines the content of the code-behind for the previous markup. The code defines the three event handlers that are added in the markup.
using System.Windows; using System.Windows.Input; namespace Recipe_10_07 { public partial class Window1 : Window { public Window1()
{ InitializeComponent(); } private void polygon1_MouseDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("polygon1_MouseDown", "Recipe_10_07"); } private void polygon2_MouseDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("polygon2_MouseDown", "Recipe_10_07"); } private void polygon3_MouseDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("polygon3_MouseDown", "Recipe_10_07"); } } }
You need to use some of the standard 2D controls such as System.Windows.Controls.Button
or System.Windows.Controls.TextBox
in a 3D scene, allowing the control to be fully interactive.
The Viewport2DVisual3D
control is used to host 2D content in a 3D content control, complementing the System.Windows.Media.Media3D.Viewport3DVisual
control that hosts 3D content within a 2D visual. This is a very powerful feature that enables you to easily build a powerful and rich 3D user interface, retaining the use of 2D controls such as System.Windows.Controls.Button
objects and System.Windows.Controls.TextBox
objects.
When using 2D content in a 3D visual, WPF can carry out coordinate system transformations, mapping the position of any input events such as mouse clicks in to their 2D equivalents. This is great if you are displaying a custom control with multiple interactive regions or child controls because you are able to process user interaction in the same way you would normally in 2D.
The System.Windows.Media.Media3D.Viewport2DVisual3D
class was introduced in .NET 3.5.
The following XAML demonstrates how to use 2D content in a 3D model by rendering various standard controls on to the faces of three squares, which are joined together to form the visible half of a cube (see Figure 10-8). Notice how all the controls respond to user input such as hover states, click states, and so on. When the button is clicked, a message is displayed that shows the location at which the mouse was pressed, relative to the button's own coordinate system, implicitly projecting and transforming the 3D point into 2D.
<Window x:Class="Recipe_10_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Recipe_10_08" Height="300" Width="300"> <Window.Resources> <!-- Front, left square --> <MeshGeometry3D x:Key="squareMeshFrontLeft" Positions="-1,-1,1 1,-1,1 1,1,1 −1,1,1"
TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <!-- Front, right square --> <MeshGeometry3D x:Key="squareMeshFrontRight" Positions="1,-1,1 1,-1,-1 1,1,-1 1,1,1" TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <!-- Top square --> <MeshGeometry3D x:Key="squareMeshTop" Positions="-1,1,1 1,1,1 1,1,-1 −1,1,-1" TriangleIndices="0 1 2 0 2 3" TextureCoordinates="0,1 1,1 1,0 0,0" /> <DiffuseMaterial x:Key="visualHostMaterial" Brush="White" Viewport2DVisual3D.IsVisualHostMaterial="True" /> </Window.Resources> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera Position="4,2.5,4" LookDirection="-1,-0.7,-1" /> </Viewport3D.Camera> <Viewport2DVisual3D Material="{StaticResource visualHostMaterial}" Geometry="{StaticResource squareMeshFrontLeft}"> <StackPanel> <Slider /> <Button Click="Button_ClickMe_Click" > <DockPanel> <Ellipse Width="20" Height="20" Stroke="Black" Fill="Purple" DockPanel.Dock="Right" /> <TextBlock VerticalAlignment="Center" Text="Click me!" /> </DockPanel> </Button> </StackPanel> </Viewport2DVisual3D> <Viewport2DVisual3D Material="{StaticResource visualHostMaterial}" Geometry="{StaticResource squareMeshFrontRight}"> <TextBox
Text="This is a TextBox!" AcceptsReturn="True" Width="200" Height="200" /> </Viewport2DVisual3D> <Viewport2DVisual3D Material="{StaticResource visualHostMaterial}" Geometry="{StaticResource squareMeshTop}"> <StackPanel> <RadioButton GroupName="rgTest" IsChecked="True" Content="RadioButton 1" /> <RadioButton GroupName="rgTest" Content="RadioButton 2" /> <RadioButton GroupName="rgTest" Content="RadioButton 3" /> <CheckBox IsChecked="True" Content="CheckBox 1" /> <CheckBox IsChecked="True" Content="CheckBox 2" /> <CheckBox IsChecked="True" Content="CheckBox 3" /> <ComboBox> <ComboBox.Items> <ComboBoxItem Content="Item 1" /> <ComboBoxItem Content="Item 2" /> <ComboBoxItem Content="Item 3" /> </ComboBox.Items> </ComboBox> </StackPanel> </Viewport2DVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight Color="White" /> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Window>
The following code defines the content of the Window1.xaml.cs
file:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_10_08 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window
{ public Window1() { InitializeComponent(); } private void Button_ClickMe_Click(object sender, RoutedEventArgs e) { //Get the position of the mouse, relative to the //button that was clicked. Point? position = Mouse.GetPosition(sender as Button); //Build a message string to display to the user. string msg = string.Format("Wow, you just clicked a " + "2D button in 3D!{0}{0}You clicked the button at" + " x = {1}, y = {2}", Environment.NewLine, (int)position.Value.X, (int)position.Value.Y); MessageBox.Show(msg, "Recipe_10_08"); } } }