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
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.
<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" />
-->
<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.
public class Bootstraper
{
static readonly string[] _shaderRepositoryDir =
{
"Shaders/Binaries/"
};
public static void Init(Window window, Canvas3D mainCanvas)
{
Service.Set<GraphicDeviceFactory>(new IgneelD3D10.GraphicManager10());
Service.Set<Igneel.Input.InputManager>(new IgneelDirectInput.DInputManager());
ShaderRepository.SetupD3D10_SM40(_shaderRepositoryDir);
var gfactory = Service.Require<GraphicDeviceFactory>();
var iFactory = Service.Get<Igneel.Input.InputManager>();
var interopHelper = new WindowInteropHelper(window);
var size = mainCanvas.RenderSize;
mainCanvas.Init();
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
}
});
Engine.Presenter = mainCanvas.CreateDafultPresenter();
EngineState.Lighting.HemisphericalAmbient = true;
EngineState.Lighting.Reflection.Enable = true;
EngineState.Lighting.TransparencyEnable = true;
EngineState.Shading.BumpMappingEnable = true;
InitializeRendering();
}
private static void InitializeRendering()
{
foreach (var type in typeof(IgneelApplication).Assembly.ExportedTypes)
{
if (!type.IsAbstract && !type.IsInterface && type.IsClass && type.GetInterface("IRenderRegistrator") != null)
{
((IRenderRegistrator)Activator.CreateInstance(type)).RegisterInstance();
}
}
}
}
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.
Service.Set<GraphicDeviceFactory>(new IgneelD3D10.GraphicManager10());
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.
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.
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.
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
{
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()
{
var scene = new Scene("Default Scene");
scene.AmbientLight.SkyColor = new Vector3(0.8f);
scene.AmbientLight.GroundColor = new Vector3(0.2f);
Engine.Scene = scene;
}
private void CreateCamera()
{
var scene = Engine.Scene;
float aspect = (float)Engine.Graphics.BackBuffer.Width / (float)Engine.Graphics.BackBuffer.Height;
var controller = new FpController()
{
MoveScale = 10.0f,
RotationScale = 0.5f,
BreakingTime = 0.2f,
UpdateCallback = c => Engine.Mouse.IsButtonPresed(Igneel.Input.MouseButton.Middle) ||
(Engine.Mouse.IsButtonPresed(Igneel.Input.MouseButton.Left) &&
Engine.KeyBoard.IsKeyPressed(Keys.Lalt)),
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()
{
var light = new Light("WhiteLight")
{
Diffuse = new Color3(1,1,1),
Specular = new Color3(0,0,0),
SpotPower = 8,
Enable=true
};
var lightFrame = new FrameLight(light);
Engine.Scene.Create("LightNode", lightFrame, new Vector3(0, 50, 0), new Euler(0, 60, 0));
}
private void CreateTerrain()
{
Texture2D heigthMap = Engine.Graphics.CreateTexture2DFromFile("terrain.png");
HeightField heigthField = new HeightField(heigthMap,32,32);
heigthField.Materials[0].Diffuse =Color3.FromArgb(System.Drawing.Color.DarkGreen.ToArgb());
heigthField.Smoot(5, 4);
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.
private void CreateScene()
{
var scene = new Scene("Default Scene");
scene.AmbientLight.SkyColor = new Vector3(0.8f);
scene.AmbientLight.GroundColor = new Vector3(0.2f);
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:
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
.
private void CreateLight()
{
var light = new Light("WhiteLight")
{
Diffuse = new Color3(1,1,1),
Specular = new Color3(0,0,0),
SpotPower = 8,
Enable=true
};
var lightFrame = new FrameLight(light);
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.
private void CreateTerrain()
{
using (Texture2D heigthMap = Engine.Graphics.CreateTexture2DFromFile("terrain.png"))
{
HeightField heigthField = new HeightField(heigthMap);
heigthField.Materials[0].Diffuse = Color3.FromArgb(System.Drawing.Color.DarkGreen.ToArgb());
heigthField.Smoot(5, 4);
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.
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
Engine.Presenter.Rendering -= Presenter_Rendering;
Engine.StopGameLoop();
base.OnClosing(e);
}
The Height Map
The generated terrain geometry with one material
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.