Click here to Skip to main content
15,878,814 members
Articles / Desktop Programming / WPF

Terrain Generator and 3D WPF Representation

Rate me:
Please Sign up or sign in to vote.
4.87/5 (34 votes)
28 Aug 2017CPOL9 min read 37.2K   1.3K   56   12
Simple terrain generator and representation through WPF Viewport3D

Introduction

This article is intended for all those people who want to approach the 3D visualization in WPF. Instead of just displaying the classic simple square / triangle, we will represent a simple 2D map in a 3D environment.

Image 1

Background

For basic information regarding 3D graphics in WPF, please refer to this very useful and easy explanation.

For the map generation, I will use an algorithm very well explained here.

Using the Code

My sample is made up of two files:

terrainGenerator.cs

Which contain the map generator to create the Heightmap.
In computer graphics, a heightmap or heightfield is a raster image used to store values, such as surface elevation data, for display in 3D computer graphics. For more information, check out this link.

The code is a partial port of the code from here.

Quote:

The algorithm

Here's the idea: take a flat square. Split it into four sub-squares, and move their center points up or down by a random offset. Split each of those into more sub-squares and repeat, each time reducing the range of the random offset so that the first choices matter most while the later choices provide smaller details.

That's the midpoint displacement algorithm. Our diamond-square algorithm is based on similar principles but generates more natural-looking results. Instead of just dividing into sub-squares, it alternates between dividing into sub-squares and dividing into sub-diamonds.

If you have time, please visit the site, the algorithm is really very well explained. It will also go further in explaining a 2D rendering with pseudo lighting effect.
Here, I will give you a short explanation:

Image 2

  1. We set manually the four starting corners of the terrain height map (see "image 1").
  2. We find the height map value of the center of the square by averaging the corner point plus a random value (see "image 2").
  3. We trace a diamond and we find the height map value of the corners by averaging its diamond neighbours (see "image 3").
  4. Now we can divide the resulting image into sub-square, and if the size is greater than 1 pixel, we go back to point 2.

MainWindow.cs

