Introduction
Preface: Notes about Surfaces
To run these applications, download the zip files and extract them into a newly-made folder in your Projects directory After that, either double-click the solution file or right-click them if you want to load them into VS 2010 or Expression Blend 4.0.
Mathematically, a surface draws a Z function on a surface for each X and Y coordinate in a region of interest. For each X and Y value, a simple surface can have at most one Z value. More to the point, in mathematics, specifically in topology, a surface is a two-dimensional topological manifold. To say that a surface is "two-dimensional" means that, about each point, there is a coordinate patch on which a two-dimensional coordinate system is defined. For example, the surface of the Earth is (ideally) a two-dimensional sphere, and latitude and longitude provide coordinates on it. Our goal, then, is to draw a surface. Because we are drawing 2 and 3 dimensional geometric shapes, we need to understand what a mesh is. A mesh is basically a representation of a surface. The mesh represents the surface through a system of points and lines. The points describe the high and low areas of the surface, and the lines connect the points to establish how you get from one point to the next.
At a minimum, a surface is a flat plane. A flat plane needs three points to define it. Thus, the simplest surface that can be described in a mesh is a single triangle. It turns out that meshes can only be described with triangles. That is because a triangle is the simplest, most granular way to define a surface. A mesh represents a surface through many triangles. A whole mesh is composed of mesh positions, triangular indices, and triangle normals. In WPF, the order in which you add mesh positions is important. A position's index value in a mesh's position collection is used when adding triangle indices. For example, let's say you have a surface composed of five positions {p0, p1, p2, p3, p4}
. If you wanted to define a triangle from p1
, p3
, and p4
, you would add triangle indices with index values 1, 3, and 4. If the positions were added in a different order {p3, p4, p0, p2, p1}
and you wanted a triangle made of the same positions, you would add triangle indices with index values 4, 0, and 1.
So let’s use WPF, in order to use these concepts to draw a surface. We will recall that a surface is composed of five positions {p0, p1, p2, p3, p4}
. If you wanted to define a triangle from p1
, p3
, and p4
, you would add triangle indices with index values 1, 3, and 4. If the positions were added in a different order {p3, p4, p0, p2, p1}
and you wanted a triangle made of the same positions, you would add triangle indices with index values 4, 0, and 1. Now examine this code. The 3DTools.dll is a downloadable component available at www.codeplex.com. This assembly will help us build the Utility
class, which will be one of the referenced DLLs used in our application:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Controls;
using _3DTools;
public class Utility
{
public static void CreateRectangleFace(
Point3D p0, Point3D p1, Point3D p2, Point3D p3,
Color surfaceColor, Viewport3D viewport)
{
MeshGeometry3D mesh = new MeshGeometry3D();
mesh.Positions.Add(p0);
mesh.Positions.Add(p1);
mesh.Positions.Add(p2);
mesh.Positions.Add(p3);
mesh.TriangleIndices.Add(0);
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(3);
mesh.TriangleIndices.Add(0);
SolidColorBrush brush = new SolidColorBrush();
brush.Color = surfaceColor;
Material material = new DiffuseMaterial(brush);
GeometryModel3D geometry =
new GeometryModel3D(mesh, material);
ModelVisual3D model = new ModelVisual3D();
model.Content = geometry;
viewport.Children.Add(model);
}
public static void CreateWireframe(
Point3D p0, Point3D p1, Point3D p2, Point3D p3,
Color lineColor, Viewport3D viewport)
{
ScreenSpaceLines3D ssl = new ScreenSpaceLines3D();
ssl.Points.Add(p0);
ssl.Points.Add(p1);
ssl.Points.Add(p1);
ssl.Points.Add(p2);
ssl.Points.Add(p2);
ssl.Points.Add(p3);
ssl.Points.Add(p3);
ssl.Points.Add(p0);
ssl.Color = lineColor;
ssl.Thickness = 2;
viewport.Children.Add(ssl);
}
public static Point3D GetNormalize(Point3D pt,
double xmin, double xmax,
double ymin, double ymax,
double zmin, double zmax)
{
pt.X = -1 + 2 * (pt.X - xmin) / (xmax - xmin);
pt.Y = -1 + 2 * (pt.Y - ymin) / (ymax - ymin);
pt.Z = -1 + 2 * (pt.Z - zmin) / (zmax - zmin);
return pt;
}
}
Having now built Utility.dll, we will build another class file to compile into a DLL by referencing the previously made DLL. Here is SimpleSurface.cs:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Controls;
public class SimpleSurface
{
public delegate Point3D Function(double x, double z);
private double xmin = -3;
private double xmax = 3;
private double ymin = -8;
private double ymax = 8;
private double zmin = -3;
private double zmax = 3;
private int nx = 30;
private int nz = 30;
private Color lineColor = Colors.Black;
private Color surfaceColor = Colors.White;
private Point3D center = new Point3D();
private bool isHiddenLine = false;
private bool isWireframe = true;
private Viewport3D viewport3d = new Viewport3D();
public bool IsWireframe
{
get { return isWireframe; }
set { isWireframe = value; }
}
public bool IsHiddenLine
{
get { return isHiddenLine; }
set { isHiddenLine = value; }
}
public Color LineColor
{
get { return lineColor; }
set { lineColor = value; }
}
public Color SurfaceColor
{
get { return surfaceColor; }
set { surfaceColor = value; }
}
public double Xmin
{
get { return xmin; }
set { xmin = value; }
}
public double Xmax
{
get { return xmax; }
set { xmax = value; }
}
public double Ymin
{
get { return ymin; }
set { ymin = value; }
}
public double Ymax
{
get { return ymax; }
set { ymax = value; }
}
public double Zmin
{
get { return zmin; }
set { zmin = value; }
}
public double Zmax
{
get { return zmax; }
set { zmax = value; }
}
public int Nx
{
get { return nx; }
set { nx = value; }
}
public int Nz
{
get { return nz; }
set { nz = value; }
}
public Point3D Center
{
get { return center; }
set { center = value; }
}
public Viewport3D Viewport3d
{
get { return viewport3d; }
set { viewport3d = value; }
}
public void CreateSurface(Function f)
{
double dx = (Xmax - Xmin) / Nx;
double dz = (Zmax - Zmin) / Nz;
if (Nx < 2 || Nz < 2)
return;
Point3D[,] pts = new Point3D[Nx, Nz];
for (int i = 0; i < Nx; i++)
{
double x = Xmin + i * dx;
for (int j = 0; j < Nz; j++)
{
double z = Zmin + j * dz;
pts[i, j] = f(x, z);
pts[i, j] += (Vector3D)Center;
pts[i, j] = Utility.GetNormalize(
pts[i, j], Xmin, Xmax,
Ymin, Ymax, Zmin, Zmax);
}
}
Point3D[] p = new Point3D[4];
for (int i = 0; i < Nx - 1; i++)
{
for (int j = 0; j < Nz - 1; j++)
{
p[0] = pts[i, j];
p[1] = pts[i, j + 1];
p[2] = pts[i + 1, j + 1];
p[3] = pts[i + 1, j];
if (IsHiddenLine == false)
Utility.CreateRectangleFace(
p[0], p[1], p[2], p[3],
SurfaceColor, Viewport3d);
if (IsWireframe == true)
Utility.CreateWireframe(
p[0], p[1], p[2], p[3],
LineColor, Viewport3d);
}
}
}
}
Notice the code a few lines above: Utility.CreateWireFrame()
. Utility
is the class and CreateWireframe()
is the method defined in this class. Recall:
public static void CreateWireframe(
Point3D p0, Point3D p1, Point3D p2, Point3D p3,
Color lineColor, Viewport3D viewport)
A Simple Surface
Now we have a Utility.dll and a SimpleSurface.dll. These can be built as class libraries. If we were to build them on the command line, the simplest way would be to go to c:\program files\referenced assemblies\Microsoft\Framework\.NETFramework\v4.0\. This directory contains the major WPF assemblies: PresentationCore.dll, PresentationFramework.dll, System.Xaml.dll, and WindowsBase.dll. We would have to set the .NET environment path:
set PATH=%PATH%;.;C:\windows\Microsoft.NET\Framework\v4.0.30319
Whether we use the command line or Visual Studio 2010, we use this series of DLLs when we use Expression Blend or Visual Studio 2010 to build a WPF project. We name the project Surf
. We add a new item (a new WPF window), and name it SimpleSurfaceTest
. This creates the corresponding SimpleSurfaceTest.xaml file and the SimpleSurfaceTest.xaml.cs code-behind file. Oh, and we right-click the MainWindow.xaml(cs) file to then click “remove from project”. When you copy and paste the XAML file, you will notice that apart the gradient, there is no surface. The surface is drawn via the code-behind file. Below are the two SimpleSurfaceTest files:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Surf
{
public partial class SimpleSurfaceTest : Window
{
private SimpleSurface ss = new SimpleSurface();
public SimpleSurfaceTest()
{
InitializeComponent();
ss.IsHiddenLine = false;
ss.Viewport3d = viewport;
AddSinc();
}
private void AddSinc()
{
ss.Xmin = -8;
ss.Xmax = 8;
ss.Zmin = -8;
ss.Zmax = 8;
ss.Ymin = -1;
ss.Ymax = 1;
ss.CreateSurface(Sinc);
}
private Point3D Sinc(double x, double z)
{
double r = Math.Sqrt(x * x + z * z) + 0.00001;
double y = Math.Sin(r) / r;
return new Point3D(x, y, z);
}
private void AddPeaks()
{
ss.Xmin = -3;
ss.Xmax = 3;
ss.Zmin = -3;
ss.Zmax = 3;
ss.Ymin = -8;
ss.Ymax = 8;
ss.CreateSurface(Peaks);
}
private Point3D Peaks(double x, double z)
{
double y = 3 * Math.Pow((1 - x), 2) *
Math.Exp(-x * x - (z + 1) * (z + 1)) -
10 * (0.2 * x - Math.Pow(x, 3) - Math.Pow(z, 5)) * Math.Exp(-x * x - z * z) -
1 / 3 * Math.Exp(-(x + 1) * (x + 1) - z * z);
return new Point3D(x, y, z);
}
private void AddRandomSurface()
{
ss.Xmin = -8;
ss.Xmax = 8;
ss.Zmin = -8;
ss.Zmax = 8;
ss.Ymin = -1;
ss.Ymax = 1;
ss.CreateSurface(RandomSurface);
}
private Random rand = new Random();
private Point3D RandomSurface(double x, double z)
{
double r = Math.Sqrt(x * x + z * z) + 0.00001;
double y = Math.Sin(r) / r +
0.2 * rand.NextDouble();
return new Point3D(x, y, z);
}
}
}
Here is the output:
Parametric Surfaces
The surface shown above is a simple surface. A key feature of this type of surface is that there is at most one Y value for each pair of X and Z values. However, sometimes you may want to create a complex surface of a certain shape. This kind of complex surface can’t be represented by a simple function. For certain values of X and Z, this surface has more than one Y value. One way to represent this type of surface is to use a set of the parametric equations. These equations define the X, Y, and Z coordinates of points on the surface in terms of the parametric variables u
and v
. So the next step is building a ParametricSurface
class to compile it into a DLL that will be referenced when we execute code to test that surface.
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Controls;
public class ParametricSurface
{
public delegate Point3D Function(double u, double v);
private int nu = 30;
private int nv = 30;
private double umin = -3;
private double umax = 3;
private double vmin = -8;
private double vmax = 8;
private double xmin = -1;
private double xmax = 1;
private double ymin = -1;
private double ymax = 1;
private double zmin = -1;
private double zmax = 1;
private Color lineColor = Colors.Black;
private Color surfaceColor = Colors.White;
private Point3D center = new Point3D();
private bool isHiddenLine = false;
private bool isWireframe = true;
private Viewport3D viewport3d = new Viewport3D();
public bool IsWireframe
{
get { return isWireframe; }
set { isWireframe = value; }
}
public bool IsHiddenLine
{
get { return isHiddenLine; }
set { isHiddenLine = value; }
}
public Color LineColor
{
get { return lineColor; }
set { lineColor = value; }
}
public Color SurfaceColor
{
get { return surfaceColor; }
set { surfaceColor = value; }
}
public double Umin
{
get { return umin; }
set { umin = value; }
}
public double Umax
{
get { return umax; }
set { umax = value; }
}
public double Vmin
{
get { return vmin; }
set { vmin = value; }
}
public double Vmax
{
get { return vmax; }
set { vmax = value; }
}
public int Nu
{
get { return nu; }
set { nu = value; }
}
public int Nv
{
get { return nv; }
set { nv = value; }
}
public double Xmin
{
get { return xmin; }
set { xmin = value; }
}
public double Xmax
{
get { return xmax; }
set { xmax = value; }
}
public double Ymin
{
get { return ymin; }
set { ymin = value; }
}
public double Ymax
{
get { return ymax; }
set { ymax = value; }
}
public double Zmin
{
get { return zmin; }
set { zmin = value; }
}
public double Zmax
{
get { return zmax; }
set { zmax = value; }
}
public Point3D Center
{
get { return center; }
set { center = value; }
}
public Viewport3D Viewport3d
{
get { return viewport3d; }
set { viewport3d = value; }
}
public void CreateSurface(Function f)
{
double du = (Umax - Umin) / (Nu - 1);
double dv = (Vmax - Vmin) / (Nv - 1);
if (Nu < 2 || Nv < 2)
return;
Point3D[,] pts = new Point3D[Nu, Nv];
for (int i = 0; i < Nu; i++)
{
double u = Umin + i * du;
for (int j = 0; j < Nv; j++)
{
double v = Vmin + j * dv;
pts[i, j] = f(u, v);
pts[i, j] += (Vector3D)Center;
pts[i, j] = Utility.GetNormalize(
pts[i, j], Xmin, Xmax,
Ymin, Ymax, Zmin, Zmax);
}
}
Point3D[] p = new Point3D[4];
for (int i = 0; i < Nu - 1; i++)
{
for (int j = 0; j < Nv - 1; j++)
{
p[0] = pts[i, j];
p[1] = pts[i, j + 1];
p[2] = pts[i + 1, j + 1];
p[3] = pts[i + 1, j];
if (IsHiddenLine == false)
Utility.CreateRectangleFace(
p[0], p[1], p[2], p[3],
SurfaceColor, Viewport3d);
if (IsWireframe == true)
Utility.CreateWireframe(
p[0], p[1], p[2], p[3],
LineColor, Viewport3d);
}
}
}
}
We build this class file into a DLL. Recall that we must still reference the utility.dll (which was built by referencing the downloaded 3DTools.dll) and the SimpleSurface.dll. Use Expression Blend 4.0, build a new WPF project and call it Dave. Add a new Window and name it ParametricSurfaceTest
. Remove the MainWindow files from the project. Below is both the XAML and the code-behind:
The ParametricSurfaceTest
code-behind file:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Dave
{
public partial class ParametricSurfaceTest : Window
{
private ParametricSurface ps =
new ParametricSurface();
public ParametricSurfaceTest()
{
InitializeComponent();
ps.IsHiddenLine = false;
ps.Viewport3d = viewport;
AddHelicoid();
}
private void AddHelicoid()
{
ps.Umin = 0;
ps.Umax = 1;
ps.Vmin = -3 * Math.PI;
ps.Vmax = 3 * Math.PI;
ps.Nv = 100;
ps.Nu = 10;
ps.Ymin = ps.Vmin;
ps.Ymax = ps.Vmax;
ps.CreateSurface(Helicoid);
}
private Point3D Helicoid(double u, double v)
{
double x = u * Math.Cos(v);
double z = u * Math.Sin(v);
double y = v;
return new Point3D(x, y, z);
}
}
}
Outputted parametric surface:
Let’s use the ParametricSurface
class and replace the Helicoid
and AddHelicoid
methods with a method called AppSphere()
:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Dave
{
public partial class ParametricSurfaceTest : Window
{
private ParametricSurface ps =
new ParametricSurface();
public ParametricSurfaceTest()
{
InitializeComponent();
ps.IsHiddenLine = false;
ps.Viewport3d = viewport;
AddSphere();
}
private void AddSphere()
{
ps.Umin = 0;
ps.Umax = 2 * Math.PI;
ps.Vmin = -0.5 * Math.PI;
ps.Vmax = 0.5 * Math.PI;
ps.Nu = 20;
ps.Nv = 20;
ps.CreateSurface(Sphere);
}
private Point3D Sphere(double u, double v)
{
double x = Math.Cos(v) * Math.Cos(u);
double z = Math.Cos(v) * Math.Sin(u);
double y = Math.Sin(v);
return new Point3D(x, y, z);
}
}
}
Here is the sphere, a type of parametric surface:
We can repeat the process by replacing the AddSphere
method with the AddTorus
method:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace Dave
{
public partial class ParametricSurfaceTest : Window
{
private ParametricSurface ps =
new ParametricSurface();
public ParametricSurfaceTest()
{
InitializeComponent();
ps.IsHiddenLine = false;
ps.Viewport3d = viewport;
AddTorus();
}
private void AddTorus()
{
ps.Umin = 0;
ps.Umax = 2 * Math.PI;
ps.Vmin = 0;
ps.Vmax = 2 * Math.PI;
ps.Nu = 50;
ps.Nv = 20;
ps.CreateSurface(Torus);
}
private Point3D Torus(double u, double v)
{
double x = (1 + 0.3 * Math.Cos(v)) * Math.Cos(u);
double z = (1 + 0.3 * Math.Cos(v)) * Math.Sin(u);
double y = 0.3 * Math.Sin(v);
return new Point3D(x, y, z);
}
}
}
Admittedly, the topic of surfaces sounds trivial. It does, however, have a strong position in scientific and topological studies. For instance, the earth is considered a surface that has latitude and longitude as its coordinate system.