Using Avalon, I was finally successful at rendering a 3D model of the state of Minnesota from geographic latitude and longitude points:

I strugged with this for about a week. The real crux of the biscuit turned out to be how I was ordering the triangle vertices in the 3D mesh.

Ordering Triangle Vertices

I don't pretend to be an expert in rendering 3D geometries, but here's an explanation of what I learned. 3D meshes are made up of individual points (or vertices), and then many triangles from those points. For example, using the mesh viewer in the DirectX 9 SDK, you can see that a cube is made up of 8 points and 12 triangles (two triangles on each face):

When you define the triangles in the mesh, you just indicate which vertices make up the triangles. For example, for a cube face made up of the vertices {0, 1, 2, 3} you could define two triangles as {0, 1, 2} and {1, 2, 3). But as I already mentioned, the ordering of the vertices in each triangle is important. A triangle defined as {0, 1, 2} will appear different than a triangle defined as {2, 1, 0}.

When you create your triangle, a vector normal to the plane of the triangle will determine the side it is visible from. For example, if you view the triangle from the top it may be visible, but may not be visible from the bottom. That normal vector is determined from the ordering of the triangle vertices.

Your triangle vertices are essentially defining vectors of the triangle. From my observation, the normal vector is a cross product of the vectors defined by the vertices. For those of you familiar with cross products and the "right hand rule", the normal vector will be normal to the triangle depending on whether the triangle vertices are ordered "clockwise" or "counter-clockwise". The direction that the resultant normal vector goes (“up“ or “down“) affects the visibility of the triangle.  You don't need to calculate these normals manually in code, as Avalon takes care of it for you.

Normals

There are other types of "normals" in meshes as well. Each point in the mesh is assigned a "normal" vector that affects lighting. I'm bringing this up because I'm going to show some code, and I don't want anyone to get confused between an explicitly assigned normal for a point and the automatic normal created from triangle vertices. ... that kind of sounds like the same thing, but stay with me!

Mesh Strategy

Since boundaries of state, cities, and zip codes are more complex than a simple rectangle, I needed to come up with a way of creating a mesh.  State, city, and zip code boundaries are essentially just complex polygons with dozens of sides. I decided to use a "spokes" strategy where a center point was placed inside the boundary and each boundary point was connected to the center point:

Each "spoke" defines a side of a triangle in the mesh. One triangle is defined as two adjancent spokes and the boundary line between them. The example above has seven triangles.

Domain Objects

I created some domain classes that let me get the geography data from a SQL Server database. Here's a simple class diagram:

What's important is that I can get a Boundary (state, city or zip code) from the database and access all of that Boundary's points through its Points property.

The XAML

Nothing fancy, just a simple 3D viewport:

   <VIEWPORT3D ClipToBounds="True" Name="mainViewport">
    <VIEWPORT3D.CAMERA>
     <PERSPECTIVECAMERA FieldOfView="100" Position="7,10,5" NearPlaneDistance="1" Up="0,1,0" LookAtPoint="0,0,0" FarPlaneDistance="500" />
    </VIEWPORT3D.CAMERA>
    <VIEWPORT3D.MODELS>
     <MODEL3DGROUP>
      <MODEL3DGROUP.CHILDREN>
       <DIRECTIONALLIGHT Direction="-3,-1,-5" Color="#FFFFFFFF" />
      </MODEL3DGROUP.CHILDREN>
     </MODEL3DGROUP>
    </VIEWPORT3D.MODELS>
   </VIEWPORT3D>

Adding the Geometry to the Viewport3D

Using a custom class named BoundaryMesh, I can generate the 3D mesh from a Boundary, feed it into a model, and add the model to the Viewport3D:

BoundaryCollection boundaries = new BoundaryCollection(BoundaryTypes.State);
Boundary stateBoundary = boundaries[0];

//move the state over nearby the 0,0,0 origin.
stateBoundary.Points.TranslatePointsToZero();

//create a boundary mesh
BoundaryMesh m = new BoundaryMesh(stateBoundary);

//get the mesh, assign it a Blue material, and add it to the boundary model
GeometryModel3D boundaryModel = new GeometryModel3D(m.Mesh, this.materialBlue);

