65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Igneel 引擎渲染高度图

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年3月5日

CPOL

9分钟阅读

viewsIcon

15229

downloadIcon

339

描述了使用 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 引擎的高级渲染模块,该模块由 TehniquesRendersEffectsMaterialsBindings 组成。

使用代码

本文介绍的渲染高度场的示例托管在 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 是所有视觉组件的主要容器,包括动画、相机、灯光、网格、骨骼和高度场等。此外,所有视觉组件都必须附加到 FrameFrame 通过定义其空间属性(如缩放、平移和旋转)将组件定位在 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;            
    }

然后,为了看到任何东西,您需要创建一个 CameraCameraCreateCamera 方法中创建。此外,还创建了一个 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,它充当 LightFrame 之间的适配器,如下所示。这种设计允许在 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 处包含一个 LayeredMaterialHeightField 可以划分为多个部分,每个部分分配给一个单独的材质。此外,材质定义了漫反射、高光和其他颜色属性。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 引擎由多个模块或子系统无缝集成在一个架构中,有趣的是该平台如何通过 TechniquesRenders 的组合使用堆栈来实现阴影映射、HDR、延迟渲染等技术,这些只是其中一部分。此外,Effects 的类型安全定义以及使用 IRenderBinding 将组件绑定到 Effects 的方式也令人瞩目。我打算写更多关于 Igneel 引擎应用程序和组件的文章,此外,Igneel 引擎代码可在 github 上获取,欢迎大家贡献。

此外,需要注意的是,为了运行示例,您必须安装 DirectX SDK https://www.microsoft.com/en-us/download/details.aspx?id=6812  然后找到安装文件夹中的 Redist 文件夹并运行 DXSETUP.exe。这将安装引擎所需的 Direct3D10 运行时库。

 

© . All rights reserved.