This contains the main controls but also the 3D representation part. When the user clicks on the generate button (_GenerateTerrainButtonClick):

  1. Generation of the terrain heightmap:
    C#
    ...
    
    //generate terrain
    TerrainGenerator tg = new TerrainGenerator(detailValue);
    tg.Generate(roughnessValue);
    
    ...

    Detail value indicates the level of detail of the map, more detail will result in a bigger map created. Default value is set to 9 (which results in a 513x513 map). The Map is a two dimensional array of float. Each dimension of the array is calculated as:

    C#
    ...
    _Size = (int)(Math.Pow(2, detail) + 1);
    _Map = new float[_Size,_Size];
    ...

    roughnessValue determines whether the terrain is smooth (values near zero) or mountainous (values near one). Default value is set to 0.3.

  2. Calculation of the minimum and maximum of the heightmap:

    Calculates the minimum and maximum value of the heightmap to visualize the map correctly centered.

  3. 3D visualization:

    In my code, the XAML file contains:

    XAML
    <Viewport3D Name="_MyViewport3D">
        <Viewport3D.Camera>
            <PerspectiveCamera x:Name = "_MainPerspectiveCamera" 
             Position = "0 0 2048" LookDirection = "0 0 -1" />
        </Viewport3D.Camera>
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup x:Name="_MyModel3DGroup">
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>
    </Viewport3D>        

    Viewport3D is a component that renders the contained 3-D content within the 2-D layout bounds of the Viewport3D element. It will contain all the elements that we need to represent our 3D scene:

    • A camera like in a film. A 3-D scene, like in the real world, looks different depending on the point of view. The Camera class allows you to specify this point of view for a 3-D scene by setting the correct Position and LookDirection variable.
      There are different types of camera:

      Quote:

      The ProjectionCamera allows you to specify different projections and their properties to change how the onlooker sees 3-D models. A PerspectiveCamera specifies a projection that foreshortens the scene. In other words, the PerspectiveCamera provides vanishing-point perspective. You can specify the position of the camera in the coordinate space of the scene, the direction and field of view for the camera, and a vector that defines the direction of "up" in the scene.

      https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview

    • A light to illuminate the scene. Light in 3-D graphics does what lights do in the real world: they make surfaces visible.
      There are different type of lights:

      Quote:
      • AmbientLight: Provides ambient lighting that illuminates all objects uniformly regardless of their location or orientation.
      • DirectionalLight: Illuminates like a distant light source. Directional lights have a Direction specified as a Vector3D, but no specified location.
      • PointLight: Illuminates like a nearby light source. PointLights have a position and cast light from that position. Objects in the scene are illuminated depending on their position and distance with respect to the light. PointLightBase exposes a Range property, which determines a distance beyond which models will not be illuminated by the light. PointLight also exposes attenuation properties which determine how the light's intensity diminishes over distance. You can specify constant, linear, or quadratic interpolations for the light's attenuation.
      • SpotLight: Inherits from PointLight. Spotlights illuminate like PointLight and have both position and direction. They project light in a cone-shaped area set by InnerConeAngle and OuterConeAngle properties, specified in degrees.

      https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview

      In my code, I add the light in code because I want to put it in a specific position related to the size of the map.

      C#
      ...
      PointLight pointLight = new PointLight
      (Colors.White, new Point3D(tg.Size / 2, tg.Size / 2, tg.Size * 3 / 5));
      ...

      Light can be applied globally (as in the real world) by adding them to the Viewport3D or to a specific object/group of object to obtain some special effect.

    • The 3D object to represent.

      Basically, any surface structure can be represented as a bunch of triangles. The triangle is the most atomic and primitive geometry.

      Image 3

      Currently, the WPF supports 3D geometries with GeometryModel3D.

      Quote:

      To build a model, begin by building a primitive, or mesh. A 3-D primitive is a collection of vertices that form a single 3-D entity. Most 3-D systems provide primitives modeled on the simplest closed figure: a triangle defined by three vertices. Because the three points of a triangle are coplanar, you can continue adding triangles in order to model more complex shapes, called meshes.

      The WPF 3-D system currently provides the MeshGeometry3D class, which allows you to specify any geometry; it does not currently support predefined 3-D primitives like spheres and cubic forms. Begin creating a MeshGeometry3D by specifying a list of triangle vertices as its Positions property. Each vertex is specified as a Point3D. (In Extensible Application Markup Language (XAML), specify this property as a list of numbers grouped in threes that represent the coordinates of each vertex.) Depending on its geometry, your mesh might be composed of many triangles, some of which share the same corners (vertices). To draw the mesh correctly, the WPF needs information about which vertices are shared by which triangles. You provide this information by specifying a list of triangle indices with the TriangleIndices property. This list specifies the order in which the points specified in the Positions list will determine a triangle.

      https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview

      For each object in our world, we can also define the Material of which the object is made of. The light will interact with the material properties according to the material specification:

      Quote:

      To define the characteristics of a model's surface, WPF uses the Material abstract class. The concrete subclasses of Material determine some of the appearance characteristics of the model's surface, and each also provides a Brush property to which you can pass a SolidColorBrush, TileBrush, or VisualBrush.

      • DiffuseMaterial specifies that the brush will be applied to the model as though that model were lit diffusely. Using DiffuseMaterial most resembles using brushes directly on 2-D models; model surfaces do not reflect light as though shiny.

      • SpecularMaterial specifies that the brush will be applied to the model as though the model's surface were hard or shiny, capable of reflecting highlights. You can set the degree to which the texture will suggest this reflective quality, or "shine," by specifying a value for the SpecularPower property.

      • EmissiveMaterial allows you to specify that the texture will be applied as though the model were emitting light equal to the color of the brush. This does not make the model a light; however, it will participate differently in shadowing than it would if textured with DiffuseMaterial or SpecularMaterial.

      https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview

      After this little explanation, I can go back with the explanation of the code, to generate the terrain I perform 3 pass:

      • Design the terrain.
        For the terrain, I will use a DiffuseMaterial with a uniform LimeGreen color applied.
        I will go through my generated map to create a collection of Point3d where I set X and Y from the map coordinates, while for the Z coordinate, I will use the value of the heightmap relative to the heightmap minimum and maximum value.

        C#
        ((MeshGeometry3D)myTerrainGeometryModel.Geometry).Positions = point3DCollection;

        All these points will be joined by using triangles.

        Image 4

        To perform this operation, we must indicate which 3D point we must use to generate triangle. We can do this by creating a collection of indexes.

        C#
        ((MeshGeometry3D)myTerrainGeometryModel.Geometry).TriangleIndices = 
                                                              triangleIndices;

        Every entry in this collection is an index in the Position list.
        Every triple of indexes in this list represents a triangle. For a triangle in a given 3-D mesh, the order in which the triangle's vertex positions are specified determines whether the triangle face is a front or back face.The WPF 3-D implementation uses a counter-clockwise winding order; that is, the points that determine a front-facing mesh triangle's positions should be specified in counterclockwise order, as viewed from the front of the mesh.

        C#
        /**
         * <summary>
         * Method that create the 3d terrain on a Viewport3D control
         * </summary>
         *
         * <param name="terrainMap">terrain to show</param>
         * <param name="terrainSize">terrain size</param>
         * <param name="minHeightValue">minimum terraing height</param>
         * <param name="maxHeightValue">maximum terraing height</param>
         */
        private void _DrawTerrain(float[,] terrainMap, int terrainSize, float minHeightValue, float maxHeightValue)
        {
            float halfSize = terrainSize / 2;
            float halfheight = (maxHeightValue - minHeightValue) / 2;
        
        	// creation of the terrain
        	GeometryModel3D myTerrainGeometryModel = new GeometryModel3D
        	(new MeshGeometry3D(), new DiffuseMaterial(new SolidColorBrush(Colors.GreenYellow)));
        	Point3DCollection point3DCollection = new Point3DCollection();
        	Int32Collection triangleIndices = new Int32Collection();
        
        	//adding point
        	for (var y = posY; y < maxPosY; y++) {
        		for (var x = posX; x < maxPosX; x++) {
        			point3DCollection.Add(new Point3D(x - halfSize, y - halfSize, terrainMap[x, y] - halfheight));
        		}
        	}
        	((MeshGeometry3D)myTerrainGeometryModel.Geometry).Positions = point3DCollection;
        
        	//defining triangles
        	int ind1 = 0;
        	int ind2 = 0;
        	int xLenght = maxPosX ;
        	for (var y = posY; y < maxPosY - 1; y++) {
        		for (var x = posX; x < maxPosX - 1; x++) {
        			ind1 = x + y * (xLenght);
        			ind2 = ind1 + (xLenght);
        
        			//first triangle
        			triangleIndices.Add(ind1);
        			triangleIndices.Add(ind2 + 1);
        			triangleIndices.Add(ind2);
        
        			//second triangle
        			triangleIndices.Add(ind1);
        			triangleIndices.Add(ind1 + 1);
        			triangleIndices.Add(ind2 + 1);
        		}
        	}
        	((MeshGeometry3D)myTerrainGeometryModel.Geometry).TriangleIndices = triangleIndices;
        
        	_MyModel3DGroup.Children.Add(myTerrainGeometryModel);
        }
      • After designing the terrain, I create some layer to add "water effect" to my world.

        To gain a simple but effective water instead of using a 'simple' DiffuseMaterial, I use an EmissiveMaterial with a uniform Blue color with an opacity of 0.2.
        I could have used a single square at a certain height to obtain a nice effect, but I preferred to use 10 layers to give a sense of depth to the water.

        Image 5

        C#
        /**
         * <summary>
         * Method that create a water effect for the terrain
         * </summary>
         *
         * <param name="terrainMap">terrain to show</param>
         * <param name="terrainSize">terrain size</param>
         * <param name="minHeightValue">minimum terraing height</param>
         * <param name="maxHeightValue">maximum terraing height</param>
         * <param name="waterHeightValue">water height value</param>
         */
        private void _DrawWater(float[,] terrainMap, 
        int terrainSize, float minHeightValue, float maxHeightValue, float waterHeightValue)
        {
            float halfSize = terrainSize / 2;
            float halfheight = (maxHeightValue - minHeightValue) / 2;
        
            // creation of the water layers
            // I'm going to use a series of emissive layer for water
            SolidColorBrush waterSolidColorBrush = new SolidColorBrush(Colors.Blue);
            waterSolidColorBrush.Opacity = 0.2;
            GeometryModel3D myWaterGeometryModel = 
            new GeometryModel3D(new MeshGeometry3D(), new EmissiveMaterial(waterSolidColorBrush));
            Point3DCollection waterPoint3DCollection = new Point3DCollection();
            Int32Collection triangleIndices = new Int32Collection();
        
            int triangleCounter = 0;
            float dfMul = 5;
            for (int i = 0; i < 10; i++) {
        
                triangleCounter = waterPoint3DCollection.Count;
        
                waterPoint3DCollection.Add(new Point3D(-halfSize, -halfSize, waterHeightValue - i * dfMul - halfheight));
                waterPoint3DCollection.Add(new Point3D(+halfSize, +halfSize, waterHeightValue - i * dfMul - halfheight));
                waterPoint3DCollection.Add(new Point3D(-halfSize, +halfSize, waterHeightValue - i * dfMul - halfheight));
                waterPoint3DCollection.Add(new Point3D(+halfSize, -halfSize, waterHeightValue - i * dfMul - halfheight));
        
                triangleIndices.Add(triangleCounter);
                triangleIndices.Add(triangleCounter + 1);
                triangleIndices.Add(triangleCounter + 2);
                triangleIndices.Add(triangleCounter);
                triangleIndices.Add(triangleCounter + 3);
                triangleIndices.Add(triangleCounter + 1);
            }
            
            ((MeshGeometry3D)myWaterGeometryModel.Geometry).Positions = waterPoint3DCollection;
            ((MeshGeometry3D)myWaterGeometryModel.Geometry).TriangleIndices = triangleIndices;
            _MyModel3DGroup.Children.Add(myWaterGeometryModel);
        }
      • Now my world is quite complete, but I have to build a containing box in order to hide some part of the object when we rotate it.

        Image 6

        The box consists of a simple black wall.

  4. 3D navigation with mouse interaction:
    For 3D navigation, I used the code from here.

