Perspective 0.5 : Easy 3D programming with WPFPerspective 0.5 : Easy 3D programming with WPF
A few years ago, I was impressed by the ease of 3D programming with VPython . This library makes it possible to build in language Python 3D models starting from basic (cube, cylinder, sphere, etc.) or composite (arrow) geometrical objects, and to anime them. The default visualization window allows to move the camera around the scene, and to zoom. Thus it is easy to get with a few lines of code a very spectacular application. VPython is very much used to illustrate physical models. Example .
More recently, I discovered POV-Ray and its specialized language for creating 3D scenes starting from objects presets. To the ease of implementation is added a good graphic quality and the realism of the resulting images, because POV-Ray exploits the technique of ray tracing .
WPF uses Direct3D technology to provide creation services for 2D user interface but also for 3D scenes. You can find many references about this subject like this tutorial , who describes the different steps of the creation of a cube. Even if WPF is much easier to program than Direct3D, we are still far from the VPython productivity. WPF misses high-level classes for the 3D programming (cube, cylinder, sphere, etc.). Several authors make this constatation and give some solutions (of which however none goes as far as VPython) :
While inspiring me by these articles, in particular those of Daniel Lehenbauer , I thus created in 2006 the first version (0.1) a Wpf3D library which offers to WPF some functionalities and a productivity close of those of VPython.
In the .NET 3.5 framework, the 3D evolves . In particular, the UIElement3D class brings a functional level which makes it possible to develop more easily interactive 3D models. Its functional level is close to that of UIElement in the 2D WPF world.
By integrating Microsoft WPF 3D team advices , I thus created a new version (0.3) of the Wpf3D library in which the visual classes inherit from UIElement3D . In addition, the code was refactored in depth, and the classes are easier to use now. . Wpf3D has been integrated to a larger project called Perspective, hosted on Codeplex .
Version 0.4 of Perspective introduces the concept of 3D interactive control, presented forward in this document. Version 0.5 introduces a new XML namespace for Perspective.
The continuation of the article applies to the version 0.5 of Perspective, which requires .NET 3.5 and Visual Studio 2008.
Version of the article concerning the version 0.3 of Perspective
Version of the article concerning the version 0.4 of Perspective
For .NET 3.0 or Visual Studio 2005, please read the article concerning the version 0.1 of Wpf3D .
Wpf3D Basics
Getting and installing Wpf3D
To use the Wpf3D classes in a Xaml file, you have to define 2 XML namespaces referring the 2 Wpf3D assemblys :
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:p="http://www.codeplex.com/perspective"
Title="Window1" WindowState="Maximized">A 3D scene is displayed in a Viewport3D object, from the System.Windows.Controls namespace.
<DockPanel>
<Viewport3D Name="vMain">
</Viewport3D>
</DockPanel>Then we have to configure the camera and lights :
<Viewport3D Name="vMain">
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection="-10.0, -2.0, -10.0"
NearPlaneDistance="1.0"
FarPlaneDistance="100.0"
Position="10.0, 3.0, 10.0"
FieldOfView="60.0"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="#FFFFFFFF" Direction="-10, 0, -15" />
<DirectionalLight Color="#FFFFFFFF" Direction="10, 0, 15" />
<DirectionalLight Color="#FFFFFFFF" Direction="0, -10, 0" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>We can now instanciate our first visual Wpf3D object : the coordinate system (XyzAxis3D class) which will visually help us to position our 3D elements :
<Viewport3D Name="vMain">
...
<p:XyzAxis3D />
</Viewport3D>If we execute now the application, we can see the coordinate system.

