使用 Igneel 引擎渲染高度图





5.00/5 (2投票s)
描述了使用 Igneel 图形引擎渲染高度图的基本步骤
引言
Igneel 引擎是一个面向 .NET 运行时的软件开发平台,用于开发实时 3D 图形应用程序。该平台对于编写游戏、模拟或交互式 3D 可视化软件特别有用。Igneel 引擎隐藏了开发此类应用程序所涉及的所有复杂性,让您能够轻松管理 3D 场景,而无需担心繁重的数学计算。此外,由于其物理引擎,您可以为 3D 对象添加逼真的物理行为。此外,物理引擎是使用 NVIDIA PhysX 实现的,因此您可以利用 GPU 加速。Igneel 引擎的另一个很酷的功能是其动画引擎,它允许您执行复杂的动画序列,包括从属性到带有不同插值方法的骨骼关键帧的多种动画类型。
该平台旨在可定制并能够随着时间的推移进行扩展,通过提供用户交互的抽象接口或契约。这些抽象是通过原生代码和托管代码的混合来实现的,以达到实时应用程序所需的高性能。更重要的是,这些抽象可以使用不同的原生 API 来实现,从而允许用户的应用程序在不同的环境中运行,而无需更改底层代码。例如,用户应用程序可以在带有 DirectX 的 Windows 环境中运行,或者在带有 .NET Core 和 OpenGL 的 Linux 环境中运行。
Igneel 引擎中的所有渲染都使用着色器完成,通过使用动态类型生成访问 GPU 内存中的着色器变量,从而实现独特的机制,如《使用 C# 和 Igneel.Graphics 在 .NET 中渲染 3D 图形》中所述。需要指出的是,与大多数游戏引擎将着色器附加到材质不同,Igneel 引擎不遵循该模式,而是将着色器分组到 Effects
中并附加到定义绘制组件逻辑的 Render
。此外,这些渲染器可以在运行时轻松替换或组合,以实现有趣的视觉效果。因此,具有相同视觉材质的对象可以通过使用不同的效果以多种方式渲染。
Igneel 引擎架构图
高度图
高度图是一种最常用于渲染地形的技术。它包括一张灰度图像,其中每个像素定义一个海拔或高度值。然后,您通过从图像中采样和缩放 y 坐标(假设 y 轴指向上方 ↑)将图像映射到 3D 网格。此外,根据图像像素格式,您可以存储的高度范围也不同。采样的高度颜色被缩放到 [0-1],因此之后您可以通过对 HeightField 场景节点应用缩放变换将其缩放到所需的高度。
背景
作为背景,最好看一下我关于《Igneel.Graphics》的文章,它通过一个示例描述了如何使用 Igneel 引擎的低级渲染 API。本文中用于渲染高度场的示例使用了 Igneel 引擎的高级渲染模块,该模块由 Tehniques
、Renders
、Effects
、Materials
和 Bindings
组成。
使用代码
本文介绍的渲染高度场的示例托管在 WPF 应用程序中。WPF 有自己的内部渲染循环,也使用 Direct3D9 渲染 3D 内容。相反,Igneel.Graphics 的实现是基于 Direct3D10 的,因此可以使用 Canvas3D WPF 控件将 Igneel 引擎与应用程序集成。另一方面,您需要处理引擎初始化例程,这将在 Bootstrapper 类描述中介绍。
但首先,创建一个简单的 WPF 应用程序。列表 1 中显示的 XAML 标记从 Igneel.Windows.Wpf
导入并声明了 Canvas3D
,所有渲染都在此控件中完成。
<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>
列表 1 主窗口 XAML
引擎初始化
Boostrapper 是包含所有引擎初始化逻辑的类。Init 方法在 MainWindows 的 Load 事件处理程序中调用。它接收 MainWindows 和 Canvas3D 实例。参见下面的列表 2。
/// <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
}
}
列表 2:引擎初始化
在 Init 方法中,我们初始化图形和输入 API。如果需要物理模拟,也必须在此处初始化。初始化引擎的第一步是注册 API 实现,这在列表 3 中显示的代码段中完成。
//Register the Graphics API implementation
Service.Set<GraphicDeviceFactory>(new IgneelD3D10.GraphicManager10());
//Register the Input API implementation
Service.Set<Igneel.Input.InputManager>(new IgneelDirectInput.DInputManager());
列表 3 使用 IOC 容器注册 API 实现
之后,您必须注册构成着色器存储库的路径。着色器存储库是您存储着色器代码的位置。您还必须指定核心引擎着色器位置,这些着色器已预编译和优化。此外,您可以通过在 _shaderRepositoryDir
数组中添加新行来替换它或指定新的实现。
//Initialize shader respository
ShaderRepository.SetupD3D10_SM40(_shaderRepositoryDir);
然后通过调用 Engine.Initialize(IInputContext inputContext, GraphicDeviceDesc graphicDeviceDesc)
初始化引擎。该调用需要两个参数,第一个是用户输入上下文,即整个窗口,第二个包含用于创建 GraphicDevice 的值,并使用 Canvas3D 窗口句柄创建背面缓冲区和交换链。
另一个重要的初始化是引擎的 GraphicPresenter
,这个对象非常重要,它将在渲染完成之前和之后设置所有适当的参数。不幸的是,这里没有空间涵盖 Igneel 引擎的所有方面,但 GraphicPresenter
是一个允许您自定义渲染工作流的功能,例如,渲染到窗口客户端区域的多个屏幕或部分。
//Set the Engine presenter to the Canvas3D presenter
//this instruct the engine to render the contents using the Canvas3D GraphicPresenter
Engine.Presenter = mainCanvas.CreateDafultPresenter();
初始化过程的最后一步是为每个受支持的 Technique 注册所有 Renders
。这在 InitializeRendering
方法中完成。此外,您还可以注册自己的组件的 Renders
。
创建场景
在 Igneel 引擎中,Scene
是所有视觉组件的主要容器,包括动画、相机、灯光、网格、骨骼和高度场等。此外,所有视觉组件都必须附加到 Frame
,Frame
通过定义其空间属性(如缩放、平移和旋转)将组件定位在 Scene
中。此外,这些属性是相对于父 Frame
的,从而构成了 Frame 的场景层次结构,最顶层称为根。因此,转换根将转换整个场景。
列表 4 将展示 MainWindows 类,在其中加载事件处理程序内部,我们初始化引擎,然后创建场景、相机、地形和灯光。此外,还挂钩了 Engine.Presenter.Rendering
事件,以便在循环步骤或帧渲染后可以执行操作。在这种情况下,我们显示 FPS 或每秒帧数比率。
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);
}
}
}
通过调用 Bootstraper.Init(this, Canvas)
初始化引擎后,在 CreateScene
方法中创建场景并分配名称。场景环境光颜色也在该方法中定义,并设置了天空和地面两种颜色,这些颜色用于半球形照明,这是默认的环境照明技术。之后,通过设置 Engine.Scene
属性,将新创建的场景 स्थापित 为当前的渲染场景。因此,一次只能有一个场景处于活动状态。
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;
}
然后,为了看到任何东西,您需要创建一个 Camera
。Camera
在 CreateCamera
方法中创建。此外,还创建了一个 FpController
,它将以第一人称相机风格控制相机的行为和对用户输入的响应。因此,为了在帧更新期间调用,控制器将被添加到场景的动态集合中,这是一个包装的 Dynamic
对象。
将调用 FpController.UpdateCallback
委托来决定控制器是否必须更新相机节点的位置和方向。
相机节点在以下代码行中创建
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())
在上一节中,创建了一个 Camera
组件,透视投影为 60 度,用于视野。然后通过调用 Scene.Create
传递相机、位置和方向,将相机附加到 Frame
。该 Frame
被指定为场景根的直接子级。
灯光在 CreateLight
方法中以相同的模式创建,首先创建 Light
组件,然后创建 Frame。但在此例中,实例化了一个 FrameLight
,它充当 Light
和 Frame
之间的适配器,如下所示。这种设计允许在 Scene
中的多个位置和方向重复使用 Light
。
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));
}
现在只需根据高度图创建地形即可完成我们的场景。
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));
}
}
上述方法创建了由 HeightField
表示的地形。它可以通过调用其构造函数之一,传入包含高度值的灰度纹理来创建。支持多种纹理格式,例如:
- R8G8B8A8_UNORM
- R16G16B16A16_UNORM
- R16G16B16A16_FLOAT
- R32G32B32A32_FLOAT
- A8_UNORM
- R16_FLOAT
- R32_FLOAT
HeightField
定义了 Materials
属性,该属性获取或设置 LayeredMaterial
数组。默认情况下,它在索引 0 处包含一个 LayeredMaterial
。HeightField
可以划分为多个部分,每个部分分配给一个单独的材质。此外,材质定义了漫反射、高光和其他颜色属性。LayeredMaterial
还定义了一个长度为 4 的漫反射、法线和高光纹理数组,这些纹理代表可以应用于地形部分的纹理层,使用 BlendFactors
纹理,其中每个颜色通道代表相应纹理索引的贡献值。此外,一个不错的选择是通过调用 HeightField.Smoot(int kernelSize, int times = 1, float stdDev = 3)
应用平滑滤镜,该方法应用高斯滤镜,指定核大小、通过次数和标准差。建议的核大小是 5,但这取决于您希望地形平滑的程度。
最后,地形被分配到其场景节点,然后平移到中心,并在 X 和 Y 轴上缩放 1000 个单位,在 Y 轴上缩放 100 个单位。进行此平移是因为地形是在 [0,0,0]-[1,1,1] 坐标范围内创建的,因此最大高度值映射到 1,最小高度值映射到 0。这样,您可以将地形缩放到任何您想要的维度。
在类的末尾,在 OnClosing
方法中,我们简单地分离 Presenter_Rendering
事件处理程序并停止游戏循环。这样应用程序就可以优雅地关闭。
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
Engine.Presenter.Rendering -= Presenter_Rendering;
Engine.StopGameLoop();
base.OnClosing(e);
}
高度图
生成的带有一种材质的地形几何体
关注点
游戏引擎是庞大而复杂的软件系统,构建 Igneel 引擎是我热情拥抱的挑战。我学到了很多关于游戏引擎架构、图形 API 和物理的知识。总结来说,Igneel 引擎由多个模块或子系统无缝集成在一个架构中,有趣的是该平台如何通过 Techniques
和 Renders
的组合使用堆栈来实现阴影映射、HDR、延迟渲染等技术,这些只是其中一部分。此外,Effects
的类型安全定义以及使用 IRenderBinding
将组件绑定到 Effects
的方式也令人瞩目。我打算写更多关于 Igneel 引擎应用程序和组件的文章,此外,Igneel 引擎代码可在 github 上获取,欢迎大家贡献。
此外,需要注意的是,为了运行示例,您必须安装 DirectX SDK https://www.microsoft.com/en-us/download/details.aspx?id=6812 然后找到安装文件夹中的 Redist 文件夹并运行 DXSETUP.exe。这将安装引擎所需的 Direct3D10 运行时库。