Terrain generator and 3D WPF representation

0
39

Introduction

This article is inteded to all the people that want to approch 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.

 

Background

For the basic information regarding the  3D graphics in WPF please refer to this very useful and easy exaplantion:
https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview

For the map generation I will use an algorithm very well explained http://www.playfuljs.com/realistic-terrain-in-130-lines/

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 info https://en.wikipedia.org/wiki/Heightmap

The code is a partial port of the code from http://www.playfuljs.com/realistic-terrain-in-130-lines/

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 exaplaining a 2d rendering with pseudo lighting effect.

 


  • MainWindow.cs

This contains the main controls but also the 3d representation part.

  1. When the user click on the generate button ( _GenerateTerrainButtonClick ) the program:
    ...
    
    
    TerrainGenerator tg = new TerrainGenerator(detailValue);
    tg.Generate(roughnessValue);
    
    ...


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


    ...
    _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. Generates of the heightmap:

    Calculates the minimum and maximum value of the heightmap to visualize the map correctly centered. When rotating on the height map it will rotate on the “water axis”

  3. Perform the 3d visualization:

    In my code the xaml file contains:


    <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 element 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 type 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 do what lights do in the real world: they make surfaces visible.
      There are different type of light:


      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 incode because I want to put it in a specific position related to the size of the map.


      ...
      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.
       



      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.


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


        All this point will be joined by using triangle,


        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.


        ((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.


        
        private void _DrawTerrain(float[,] terrainMap, int terrainSize, float minHeightValue, float maxHeightValue)
        {
 float halfSize = terrainSize / 2;
 float halfheight = (maxHeightValue - minHeightValue) / 2;
        
 
 GeometryModel3D myTerrainGeometryModel = new GeometryModel3D(new MeshGeometry3D(), new DiffuseMaterial(new SolidColorBrush(Colors.GreenYellow)));
 Point3DCollection point3DCollection = new Point3DCollection();
 Int32Collection triangleIndices = new Int32Collection();
        
 int triangleCounter = 0;
 for (var y = 0; y < terrainSize - 1; y++) {
 for (var x = 0; x < terrainSize - 1; x++) {
 float val = terrainMap[x, y];
        
 float valY = terrainMap[x, y + 1];
 float valX = terrainMap[x + 1, y];
 float valXY = terrainMap[x + 1, y + 1];
        
 triangleCounter = point3DCollection.Count;
        
 point3DCollection.Add(new Point3D(x - halfSize, y - halfSize, val - halfheight));
 point3DCollection.Add(new Point3D(x + 1 - halfSize, y + 1 - halfSize, valXY - halfheight));
 point3DCollection.Add(new Point3D(x - halfSize, y + 1 - halfSize, valY - halfheight));
 point3DCollection.Add(new Point3D(x + 1 - halfSize, y - halfSize, valX - halfheight));
        
 triangleIndices.Add(triangleCounter);
 triangleIndices.Add(triangleCounter + 1);
 triangleIndices.Add(triangleCounter + 2);
 triangleIndices.Add(triangleCounter);
 triangleIndices.Add(triangleCounter + 3);
 triangleIndices.Add(triangleCounter + 1);
 }
 }
 ((MeshGeometry3D)myTerrainGeometryModel.Geometry).Positions = point3DCollection;
 ((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.



        
        private void _DrawWater(float[,] terrainMap, int terrainSize, float minHeightValue, float maxHeightValue, float waterHeightValue)
        {
 float halfSize = terrainSize / 2;
 float halfheight = (maxHeightValue - minHeightValue) / 2;
        
 
 
 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



        The box consist of simple black wall.

  4. 3D navigation with mouse intercation:
    For the 3D navigation I used the code from: https://www.codeproject.com/Articles/23332/WPF-D-Primer

Points of Interest

When I approched 3D I found 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 suggestion and modify to suggest me please don’t esitate to contact me.

History

v 1.0.0 – July 2017 – First release

LEAVE A REPLY