65.9K
CodeProject is changing. Read more.
Home

Generating a sphere-mesh in XAML

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (35 votes)

Jun 11, 2006

2 min read

viewsIcon

151722

downloadIcon

5159

This article shows a way how to create a 3D sphere in C# and XAML

Sample Image - XamlUVSphere.jpg

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:

Blender 3D UVSphere

(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"/>
                <!--<OrthographicCamera x:Name="myCamera" 
                      Position="200 0 0" LookDirection="-1 0 0" 
                      Width="180" UpDirection="0,1,0"/>-->
            </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.