The main properties of XyzAxis3D are : Radius, radius of each body axe, Length, size of each axis, and Signed which dislays the negative axis.
<Viewport3D Name="vMain">
...
<p:XyzAxis3D Length="5.0" />
</Viewport3D>
In order to be able to move the point of view and to turn around the model, rotations of the camera (RotateTransform3D) around the X, Y and Z axis are bound to 3 sliders on a 2D panel on the right. To simulate a zoom effect, a scaling (ScaleTransform3D) is applied to the camera and is bound to the bottom slider.
<Window.Resources>
...
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="slider">
<Setter Property="Slider.Orientation" Value="Vertical" />
<Setter Property="Slider.Height" Value="130.0" />
<Setter Property="Slider.HorizontalAlignment" Value="Center" />
<Setter Property="Slider.VerticalAlignment" Value="Center" />
</Style>
</Window.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Right" Width="100.0" Background="LightGray">
<GroupBox Header="Rotation" Margin="4.0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="X" Grid.Column="0" Grid.Row="0"/>
<TextBlock Text="Y" Grid.Column="1" Grid.Row="0"/>
<TextBlock Text="Z" Grid.Column="2" Grid.Row="0"/>
<Slider x:Name="sliderX" Grid.Column="0" Grid.Row="1" Minimum="-180.0" Maximum="180.0" Style="{StaticResource slider}"/>
<Slider x:Name="sliderY" Grid.Column="1" Grid.Row="1" Minimum="-180.0" Maximum="180.0" Style="{StaticResource slider}"/>
<Slider x:Name="sliderZ" Grid.Column="2" Grid.Row="1" Minimum="-180.0" Maximum="180.0" Style="{StaticResource slider}"/>
</Grid>
</GroupBox>
<GroupBox Header="Zoom" Margin="4.0">
<Slider x:Name="sliderZoom" IsDirectionReversed="True" Minimum="0.1" Maximum="3.0" Value="1.0" Style="{StaticResource slider}" />
</GroupBox>
</StackPanel>
<Viewport3D Name="vMain">
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection="-10.0, -2.0, -10.0"
NearPlaneDistance="1.0"
FarPlaneDistance="100.0"
Position="10.0, 3.0, 10.0"
FieldOfView="60.0">
<PerspectiveCamera.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="1.0, 0.0, 0.0"
Angle="{Binding ElementName=sliderX, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="0.0, 1.0, 0.0"
Angle="{Binding ElementName=sliderY, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="0.0, 0.0, 1.0"
Angle="{Binding ElementName=sliderZ, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleX="{Binding ElementName=sliderZoom, Path=Value}"
ScaleY="{Binding ElementName=sliderZoom, Path=Value}"
ScaleZ="{Binding ElementName=sliderZoom, Path=Value}"/>
</Transform3DGroup>
</PerspectiveCamera.Transform>
</PerspectiveCamera>
</Viewport3D.Camera>
...
We can now begin to create our models. Let's start with a cube using the Box3D class:
<Viewport3D Name="vMain">
...
<p:Box3D />
</Viewport3D>
Each Wpf3D object has a default material, which can be modified using the Material property.
Our cube is positioned at the origin of the coordinate system, and the edges size is of 1 unit. While reading the documentation, it may seem surprising that no property enables us to modify the position and the size of the cube. But that is completely normal. WPF uses the graphics processor (GPU) which supports instructions to transform the position or the aspect of the models. Modifying the position and the size of the cube in our code would use the main processor (CPU) instead of the GPU. To respect the WPF logic, we will use 3D transformations and assign them to the Transform property (always with the possibility of defining the transformation in the resources centralized zone) :
<Window x:Class="WpfApplication1.Window1"
...
<Window.Resources>
<Transform3DGroup x:Key="modelTransform">
<TranslateTransform3D OffsetX="1.0" OffsetY="-1.0" OffsetZ="0.5" />
<ScaleTransform3D ScaleX="1.0" ScaleY="1.0" ScaleZ="3.0" />
</Transform3DGroup>
</Window.Resources>
<DockPanel>
...
<Viewport3D Name="vMain">
...
<p:Box3D Transform="{StaticResource modelTransform}"/>
</Viewport3D>
...
This principle applies to all the basic models delivered in Wpf3D. Their basic dimension is the unit, and they are based on the origin of the coordinate system (the point of coordinates 0.0, 0.0, 0.0).
Interactivity
Because the Perspective 3D objects are inheriting from UIElement3D, they support keyboard and mouse interactions, and they have the corresponding events. The following example shows how to trigger a rotation of the 3D model when the mouse is over it, through the use of the events MouseEnter and MouseLeave :
<p:Box3D
MouseEnter="Box3D_MouseEnter"
MouseLeave="Box3D_MouseLeave">
<p:Box3D.Transform >
<RotateTransform3D CenterX="0.5" CenterZ="0.5">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
x:Name="boxRotation"
Axis="0.0, 1.0, 0.0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</p:Box3D.Transform>
</p:Box3D>
private void Box3D_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
// Initialization of the animation object : on 360 degrees after the current angle
DoubleAnimation da = new DoubleAnimation(boxRotation.Angle + 360.0, new Duration(TimeSpan.FromSeconds(5.0)));
da.RepeatBehavior = RepeatBehavior.Forever;
// Trigger the animation on the boxRotation's Angle property
boxRotation.ApplyAnimationClock(AxisAngleRotation3D.AngleProperty, da.CreateClock());
}
private void Box3D_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
// current angle memorization
double currentAngle = boxRotation.Angle;
// Stop the animation
boxRotation.ApplyAnimationClock(AxisAngleRotation3D.AngleProperty, null);
// Reassignment of the angle (which otherwise resumes its initial value)
boxRotation.Angle = currentAngle;
}Flat models
The Square3D defines... a 2D square !
<p:Square3D />