Points of Interest

When I approached 3D, I found a very simple and intuitive tutorial that was explaining the very basic knowledge but they did not get me involved. I hope this tutorial will be a bit more funny to understand and use.
This is my very first approach to 3D so if you have any suggestions for modifications, please don't hesitate to contact me.

History

  • Version 1.0.0 - July 2017 - First release
  • Version 1.0.1 - July 2017 - Limited the details of the map generation to 12, the 3D terrain generation is now divided in cells of maximum dim 4096*4096
  • Version 1.0.2 - August 2017 - Optimized point definition, now I don't duplicate point already inserted
  • Version 1.0.3 - September 2017 - Optimized the division in cell, now with certain graphic card, we can push the limit of the details to 13

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader ATS med
Italy Italy
I started programming at Teinos, in a 5 person software working team in march 2004. I got some experience interfacing our main program to various external applications. All of these experiences allowed me to get in touch with many people: end-user, technician and commercial one; learning how to interface myself with these people. After some period I was moved to 'single' software application development. This led me to learn the whole process of software development. From the definition of the specifications to the software release and many times also the installation of the product to the end-user.

In 2009, I moved to ATS. I was charged as Lead Software Developer in a team for a long term new project: Primo (released for the first time in 2010-Q3). The software part for this project was completely written from scratch. These experience led me to understand how to organize the work of a team and take critical decision.

In 2014, in collaboration with a multinational corporation, we started the development of a completely new machine: ARCO FP (released for the first time in 2015-Q3). The software part for this project was completely written from scratch. This experience teaches me how to integrate our company with the needs and rules of other company and how to mitigate different approaches to the production phases.

Comments and Discussions

 
BugThe download doesn´t works Pin
assismauroSJC21-Aug-17 9:27
assismauroSJC21-Aug-17 9:27 
GeneralRe: The download doesn´t works Pin
timeback21-Aug-17 21:09
timeback21-Aug-17 21:09 
GeneralRe: The download doesn´t works Pin
Andreoli Carlo23-Aug-17 8:51
professionalAndreoli Carlo23-Aug-17 8:51 
GeneralRe: The download doesn´t works Pin
Member 1410051130-Dec-18 20:30
Member 1410051130-Dec-18 20:30 
GeneralUnable to download the attachment! Pin
akjoshi14-Aug-17 0:06
akjoshi14-Aug-17 0:06 
GeneralRe: Unable to download the attachment! Pin
Andreoli Carlo23-Aug-17 8:50
professionalAndreoli Carlo23-Aug-17 8:50 
QuestionInteresting article Pin
AnotherKen1-Aug-17 14:37
professionalAnotherKen1-Aug-17 14:37 
GeneralMy vote of 5 Pin
Robert_Dyball26-Jul-17 21:22
professionalRobert_Dyball26-Jul-17 21:22 
Nice work, thank you very much for your effort in sharing this.
Questionnice job Pin
avisal25-Jul-17 10:00
professionalavisal25-Jul-17 10:00 
AnswerRe: nice job Pin
Andreoli Carlo25-Jul-17 22:10
professionalAndreoli Carlo25-Jul-17 22:10 
GeneralRe: nice job Pin
avisal26-Jul-17 1:44
professionalavisal26-Jul-17 1:44 
PraiseGreat Pin
Alexander5524-Jul-17 5:24
Alexander5524-Jul-17 5:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.