Make a 3D globe in WPF and C#

[3D globe]

This example shows how you can draw a 3D globe. More generally it shows how you can make a three-dimensional textured sphere in WPF.

The example Draw smooth spheres using WPF and C# shows how to make smooth colored spheres. This example modifies that technique to make a textured sphere.

The following code shows how the example program builds the 3D globe.

// Add the model to the Model3DGroup.
private void DefineModel(Model3DGroup model_group)
{
    // Globe. Place it in a new model so we can transform it.
    Model3DGroup globe_model = new Model3DGroup();
    model_group.Children.Add(globe_model);

    ImageBrush globe_brush = new ImageBrush(new BitmapImage(
        new Uri("pack://application:,,,/world.jpg")));
    Material globe_material = new DiffuseMaterial(globe_brush);
    MeshGeometry3D globe_mesh = null;
    MakeSphere(globe_model, ref globe_mesh, globe_material,
        1, 0, 0, 0, 20, 30);
}

This code creates a Model3DGroup to hold the 3D globe and adds it to the program’s model group.

Next the code creates a brush that uses the image file world.jpg. It uses the brush to make a material for the 3D globe and then calls the MakeSphere method to create the sphere.

The code shows the MakeSphere method.

// Make a sphere.
private void MakeSphere(Model3DGroup model_group,
    ref MeshGeometry3D sphere_mesh, Material sphere_material,
    double radius, double cx, double cy, double cz, int num_phi,
    int num_theta)
{
    // Make the mesh if we must.
    if (sphere_mesh == null)
    {
        sphere_mesh = new MeshGeometry3D();
        GeometryModel3D new_model =
            new GeometryModel3D(sphere_mesh, sphere_material);
        model_group.Children.Add(new_model);
    }

    double dphi = Math.PI / num_phi;
    double dtheta = 2 * Math.PI / num_theta;

    // Remember the first point.
    int pt0 = sphere_mesh.Positions.Count;

    // Make the points.
    double phi1 = Math.PI / 2;
    for (int p = 0; p <= num_phi; p++)
    {
        double r1 = radius * Math.Cos(phi1);
        double y1 = radius * Math.Sin(phi1);

        double theta = 0;
        for (int t = 0; t <= num_theta; t++)
        {
            sphere_mesh.Positions.Add(new Point3D(
                cx + r1 * Math.Cos(theta),
                cy + y1,
                cz + -r1 * Math.Sin(theta)));
            sphere_mesh.TextureCoordinates.Add(new Point(
                (double)t / num_theta, (double)p / num_phi));
            theta += dtheta;
        }
        phi1 -= dphi;
    }

    // Make the triangles.
    int i1, i2, i3, i4;
    for (int p = 0; p <= num_phi - 1; p++)
    {
        i1 = p * (num_theta + 1);
        i2 = i1 + (num_theta + 1);
        for (int t = 0; t <= num_theta - 1; t++)
        {
            i3 = i1 + 1;
            i4 = i2 + 1;
            sphere_mesh.TriangleIndices.Add(pt0 + i1);
            sphere_mesh.TriangleIndices.Add(pt0 + i2);
            sphere_mesh.TriangleIndices.Add(pt0 + i4);

            sphere_mesh.TriangleIndices.Add(pt0 + i1);
            sphere_mesh.TriangleIndices.Add(pt0 + i4);
            sphere_mesh.TriangleIndices.Add(pt0 + i3);
            i1 += 1;
            i2 += 1;
        }
    }
}

The method starts by creating a mesh if necessary. It then uses spherical coordinates to generate points on the sphere. It loops through phi values between 0 and π/2, and theta values between 0 and 2π.

As it generates the points, it adds them to the mesh’s Positions collection. It also adds their texture coordinates to the mesh’s TextureCoordinates collection. The texture coordinates are scaled so they range from 0 to 1 for the phi and theta values.

After is finishes defining the points, the code loops through the phi and theta values again and defines the sphere’s triangles. It takes some non-trivial bookkeeping to keep track of which points should be combined to make the triangles.

Because adjacent triangles share vertices, the sphere is smooth.

It may not be obvious, but finding a good image to wrap around the sphere is one of the harder parts of the program. This example uses the best globe image I could find after a fair amount of searching.

[3D globe]

When the program is working with the top and bottom of the sphere, the trapezoids it uses to build the sphere degenerate into triangles as shown in the picture on the right. That means the texture image fits the sides reasonably well but it probably won’t fit the top or bottom very well.



There are several ways you can address that problem.

[3D globe]

  • You can use a lot of phi values. That makes the triangles at the sphere’s top and bottom smaller so any mismatch is smaller.
  • You can pick a texture image that has a solid color at the top and bottom. If the triangles are solid colors, then they will match up well.
  • You can explicitly pick a texture designed to exactly fit the sphere. Its top and bottom would be warped to fit triangular areas. Unfortunately mapping the points to places on the warped image would be hard.
  • You could use a geodesic tiling of the sphere. In theory that would provide the best result but I don’t even know where you would get a texture map for that.
  • You can just ignore the issue. If the texture is chaotic, the user probably won’t notice.

This is why there are so many different kinds of maps of the globe. None of them are perfectly accurate everywhere. The most accurate map of the Earth is a globe.

This example relies on the fact that the North Pole is mostly water, the South Pole is mostly land, and that most people don’t really know what the poles look like in detail.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.

This entry was posted in algorithms, drawing, geometry, graphics, mathematics, wpf, XAML and tagged , , , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *