Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Height Map Rendering with Igneel Engine

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
7 Mar 2017CPOL9 min read 13.1K   257   3   3
Describes the basic steps to render height maps using the graphics engine of Igneel

Introduction

Igneel Engine is a software development platform targeting .NET runtime for develop real-time 3D graphic applications. This platform results especially useful for writing games, simulation or interactive 3D visualization softwares. Igneel Engine hides all the complexity involved in the development of such types of applications and let you manage your 3D scene in an easy way without having to worry about the heavy math begin. Also thanks to its physics engine you can add realistic physical behavior to your 3D objects. Furthermore the physics engine is implemented using NVIDIA PhysX so you can take benefits for GPU acceleration. Another cool feature of Igneel Engine is its animation engine that allows you to performs complex animation sequences involving several types of animations from properties to skeleton key-frames with different interpolation methods.

The platform was designed to be customizable and able to scale over time by providing an abstract interface or contract the users interact to. Those abstractions are implemented in a mixture of native and managed code in order to achieve the high performance required in real-time applications. Even more, the abstractions can be implemented using different native APIs and thus allowing the user's application to run in different environments without having to change the base code. For example the user application can run in a windows environment with DirectX or a Linux environment with .NET Core and OpenGL.

All rendering in Igneel engine is done using shaders with a unique mechanism by using dynamic type generation for accesing shader variables in GPU memory as described in Rendering 3D Graphic in .NET with C# and Igneel.Graphics. Something to point out is that unlike most game engines where shaders are attached to materials, Igneel Engine does not follows that pattern instead shaders are grouped into Effects and attached to a Render which defines the logics for drawing a component. Furthermore those renders can be replaced or combined in runtime without complication to achieve interesting visual effects. Therefore an object with the same visual material can be rendered in many ways by using different effects.

Igneel engine architecture diagram

Image 1

 

Height Maps

Height maps are a technique most used for rendering terrains. It consist  in having a grayscale image where every pixel defines an altitude or a height value. Then you map the image onto a 3D grid by sampling and scaling the y-coordinate (assuming y-axis is pointing up ) from the image. Also dependending the image pixel format is the range of heights you can store on it. The sampled color for the height is scaled to [0-1] so after you can scaled it to your desired height by applying a scaling transform to the HeightField scene node.

Background

As a background it will be good to take a look to my article about Igneel.Graphics which describes by using an example how to use the low-level rendering API of Igneel Engine. The sample for rendering height fields in this article uses the high-level rendering module of Igneel Engine composed of Tehniques, Renders, Effects, Materials and Bindings.

Using the code

The example presented here for rendering height fields is hosted in a WPF applications. WPF has its own internal rendering loop and also uses Direct3D9 for rendering 3D content. In constrat Igneel.Graphics implementation is on Direc3D10 therefore a Canvas3D WPF control can be used in order to integrate Igneel Engine with the application. On the other hand you need to handle the engine initialization routines which will be covered in the Bootstrapper class description.

But first of all a simple WPF application is created. The XAML markup showed in listing 1 imports from Igneel.Windows.Wpf and declares the Canvas3D, all the rendering is done into this control.

XML
<Window x:Class="HeightFieldSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HeightFieldSample"
        mc:Ignorable="d"
        xmlns:c="clr-namespace:Igneel.Windows.Wpf;assembly=Igneel.Windows.Wpf"
        Title="MainWindow" Height="350" Width="525"
        WindowState="Maximized"
        Loaded="MainWindow_Loaded" >
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition  Height="auto" />
        </Grid.RowDefinitions>

        <c:Canvas3D Name="Canvas"  />

        <!--Footer-->
        <StatusBar Grid.Row="3"  VerticalAlignment="Bottom">
            <StatusBarItem>
                <StackPanel Orientation="Horizontal">
                    <TextBlock>FPS:</TextBlock>
                    <TextBlock Name="FPS" />
                </StackPanel>
            </StatusBarItem>
        </StatusBar>
        
    </Grid>
</Window>
Listing 1 Main Windows XAML

Engine initialization

The Boostrapper is the class that contains all the logic for initializing the engine. The Init method is called in the MainWindows Load event handler. It receives the MainWindows and the Canvas3D instances. see Listing 2 below.

C#
 /// <summary>
/// Initialize the Engine
/// </summary>
public class Bootstraper
{
    static readonly string[] _shaderRepositoryDir =
    {
        //IGNEEL CORE SHADERS
        "Shaders/Binaries/"
    };

