Introduction
A few days ago, I thought it would be funny to play around with WPF's new 3D capabilities. By then, I had only looked at 2D graphics and animations with XAML. Of course, my starting point was MSDN. You can find quite a good introduction in 3D graphics with XAML at MSDN. There is also another article on CodeProject: 3D in XAML, that gives you a good start for 3D in XAML. I will not go into the basics of cameras, meshes, lights, etc. in my article.
I was surprised when I read in MSDN that WPF "does not currently support predefined 3-D primitives like spheres and cubic forms". It gives you the MeshGeometry3D
class which allows to build any geometry as a list of triangles. Therefore, I decided that my first 3D mini-project in WPF will be an algorithm that generates a mesh that represents a sphere.
Unfortunately, I am no specialist in writing 3D graphics code. Therefore, I decided to implement quite a simple algorithm that generates a sphere from a mesh of triangles: I do quite what the open-source 3D modeler Blender does with its UVSphere mesh:
(Source: Wiki: Grundkörper)
As you can see from the picture above, I split the sphere into segments and rings. The result is a list of squares (that can easily be split into two triangles), and triangles at the top and the bottom. Blender's Icosphere (see Wiki: Ikosaeder (German) for more details) would have been even more suitable for XAML meshes. However, I decided to start with UVSphere.
A sphere is not the only round mesh that can be generated by splitting a circle into segments. Therefore, I decided to write an abstract base class that can also be used for, e.g., a disc (a circle in 3D space):
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Sphere3D
{
abstract class RoundMesh3D
{
protected int n = 10;
protected int r = 20;
protected Point3DCollection points;
protected Int32Collection triangleIndices;
public virtual int Radius
{
get { return r; }
set { r = value; CalculateGeometry(); }
}
public virtual int Separators
{
get { return n; }
set { n = value; CalculateGeometry(); }
}
public Point3DCollection Points
{
get { return points; }
}
public Int32Collection TriangleIndices
{
get { return triangleIndices; }
}
protected abstract void CalculateGeometry();
}
}
r
stands for the radius of the mesh, and n
stands for the number of segments into which I split the circle (4*n+4 is the number of points that I equally distribute on the circle).
My first test was the implementation of a disc. Here is the code. It is not very complex, just some trigonometric functions:
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Diagnostics;
namespace Sphere3D
{
class DiscGeometry3D : RoundMesh3D
{
protected override void CalculateGeometry()
{
int numberOfSeparators = 4 * n + 4;
points = new Point3DCollection(numberOfSeparators + 1);
triangleIndices = new Int32Collection((numberOfSeparators + 1) * 3);
points.Add(new Point3D(0, 0, 0));
for (int divider = 0; divider < numberOfSeparators; divider++)
{
double alpha = Math.PI / 2 / (n + 1) * divider;
points.Add(new Point3D(r * Math.Cos(alpha),
0, -1 * r * Math.Sin(alpha)));
triangleIndices.Add(0);
triangleIndices.Add(divider + 1);
triangleIndices.Add((divider ==
(numberOfSeparators-1)) ? 1 : (divider + 2));
}
}
public DiscGeometry3D()
{ }
}
}
The code for generating the sphere is a little bit longer. Distributing the points on the sphere is the simple part. I found it harder to generate the triangles correctly:
using System;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Diagnostics;
namespace Sphere3D
{
class SphereGeometry3D : RoundMesh3D
{
protected override void CalculateGeometry()
{
int e;
double segmentRad = Math.PI / 2 / (n + 1);
int numberOfSeparators = 4 * n + 4;
points = new Point3DCollection();
triangleIndices = new Int32Collection();
for (e = -n; e <= n; e++)
{
double r_e = r * Math.Cos(segmentRad * e);
double y_e = r * Math.Sin(segmentRad * e);
for (int s = 0; s <= (numberOfSeparators - 1); s++)
{
double z_s = r_e * Math.Sin(segmentRad * s) * (-1);
double x_s = r_e * Math.Cos(segmentRad * s);
points.Add(new Point3D(x_s, y_e, z_s));
}
}
points.Add(new Point3D(0, r, 0));
points.Add(new Point3D(0, -1 * r, 0));
for (e = 0; e < 2 * n; e++)
{
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(e * numberOfSeparators + i);
triangleIndices.Add(e * numberOfSeparators + i +
numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators + numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators + numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators +
(i + 1) % numberOfSeparators);
triangleIndices.Add(e * numberOfSeparators + i);
}
}
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(e * numberOfSeparators + i);
triangleIndices.Add(e * numberOfSeparators + (i + 1) %
numberOfSeparators);
triangleIndices.Add(numberOfSeparators * (2 * n + 1));
}
for (int i = 0; i < numberOfSeparators; i++)
{
triangleIndices.Add(i);
triangleIndices.Add((i + 1) % numberOfSeparators);
triangleIndices.Add(numberOfSeparators * (2 * n + 1) + 1);
}
}
public SphereGeometry3D()
{ }
}
}
For my sample, I wanted to display two spheres and a nice picture in the background (see the image at the top of the article). Therefore, I decided to create two descendent classes from SphereGeometry3D
:
namespace Sphere3D
{
class BigPlanet : SphereGeometry3D
{
BigPlanet()
{
Radius = 30;
Separators = 5;
}
}
class SmallPlanet : SphereGeometry3D
{
SmallPlanet()
{
Radius = 5;
Separators = 5;
}
}
}
Finally, I used XAML's data binding mechanisms to bind the properties Positions
and TriangleIndices
of MeshGeometry3D
to the algorithms shown above:
<Window x:Class="Sphere3D.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sphere3D"
Title="Labyrinth3d" Height="600" Width="600"
>
<Window.Background>
<ImageBrush Stretch="UniformToFill"
ImageSource="Images/Pleiades.jpg"/>
</Window.Background>
<Grid VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" x:Name="Grid1">
<Grid.Resources>
<local:BigPlanet x:Key="SphereGeometrySource1"/>
<local:SmallPlanet x:Key="SphereGeometrySource2"/>
<MeshGeometry3D x:Key="SphereGeometry1"
Positions="{Binding Source={StaticResource
SphereGeometrySource1}, Path=Points}"
TriangleIndices="{Binding Source={StaticResource
SphereGeometrySource1},
Path=TriangleIndices}"/>
<MeshGeometry3D x:Key="SphereGeometry2"
Positions="{Binding Source={StaticResource
SphereGeometrySource2}, Path=Points}"
TriangleIndices="{Binding Source={StaticResource
SphereGeometrySource2},
Path=TriangleIndices}"/>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Viewport3D Grid.Column="1" Grid.Row="1"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Name="Viewport1">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="myCamera" Position="100 30 0"
LookDirection="-50 -33 0"
UpDirection="0,1,0" FieldOfView="90"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="#FFFFFF"
Direction="0 -30 0" />
<DirectionalLight Color="#FFFFFF"
Direction="0 +30 0" />
<GeometryModel3D
Geometry="{StaticResource SphereGeometry1}">
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="Orange"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource SphereGeometry2}">
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<SolidColorBrush Color="Yellow"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Transform>
<TranslateTransform3D
x:Name="Sphere2Translation" OffsetZ="50" />
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
If my implementation of the 3D sphere for XAML is helpful for you, I would be happy if you could vote for my article here at CodeProject. If you have questions, feel free to send me an email.