It is possible to display an image on a Square3D, by using an ImageBrush or a VisualBrush in the Material definition.
<p:Square3D>
<p:Square3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="C:\Users\Public\Pictures\Sample Pictures\Tree.jpg" TileMode="None" Stretch="Fill"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</p:Square3D.Material>
</p:Square3D>
The BackMaterial property has the same role than the Material property but for the back face. If it is empty, the back face uses the default material.
Polygon-based models
Polygon3D defines a flat polygon. The SideCount property defines a regular polygon of a given side count.
<p:Polygon3D SideCount="6" />

The polygon fits in a circle of radius 1 in the X, Y plan. The polygon vertices are calculated by dividing the circumference by the SideCount value. By default, the 1st point is at 1,0,0. The InitialAngle property makes it possible to move this point according to the angle of the segment that it forms with the origin compared to the X axis. Example for an angle of 30 degrees :
<p:Polygon3D SideCount="6" InitialAngle="30.0"/>

The RoundingRate makes it possible to round the polygon vertices. The value must be comprised between 0.0 and 0.5. It represents a side proportion submitted to the rounding for the 2 sides of a vertex. Thus a value of 0.5 generates a perfect circle. Example with a value of 0.15 :
<p:Polygon3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"/>

The bar3D class défines a bar with a polygonal section. The section has the same characteristics than a Polygon3D : side count, initial angle and rounding rate. For a better emphasizing of the rounding, we use a glossy material (using SpecularMaterial) and move the model compared to the light position.
...
<Window.Resources>
...
<MaterialGroup x:Key="glossyMaterial">
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="Gold" Opacity="1.0"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<SpecularMaterial SpecularPower="100.0">
<SpecularMaterial.Brush>
<SolidColorBrush Color="White" Opacity="1.0" />
</SpecularMaterial.Brush>
</SpecularMaterial>
</MaterialGroup>
</Window.Resources>
...
<p:Bar3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Transform="{StaticResource modelTransform}"
Material="{StaticResource glossyMaterial}" />
To build a cylinder with a radius of 1.0, you can grow the SideCount property :
<p:Bar3D SideCount="100" Material="{StaticResource glossyMaterial}" />
Conical3D is a cone with a polygonal base, and works as Bar3D .
<p:Conical3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Transform="{StaticResource modelTransform}"
Material="{StaticResource glossyMaterial}" />
Finally Ring3D is a ring with a polygonal section. It has additional properties to indicate its radius and its segment count.
<p:Ring3D
Radius="10.0" SegmentCount="100"
SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Material="{StaticResource glossyMaterial}" />
Spherical models
The Spherical3D class defines a sphere with a radius of 1 unit. The smoothness of the spherical aspect is defined using the ParallelCount property.
<p:Spherical3D ParallelCount="100" Material="{StaticResource glossyMaterial}"/>
It is easy to transform the sphere into an ellipsoid using a tranform object.
<p:Spherical3D ParallelCount="100"
Material="{StaticResource glossyMaterial}"
Transform="{StaticResource modelTransform}"/>
Polyhedrons
Just for fun, the Isocahedron3D class defines a regular isocahedron , that is a 20 faces polyhedron.
<p:Isocahedron3D Material="{StaticResource glossyMaterial}" /> 
If we truncate its vertices, we get a truncated isocahedron... represented by the TruncatedIsocahedron3D class.
<p:Isocahedron3D Truncated="True" Material="{StaticResource glossyMaterial}" />
Anyway, you see what I mean... A very poor translation from my french article ! The Football3D class genesis began at a certain evening of July 2006. I am however not impassioned by football (soccer). But after often seeing 3D animated credits at the TV, I wanted to prove that WPF can do that. Here we define a background with a new color and without a displayed coordinate system, and we can animate the whole with a 3D WPF animation ! Play video
<p:Football3D />

Composite models
The Wpf3D objects can be gathered in a ModelVisual3D or in a ContainerUIElement3D to form a composite model, such as this animated gyroscope :

The source code of the gyroscope is provided with the Wpf3D library in the form of a custom control Gyroscope3D. It consists of a ContainerUIElement3D for the 2 external rings (Ring3d), overlapped in the basic ContainerUIElement3D including the central ring, the axis and the wheel. Thus, it is possible to imbricate animations (the one of a ring and the one of a group of rings), resulting in a complex global animation. Play video
The Wpf3D library is extensible, and we will see later that it is easy to define in .NET code (C# or Visual Basic) some new composite model classes. After XyzAxis3D and Football3D, the Arrow3D class is another example :
<p:Arrow3D Material="{StaticResource glossyMaterial}" />
3D Controls
In its 0.4 version, Perspective introduces the concept of 3D skinnable control .

About this article
Author : Olivier Dewit
History :
- the 16th november, 2007 : update for the 0.4 version of Perspective.
- the 11th november, 2007 : chapter about interactivity.
- the 3rd november, 2007 : update for the 0.3 version of Perspective.
- October, 2006 : creation, for the 0.1 version of Perspective (Wpf3D).