    public static void Init(Window window, Canvas3D mainCanvas)
    {
        //Initialize Graphics API
        Service.Set<GraphicDeviceFactory>(new IgneelD3D10.GraphicManager10());

        //Initialize Input API
        Service.Set<Igneel.Input.InputManager>(new IgneelDirectInput.DInputManager());

        //Initialize shader respository
        ShaderRepository.SetupD3D10_SM40(_shaderRepositoryDir);

        var gfactory = Service.Require<GraphicDeviceFactory>();
        var iFactory = Service.Get<Igneel.Input.InputManager>();

        //Helper to get the HWND Handle
        var interopHelper = new WindowInteropHelper(window);

        //The canvas size
        var size = mainCanvas.RenderSize;

        //Initialize the Canvas. The Canvas3D is used to render to the screen in a WPF context
        //this is achived by creating a GraphicPresenter
        mainCanvas.Init();

        //Initialize the Engine and creating the IGraphicContext by setting the BackBuffer and DepthStencil descriptions
        Engine.Initialize(new InputContext(interopHelper.Handle), new GraphicDeviceDesc
        {
            Adapter = 0,
            DriverType = GraphicDeviceType.Hardware,
            Context = new WindowContext(mainCanvas.Handle)
            {
                BackBufferWidth = (int)size.Width,
                BackBufferHeight = (int)size.Height,
                BackBufferFormat = Format.R8G8B8A8_UNORM_SRGB,
                DepthStencilFormat = Format.D24_UNORM_S8_UINT,
                FullScreen = false,
                Sampling = new Multisampling(1, 0),
                Presentation = PresentionInterval.Default
            }
        });

        //Set the Engine presenter to the Canvas3D presenter
        //this instruct the Engine to render the content using the Canvas3D GraphicPresenter
        Engine.Presenter = mainCanvas.CreateDafultPresenter();

        //Set default Lighting and Shading properties
        EngineState.Lighting.HemisphericalAmbient = true;
        EngineState.Lighting.Reflection.Enable = true;
        EngineState.Lighting.TransparencyEnable = true;
        EngineState.Shading.BumpMappingEnable = true;

        //Initialize the Hight Level rendering System
        //This will register the renders fo each components for each supported shading techniques
        InitializeRendering();

    }


    private static void InitializeRendering()
    {
        // Register Engine components First
        foreach (var type in typeof(IgneelApplication).Assembly.ExportedTypes)
        {
            if (!type.IsAbstract && !type.IsInterface && type.IsClass && type.GetInterface("IRenderRegistrator") != null)
            {
                ((IRenderRegistrator)Activator.CreateInstance(type)).RegisterInstance();
            }
        }

        //Register Custom components here

    }

}
Listing 2: Engine Initialization

In the Init method we initialize the graphics and input APIs. If physics simulation is required it must be initialized here as well. The first steps to initialize the engine is to register the API implementations ,this is done in the section of code showed bellow in Listing 3.

C#
//Register the Graphics API implementation
Service.Set<GraphicDeviceFactory>(new IgneelD3D10.GraphicManager10());

//Register the Input API implementation
Service.Set<Igneel.Input.InputManager>(new IgneelDirectInput.DInputManager());
Listing 3 Register API implementation with the IOC container

After you must register the paths that forms your shader respository. The shader respository is the locations where you store the shader code. Also you must specify the core engine shader location, those shaders are already pre-compiled and optimized. In addition you can replace it with your own implementation or specify new ones by adding a new line to the _shaderRepositoryDir array.

C#
//Initialize shader respository
ShaderRepository.SetupD3D10_SM40(_shaderRepositoryDir);

Then the engine is initialized by calling Engine.Initialize(IInputContext inputContext, GraphicDeviceDesc graphicDeviceDesc).  The call requires two parameters, the first is the user's input context which is the whole windows, the second contains the values for creating the GraphicDevice and use the Canvas3D windows handle for creating the back buffer and swap chain.

Another important initialization is the engine's GraphicPresenter this object is very important, it will set all the appropriated parameters previous and after the rendering is done. Unfortunately there is no space here to cover all aspect of Igneel Engine but the GraphicPresenter is a feature that allows you to customize the rendering workflow, like for instance to render to several screens or sections in the window's client area.

C#
//Set the Engine presenter to the Canvas3D presenter
//this instruct the engine to render the contents using the Canvas3D GraphicPresenter
 Engine.Presenter = mainCanvas.CreateDafultPresenter();

The last step during initialization is the registering of all the Renders for each supported Technique. This is done in the method InitializeRendering. Additionally you can register your own component's Renders.

Creating the Scene

In Igneel Engine the Scene is the main container for all the visual components including animations, cameras, lights, meshes, skeletton and height fields amount others. Also all visual components must be attached to a Frame that positions the component within the Scene by defining its spatial properties like scaling, translation and rotation.In addition those properties are relative to a parent Frame conforming in that way the scene heirarchy of Frames, with the top most called the root. As a result transforming the root will transform the whole scene.

The listing 4 will show the MainWindows class in where inside the Loaded event handler we initialize the engine followed by the scene, camera, terrain and light creation. Also the Engine.Presenter.Rendering event is hocked so actions can be taken after a loop step or frame is rendered. In this case we show the FPS or frames per second ratio.

C#
using Igneel;
using Igneel.Components;
using Igneel.Controllers;
using Igneel.Graphics;
using Igneel.Input;
using Igneel.SceneComponents;
using Igneel.SceneManagement;
using System.Windows;

namespace HeightFieldSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {     

        private float fps;
        private float baseTime;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Bootstraper.Init(this, Canvas);

            CreateScene();

            CreateCamera();

            CreateTerrain();

            CreateLight();

            Engine.Presenter.Rendering += Presenter_Rendering;

            Engine.StartGameLoop();
        }

     

        private void CreateScene()
        {
            //Create a scene
            //The scene is the primary container for all the graphic objects
            var scene = new Scene("Default Scene");

            //Set scene hemispherical ambient colors
            scene.AmbientLight.SkyColor = new Vector3(0.8f);
            scene.AmbientLight.GroundColor = new Vector3(0.2f);
            
            //Set the engine scene
            Engine.Scene = scene;            
        }

        private void CreateCamera()
        {
            var scene = Engine.Scene;             
            
            //Compute the render target aspect ratio                                      
            float aspect = (float)Engine.Graphics.BackBuffer.Width / (float)Engine.Graphics.BackBuffer.Height;

            //Create a First-Person camera controller. The controller will responds to user inputs and move the camera
            var controller = new FpController()
            {
                MoveScale = 10.0f,
                RotationScale = 0.5f,
                BreakingTime = 0.2f,

                //The callback that is called during a frame update ,if it returns true the camera respond to the user input
                UpdateCallback = c => Engine.Mouse.IsButtonPresed(Igneel.Input.MouseButton.Middle) ||
                                      (Engine.Mouse.IsButtonPresed(Igneel.Input.MouseButton.Left) &&
                                       Engine.KeyBoard.IsKeyPressed(Keys.Lalt)),

                //Create the camera and the camera node
                Node = scene.Create("cameraNode", Camera.FromOrientation("camera", zn: 0.05f, zf: 1000f).SetPerspective(Numerics.ToRadians(60), aspect),
                            localPosition: new Vector3(0, 200, -500),
                            localRotation: new Euler(0, Numerics.ToRadians(30), 0).ToMatrix())
            };
                           
            scene.Dynamics.Add(new Dynamic(x => controller.Update(x)));          
        }

        private void CreateLight()
        {
            //Create a light ,the light containg properties like colors and specular powers
            var light = new Light("WhiteLight")
            {
                Diffuse = new Color3(1,1,1),
                Specular = new Color3(0,0,0),
                SpotPower = 8,
                Enable=true
            };

            //Assign the light to a FrameLight wich acts like and adapter for the scene node 
            //so it will set light spatial properties like direccion and position when the scene node change its pose.
            var lightFrame = new FrameLight(light);

            Engine.Scene.Create("LightNode", lightFrame, new Vector3(0, 50, 0), new Euler(0, 60, 0));                              

        }

        private void CreateTerrain()
        {
            //Load the height map            
            Texture2D heigthMap = Engine.Graphics.CreateTexture2DFromFile("terrain.png");            

            //Create the HeightField using the heigth map ,divide the HeightField into and 8x8 grid of sections 
            //this will improve culling
            HeightField heigthField = new HeightField(heigthMap,32,32);

            heigthField.Materials[0].Diffuse =Color3.FromArgb(System.Drawing.Color.DarkGreen.ToArgb());

            //Uncomment this to texture the terrain
            //heigthField.Materials[0].DiffuseMaps = new Texture2D[]
            //{
            //    Engine.Graphics.CreateTexture2DFromFile("grass.jpg")
            //};

            //smoot the height field using a 5x5 gaussian kernel with 4 pass
            heigthField.Smoot(5, 4);

            //Create the HeightField node translat it to the center of the scene, then scaling it to 1000 units in X and Z
            Engine.Scene.Create("HeightFieldNode", heigthField,
                Igneel.Matrix.Translate(-0.5f, 0, -0.5f) *
                Igneel.Matrix.Scale(1000, 100, 1000));
        }


        void Presenter_Rendering()
        {
            if (fps == -1)
            {
                fps = 0;
                baseTime = Engine.Time.Time;
            }
            else
            {
                float time = Engine.Time.Time;
                if ((time - baseTime) > 1.0f)
                {
                    Dispatcher.Invoke(delegate ()
                    {
                        FPS.Text = fps.ToString();
                    });

                    fps = 0;
                    baseTime = time;
                }
                fps++;
            }

        }

        protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
        {
            Engine.Presenter.Rendering -= Presenter_Rendering;

            Engine.StopGameLoop();

            base.OnClosing(e);
        }

    }
}

After initializing the Engine by calling Bootstraper.Init(this, Canvas) the scene is created in the CreateScene method assigning a name. The scene ambient ligh colors are also defined in this method and two colors are set the Sky and Ground colors which are used in the hemispherical lighting that is the default ambient lighting technique. After that by setting the Engine.Scene property will stablish the newlly created scene as the current scene for rendering. So one scene can be active at a time.

C#
private void CreateScene()
{
    //Create a scene
    //The scene is the primary container for all the graphic objects
    var scene = new Scene("Default Scene");

    //Set scene hemispherical ambient colors
    scene.AmbientLight.SkyColor = new Vector3(0.8f);
    scene.AmbientLight.GroundColor = new Vector3(0.2f);

    //Set the engine scene
    Engine.Scene = scene;
}

Then in order to see anything you need to create a Camera. The Camera is created in the CreateCamera method. Also a FpController is created that will control the camera behavior and responses to user inputs in a first person camera style. Therefore to be called during the frame update,  the controller will be added to the dynamic's collection of the scene, wrapped Dynamic object.

The FpController.UpdateCallback delegate will be called for deciding whether the controller must update the camera's node position and orientation or not.

The camera's node is created in the following lines of codes:

C#
Node = scene.Create("cameraNode", Camera.FromOrientation("camera", zn: 0.05f, zf: 1000f).SetPerspective(Numerics.ToRadians(60), aspect),
                       localPosition: new Vector3(0, 200, -500),
                       localRotation: new Euler(0, Numerics.ToRadians(30), 0).ToMatrix())

In the previus section a Camera component is created with persperctive projection of 60 grades for fied of view. The camera is then attached to a Frame by calling Scene.Create passing the camera ,the position and orientation. The Frame is assigned as a direct child of the scene root.

The light is created in the CreateLight method with the same pattern ,first the Light component is created then the Frame. But in this case a FrameLight is instantiated which functions as an adapter between the Light and the Frame as shown bellow. This design allows to reuse a Light in several positions and orientation in the Scene.

C#
private void CreateLight()
{
    //Create a light ,the light containg properties like colors and specular powers
    var light = new Light("WhiteLight")
    {
        Diffuse = new Color3(1,1,1),
        Specular = new Color3(0,0,0),
        SpotPower = 8,
        Enable=true
    };

    //Assign the light to a FrameLight wich is an adapter for the scene node
    //It will set light spatial properties like direccion and position after the node is transformed.
    var lightFrame = new FrameLight(light);

    //Attach the FrameLight to the scene
    Engine.Scene.Create("LightNode", lightFrame, new Vector3(0, 50, 0), new Euler(0, 60, 0));
}

Now just left to create the terrain from the height map to complete our scene.