//add the boundary model to the viewport
mainViewport.Models.Children.Add(boundaryModel);

The BoundaryMesh Class

The BoundaryMesh class is what does all of the work. When its Mesh property is accessed the mesh is created using a method named CreateMesh(). Here's the code of CreateMesh():

private MeshGeometry3D CreateMesh()
{
    MeshGeometry3D mesh = new MeshGeometry3D();
    double posY = 1;
    double negY = 0;
    int distinctPositionIndeces = 0;
    int totalIndeces = 0;

    //Hack: The first point in the collection is always the center point.  
    //Add the center point for the top plane of the boundary.
    mesh.Positions.Add(new Point3D(boundary.Points[0].X, posY, boundary.Points[0].Y));
    mesh.Normals.Add(new Vector3D(0, 0, 1));

    //loop through boundary points and add them to the top plane.
    for (int i = 1; i < boundary.Points.Count; i++)
    {
        mesh.Positions.Add(new Point3D(boundary.Points[i].X, posY, boundary.Points[i].Y));

        //Hack: add a some normals for lighting
        mesh.Normals.Add(new Vector3D(0, 0, 1));
        mesh.Normals.Add(new Vector3D(1, 0, 0));

        //increment counters
        distinctPositionIndeces += 1;
        totalIndeces += 1;
    }

    //bottom surface center point
    mesh.Positions.Add(new Point3D(boundary.Points[0].X, negY, boundary.Points[0].Y));
    mesh.Normals.Add(new Vector3D(0, 0, 1));
    totalIndeces += 1;

    //loop through boundary points for bottom side
    for (int i = 1; i < boundary.Points.Count; i++)
    {
        mesh.Positions.Add(new Point3D(boundary.Points[i].X, negY, boundary.Points[i].Y));
        mesh.Normals.Add(new Vector3D(0, 0, -1));
        totalIndeces += 1;
    }

    //Create mesh triangles for top surface.  Ordering is critical!
    //Go "counterclockwise".
    for (int i = 1; i < distinctPositionIndeces - 1; i++)
    {
        mesh.TriangleIndices.Add(0);
        mesh.TriangleIndices.Add(i + 1);
        mesh.TriangleIndices.Add(i);
    }

    //add the last top triangle
    mesh.TriangleIndices.Add(0);
    mesh.TriangleIndices.Add(1);
    mesh.TriangleIndices.Add(distinctPositionIndeces);

    //Create mesh triangles for bottom surface.  Ordering is critical!
    //For the bottom side, go clockwise (or counter-clockwise if you
    //are looking at the boundary from the bottom side).
    for (int i = distinctPositionIndeces + 2; i < totalIndeces - 1; i++)
    {
        mesh.TriangleIndices.Add(distinctPositionIndeces+1);
        mesh.TriangleIndices.Add(i);
        mesh.TriangleIndices.Add(i + 1);
    }

    //add the last bottom triangle
    mesh.TriangleIndices.Add(distinctPositionIndeces + 1);
    mesh.TriangleIndices.Add(totalIndeces);
    mesh.TriangleIndices.Add(distinctPositionIndeces + 2);

    //Add side triangles.  Indeces are added
    //"counterclockwise" if you were looking at the
    //boundary from the side.
    for (int i = 1; i <= distinctPositionIndeces - 1; i++)
    {
        mesh.TriangleIndices.Add(i);
        mesh.TriangleIndices.Add(i + distinctPositionIndeces + 3);
        mesh.TriangleIndices.Add(i + distinctPositionIndeces + 2);
        mesh.TriangleIndices.Add(i);
        mesh.TriangleIndices.Add(i + 1);
        mesh.TriangleIndices.Add(i + distinctPositionIndeces + 3);
    }
    
    return mesh;
}

More Screenshots

From the negative X/Y space:

Close-up near the origin (northwest corner of the state). Here you can get a feel for the thickness of the boundary. You can also make out the individual triangles in the mesh on the sides:

Another close-up near the southeast corner of the state:

What's Next?

  • Fix normal vector hack used to help define edges.
  • Produce smoother lighting effects.
  • Add cities and zip codes to the state.

Resources