C#
private void CreateTerrain()
{
    //Load the height map
    using (Texture2D heigthMap = Engine.Graphics.CreateTexture2DFromFile("terrain.png"))
    {
        //Create the HeightField using the heigth map
        HeightField heigthField = new HeightField(heigthMap);

        //Set diffuse color for default material layer
        heigthField.Materials[0].Diffuse = Color3.FromArgb(System.Drawing.Color.DarkGreen.ToArgb());

        //uncomment to texture the terrain
        //heigthField.Materials[0].DiffuseMaps = new Texture2D[]
        //{
        //    Engine.Graphics.CreateTexture2DFromFile("grass.jpg")
        //};

        //Apply a smoot filter by using a 5x5 gaussian kernel with 4 passes
        heigthField.Smoot(5, 4);

        //Create the HeightField node translating it to the center of the scene,
        //then scaling it to 1000 units in X and Z
        Engine.Scene.Create("HeightFieldNode", heigthField,
            Igneel.Matrix.Translate(-0.5f, 0, -0.5f) *
            Igneel.Matrix.Scale(1000, 100, 1000));
    }
}

The previus method creates the terrain represented by the HeightField. It can be created by calling one of its constructor passing a grayscale texture containing the height values. Several texture formats are supported like:

  • R8G8B8A8_UNORM
  • R16G16B16A16_UNORM
  • R16G16B16A16_FLOAT
  • R32G32B32A32_FLOAT
  • A8_UNORM
  • R16_FLOAT
  • R32_FLOAT

The HeightField defines the Materials property that get or sets a array of LayeredMaterial. By default it contains one LayeredMaterial at index 0. The HeightField can be devided into several sections with one section assigned to an individual material. In addition a material defines the diffuse, specular ando other color properties. Also the LayeredMaterial defines a 4-lenght array of Diffuse ,Normal and Specular textures, those textures represent texture layers than can be applied to a terrain section using the BlendFactors texture, where each color channel represent a contribution value for the corresponding texture index. Also a good choice is to apply a smoot filter by calling HeightField.Smoot(int kernelSize, int times = 1, float stdDev = 3) wich apply a gaussian filter specifying the kernel size, the number of passes and the standar desviation. A recommendable kernel size is 5, but that depends how smoot you want your terrain.

Finally the terrain is assigned to its scene node, then translated to the center and scaled to 1000 units in X and Y and 100 units in Y. This translation was apply because the terrain is created in the [0,0,0]-[1,1,1] coordinate range so the maximun height value is mapped to 1 and the minimun to 0. In that way you can scale the terrain to any dimesion you desire.

At the end of the class in the OnClosing method we simple detach the Presenter_Rendering event handler and stop the game loop. So the aplication can be shoutdowned gracefully.

C#
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
    Engine.Presenter.Rendering -= Presenter_Rendering;

    Engine.StopGameLoop();

    base.OnClosing(e);
}

The Height Map

Image 2

The generated terrain geometry with one material

Image 3

 

Points of Interest

Game Engines are big and complex software systems to build and Igneel Engine was a challenge I embrace with enthusiasm. I have learned a lot studing about game engines architectures , graphics API and physics. So as a summary Igneel Engine is integraded by serveral modules or sub-sistemas in a seamless arquitecture, it is interesting how the platform allows the implementation of techniques like shadow-mapping, hdr, deferred rendering just for mention somes by the combination of Techniques and Renders using stacks. also it is remarkable how type-safe Effects are defined and the usings of IRenderBinding to bind components to Effects. I have in mind to write more about Igneel Engine applications and components , besides Igneel Engine code is available in github so your are welcome to contribute.

Also as a remark in order to run the sample you must install the DirectX SDK https://www.microsoft.com/en-us/download/details.aspx?id=6812  then locate the Redist folder inside the installation folder and run DXSETUP.exe. This will install the required Direct3D10 runtimes libraries used by the Engine.

 

License

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


Written By
Software Developer (Senior)
Uruguay Uruguay
Senior Software Engineer with more than 8 years of experience in the industry. Graduated from Computer Science ,focused on .NET and Java technologies with special interest on Computer Graphics, Compilers , Languages and Machine Learning.

Comments and Discussions

 
Questionyour sample Pin
avisal8-Mar-17 10:57
professionalavisal8-Mar-17 10:57 
AnswerRe: your sample Pin
Ansel Castro14-Mar-17 14:33
Ansel Castro14-Mar-17 14:33 
In order to run the sample you must install the DirectX SDK https://www.microsoft.com/en-us/download/details.aspx?id=6812 then locate the Redist folder inside the installation folder and run DXSETUP.exe. This will install the required Direct3D10 runtimes libraries used by the Engine.

After that if the problem continues just copy all the .dll files inside the Libs folder to bin/Debug.
GeneralRe: your sample Pin
avisal14-Mar-17 15:37
professionalavisal14-Mar-17 15:37 

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.