Diligent Engine: 一个现代化的跨平台低级图形库
本文介绍了 Diligent Engine,一个现代化的跨平台图形 API 抽象库和渲染框架。
免责声明:本文使用了 Diligent Engine 网站上发布的内容。
背景
这篇博文讨论了图形 API,所以最好对 Direct3D11、Direct3D12、OpenGL 或 Vulkan 有所了解。
引言
图形 API 从一小部分基本命令,允许有限地控制早期 3D 加速器的可配置阶段,发展到非常底层的编程接口,暴露了底层图形硬件的几乎所有方面。下一代 API,微软的 Direct3D12 和 Khronos 的 Vulkan 相对较新,刚刚开始获得广泛采用和硬件供应商的支持,而 Direct3D11 和 OpenGL 仍然被认为是行业标准。新的 API 可以提供实质性的性能和功能改进,但可能不受旧平台的支持。面向广泛平台的应用程序必须支持 Direct3D11 和 OpenGL。当与旧范式一起使用时,新的 API 将不会带来任何优势。通过 Direct3D12 实现 Direct3D11 接口来为现有渲染器添加 Direct3D12 支持是完全可能的,但这将没有任何好处。相反,预计将开发利用下一代 API 提供的灵活性的新方法和渲染架构。
至少存在四种 API(Direct3D11、Direct3D12、OpenGL/GLESplus、Vulkan,以及苹果针对 iOS 和 OSX 平台的 Metal),一个跨平台 3D 应用程序可能需要支持它们。为所有 API 编写单独的代码路径对于任何实际应用程序来说显然不是一个选择,因此对跨平台图形抽象层的需求是显而易见的。以下是我认为这种层需要满足的要求列表:
- 轻量级抽象:API 应该尽可能接近底层原生 API,以允许应用程序利用所有可用的底层功能。在许多情况下,这个要求很难实现,因为不同 API 公开的特定功能可能差异很大。
- 低性能开销:抽象层需要从性能角度来看是高效的。如果它引入了相当大的开销,那么使用它就没有意义。
- 便利性:API 需要方便使用。它需要帮助开发人员实现他们的目标,而不是限制他们对图形硬件的控制。
- 多线程:高效并行化工作的能力是 Direct3D12 和 Vulkan 的核心,也是新 API 的主要卖点之一。跨平台层中对多线程的支持是必不可少的。
- 可扩展性:无论 API 设计得多么好,它仍然引入了一定程度的抽象。在某些情况下,实现某些功能最有效的方法是直接使用原生 API。抽象层需要提供对底层原生 API 对象的访问,以便为应用程序提供添加可能缺失功能的方法。
本文介绍了 Diligent Engine,一个轻量级的跨平台图形 API 抽象层,旨在解决这些问题。它的主要目标是利用 Direct3D12、Vulkan、Metal 和 WebGPU 等下一代 API 的优势,同时通过 Direct3D11、OpenGL 和 OpenGLES 提供对旧平台的支持。Diligent Engine 为所有支持的平台公开了通用的 C/C++ 前端,并提供了与底层原生 API 的互操作性。它还支持与 Unity 集成,并被设计为作为独立游戏引擎、Unity 原生插件或任何其他 3D 应用程序中的图形子系统。完整的源代码可在 GitHub 上免费下载和使用。该引擎目前支持以下平台和底层图形 API:
平台 | API |
Win32(Windows 桌面) | Direct3D11、Direct3D12、OpenGL4.2+、Vulkan、WebGPU(通过 Dawn) |
通用 Windows 平台 | Direct3D11、Direct3D12 |
Linux | OpenGL4.2+、Vulkan、WebGPU(通过 Dawn) |
MacOS | OpenGL4.1、Vulkan(通过 MoltenVK)、Metal、WebGPU(通过 Dawn) |
Android | OpenGLES3.0+、Vulkan |
iOS | OpenGLES3.0、Vulkan(通过 MoltenVK)、Metal |
tvOS | Vulkan(通过 MoltenVK)、Metal |
Emscripten | WebGL2.0、WebGPU |
概述
Diligent Engine API 借鉴了 Direct3D11 和 Direct3D12 的一些功能,并引入了新概念。它包含以下主要组件:
渲染设备 (IRenderDevice
接口) 负责创建所有其他对象(纹理、缓冲区、着色器、管道状态等)。
设备上下文 (IDeviceContext
接口) 是渲染命令记录的主要接口。类似于 Direct3D11,有即时上下文和延迟上下文(在 D3D11 实现中直接映射到相应的上下文类型)。即时上下文结合了命令队列和命令列表记录功能。当命令列表中包含足够数量的命令时,它会记录命令并提交命令列表以供执行。延迟上下文旨在仅记录命令列表,这些命令列表可以通过即时上下文提交以供执行。
设计 API 的另一种方式是直接公开命令队列和命令列表。然而,这种方法与 Direct3D11 和 OpenGL 的映射不佳。此外,当已知命令列表由某个延迟上下文从某个线程记录时,某些功能(例如 动态描述符分配)可以更有效地实现。
引擎采用的方法不会限制可扩展性,因为应用程序预计为每个线程创建一个延迟上下文,并且在内部,每个延迟上下文都以无锁方式记录命令列表。同时,这种方法与旧版 API 映射良好。
在当前实现中,只创建了一个使用默认图形命令队列的即时上下文。为了支持多个 GPU 或多种命令队列类型(计算、复制等),为每个队列配置一个即时上下文是很自然的。跨上下文同步工具将是必要的。
交换链 (ISwapChain
接口)。交换链接口表示一系列后端缓冲区,负责在屏幕上显示最终渲染图像。
渲染设备、设备上下文和交换链在引擎初始化期间创建。
资源 (ITexture
和 IBuffer
接口)。有两种类型的资源——纹理和缓冲区。有许多不同的纹理类型(2D 纹理、3D 纹理、纹理数组、立方体贴图等),它们都可以由 ITexture
接口表示。
资源视图 (ITextureView
和 IBufferView
接口)。虽然纹理和缓冲区仅仅是数据容器,但纹理视图和缓冲区视图描述了数据应该如何解释。例如,2D 纹理可以用作渲染命令的渲染目标或作为着色器资源。
管道状态 (IPipelineState
接口)。GPU 管道包含许多可配置阶段(深度模板、光栅化器和混合状态、不同着色器阶段等)。Direct3D11 使用粗粒度对象一次性设置所有阶段参数(例如,一个光栅化器对象包含所有光栅化器属性),而 OpenGL 包含无数函数来细粒度控制每个阶段的每个单独属性。这两种方法都不能很好地映射到现代图形硬件,它们在底层将所有状态组合成一个整体状态。Direct3D12/Vulkan 在 API 中直接公开管道状态对象,Diligent Engine 使用相同的方法。
着色器资源绑定 (IShaderResourceBinding
接口)。着色器是运行在 GPU 上的程序。着色器可以访问各种资源(纹理和缓冲区),设置着色器变量与实际资源之间的对应关系称为资源绑定。资源绑定实现在不同 API 之间差异很大。Diligent Engine 引入了一个名为“着色器资源绑定”的新对象,它包含了某个管道状态中所有着色器所需的所有资源。
创建资源
设备资源由渲染设备创建。两种主要资源类型是缓冲区(表示线性内存)和纹理(使用针对快速过滤优化的内存布局)。图形 API 通常有一个表示线性缓冲区的原生对象。Diligent Engine 使用 IBuffer
接口作为原生缓冲区的抽象。要创建缓冲区,需要填充 BufferDesc
结构体并调用 IRenderDevice::CreateBuffer()
方法,如下例所示:
BufferDesc BuffDesc;
BuffDesc.Name = "Uniform buffer";
BuffDesc.BindFlags = BIND_UNIFORM_BUFFER;
BuffDesc.Usage = USAGE_DYNAMIC;
BuffDesc.uiSizeInBytes = sizeof(ShaderConstants);
BuffDesc.CPUAccessFlags = CPU_ACCESS_WRITE;
m_pDevice->CreateBuffer(BuffDesc, nullptr, &m_pConstantBuffer);
虽然通常只有一个缓冲区对象,但不同的 API 使用截然不同的方法来表示纹理。例如,在 Direct3D11 中,有 1D 纹理、2D 纹理和 3D 纹理对象。在 OpenGL 中,每个纹理维度都有单独的对象,它也可以是纹理数组,也可以是多重采样(例如 GL_TEXTURE_2D_MULTISAMPLE_ARRAY
)。因此,Diligent Engine 在底层可能创建九种不同的 GL 纹理类型。对于 Direct3D12,只有一个资源接口。Diligent Engine 将所有这些细节隐藏在 ITexture
接口中。只有一个 IRenderDevice::CreateTexture()
方法能够创建所有纹理类型。维度、格式、数组大小和所有其他参数由 TextureDesc
结构的成员指定。
TextureDesc TexDesc;
TexDesc.Name = "My texture 2D";
TexDesc.Type = TEXTURE_TYPE_2D;
TexDesc.Width = 1024;
TexDesc.Height = 1024;
TexDesc.Format = TEX_FORMAT_RGBA8_UNORM;
TexDesc.Usage = USAGE_DEFAULT;
TexDesc.BindFlags = BIND_SHADER_RESOURCE | BIND_RENDER_TARGET;
m_pRenderDevice->CreateTexture(TexDesc, nullptr, &m_pTestTex);
如果原生 API 支持多线程资源创建,纹理和缓冲区可以由多个线程同时创建。
下一代 API 允许对资源分配方式进行精细控制。Diligent Engine 目前未公开此功能,但可以通过实现封装资源分配细节的 IResourceAllocator
接口,并将其提供给 CreateBuffer()
或 CreateTexture()
方法来添加此功能。如果提供 null
,则应使用默认分配器。
创建着色器
在早期 API 中,着色器是独立的对象,而在下一代 API 以及 Diligent Engine 中,着色器是管道状态的一部分。编写着色器时最大的挑战是 Direct3D 和 OpenGL/Vulkan 使用不同的着色器语言(而 Apple 在其 Metal
API 中使用另一种语言)。为每个着色器维护两个版本对于实际应用程序来说是不可行的,Diligent Engine 实现了 着色器源代码转换器,允许将用 HLSL 编写的着色器转换为 GLSL。
要创建着色器,请填充 ShaderCreateInfo
结构体:
ShaderCreateInfo ShaderCI;
此结构的 SourceLanguage
成员告诉系统着色器是用哪种语言编写的:
SHADER_SOURCE_LANGUAGE_DEFAULT
- 着色器源代码语言与底层图形 API 匹配:Direct3D11/Direct3D12 模式下为 HLSL,OpenGL、OpenGLES 和 Vulkan 模式下为 GLSL。SHADER_SOURCE_LANGUAGE_HLSL
- 着色器源代码为 HLSL。对于 OpenGL 和 OpenGLES 模式,源代码将被转换为 GLSL。Vulkan 后端直接将 HLSL 代码编译为 SPIRV。SHADER_SOURCE_LANGUAGE_GLSL
- 着色器源代码为 GLSL。目前没有 GLSL 到 HLSL 的转换器,因此此值仅应用于 OpenGL、OpenGLES 和 Vulkan 模式。
有两种方法可以提供着色器源代码。第一种方法是使用 Source
成员。第二种方法是在 FilePath
成员中提供文件路径。由于引擎完全与平台解耦,并且主机文件系统是平台相关的,因此该结构公开了 pShaderSourceStreamFactory
成员,旨在为引擎提供对文件系统的访问。如果提供了 FilePath
,则还必须提供着色器源工厂。如果着色器源代码包含任何 #include
指令,源流工厂也将用于加载这些文件。引擎为每个支持的平台提供了默认实现,在大多数情况下应该足够了。在需要时可以提供自定义实现。
一切准备就绪后,调用 IRenderDevice::CreateShader()
创建 shader
对象:
ShaderCI.Desc.Name = "MyPixelShader";
ShaderCI.FilePath = "MyShaderFile.fx";
ShaderCI.EntryPoint = "MyPixelShader";
ShaderCI.Desc.ShaderType = SHADER_TYPE_PIXEL;
ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
ShaderMacroHelper Macros;
Macros.AddShaderMacro("USE_SHADOWS", 1);
Macros.AddShaderMacro("NUM_SHADOW_SAMPLES", 4);
Macros.Finalize();
ShaderCI.Macros = Macros;
const auto* SearchDirectories = "shaders;shaders\\inc;";
BasicShaderSourceStreamFactory BasicSSSFactory(SearchDirectories);
ShaderCI.pShaderSourceStreamFactory = &BasicSSSFactory;
RefCntAutoPtr<IShader> pShader;
m_pDevice->CreateShader( Attrs, &pShader );
初始化管道状态
如前所述,Diligent Engine 遵循下一代 API 来配置图形/计算管道。一个大的 管道状态对象 (PSO) 包含所有必需的状态(所有着色器阶段、输入布局描述、深度模板、光栅化器和混合状态描述等)。这种方法直接映射到 Direct3D12/Vulkan,但对于旧版 API 也有益,因为它消除了管道配置错误。通过许多单独的调用调整各种 GPU 管道设置时,很容易忘记设置其中一个状态,或者假设阶段已经正确配置而实际上没有。使用管道状态对象有助于避免这些问题,因为所有阶段都是一次性配置的。
要创建管道状态对象,请定义 PipelineStateCreateInfo
结构体实例。
PipelineStateCreateInfo PSOCreateInfo;
PipelineStateDesc& PSODesc = PSOCreateInfo.PSODesc;
PSODesc.Name = "My pipeline state";
PipelineStateDesc
结构的字段提供了关于管道状态的详细信息,例如深度模板、光栅化器和混合状态描述、渲染目标的数量和格式、输入布局格式等。例如,光栅化器状态可以如下描述:
RasterizerStateDesc &RasterizerDesc = PSODesc.GraphicsPipeline.RasterizerDesc;
RasterizerDesc.FillMode = FILL_MODE_SOLID;
RasterizerDesc.CullMode = CULL_MODE_NONE;
RasterizerDesc.FrontCounterClockwise = True;
RasterizerDesc.ScissorEnable = True;
RasterizerDesc.AntialiasedLineEnable = False;
深度模板和混合状态以类似的方式定义。
管道状态对象还包含一个重要的部分是输入布局描述,它定义了顶点着色器(即第一个着色器阶段)的输入应该如何从内存中读取。输入布局可以定义多个顶点流,它们包含不同格式和大小的值。
// Define input layout
InputLayoutDesc &Layout = PSODesc.GraphicsPipeline.InputLayout;
LayoutElement TextLayoutElems[] =
{
LayoutElement( 0, 0, 3, VT_FLOAT32, False ),
LayoutElement( 1, 0, 4, VT_UINT8, True ),
LayoutElement( 2, 0, 2, VT_FLOAT32, False ),
};
Layout.LayoutElements = TextLayoutElems;
Layout.NumElements = _countof( TextLayoutElems );
最后,管道状态定义了图元拓扑。
// Define shader and primitive topology
PSODesc.GraphicsPipeline.PrimitiveTopology = PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
PSODesc.GraphicsPipeline.pVS = pVertexShader;
PSODesc.GraphicsPipeline.pPS = pPixelShader;
管道资源布局
管道状态对象还定义了着色器资源布局(参见下文),它告知引擎应用程序预计如何使用不同的着色器变量。
在着色器中采样纹理时,纹理采样器传统上被指定为在运行时绑定到管道或作为纹理对象本身的一部分设置的独立对象。然而,在大多数情况下,着色器中将使用何种采样器是事先已知的。下一代 API 公开了一种名为“静态采样器”的新型采样器,可以直接在管道状态中初始化。Diligent Engine 公开了此功能:在创建 PSO 时,可以为纹理分配静态采样器。如果分配了静态采样器,它将始终被使用,而不是纹理着色器资源视图中初始化的采样器。要初始化静态采样器,请准备一个 StaticSamplerDesc
结构数组,并初始化 StaticSamplers
和 NumStaticSamplers
成员。静态采样器效率更高,强烈建议尽可能使用它们。
以下是管道资源布局定义的示例:
ShaderResourceVariableDesc ShaderVars[] =
{
{SHADER_TYPE_PIXEL, "g_StaticTexture", SHADER_VARIABLE_TYPE_STATIC},
{SHADER_TYPE_PIXEL, "g_MutableTexture", SHADER_VARIABLE_TYPE_MUTABLE},
{SHADER_TYPE_PIXEL, "g_DynamicTexture", SHADER_VARIABLE_TYPE_DYNAMIC}
};
PSODesc.ResourceLayout.Variables = ShaderVars;
PSODesc.ResourceLayout.NumVariables = _countof(ShaderVars);
PSODesc.ResourceLayout.DefaultVariableType = SHADER_VARIABLE_TYPE_STATIC;
StaticSamplerDesc StaticSampler;
StaticSampler.ShaderStages = SHADER_TYPE_PIXEL;
StaticSampler.Desc.MinFilter = FILTER_TYPE_LINEAR;
StaticSampler.Desc.MagFilter = FILTER_TYPE_LINEAR;
StaticSampler.Desc.MipFilter = FILTER_TYPE_LINEAR;
StaticSampler.TextureName = "g_MutableTexture";
PSODesc.ResourceLayout.NumStaticSamplers = 1;
PSODesc.ResourceLayout.StaticSamplers = &StaticSampler;
当所有必需成员都初始化后,可以通过 IRenderDevice::CreatePipelineState()
方法创建管道状态对象。
m_pDevice->CreatePipelineState(PSOCreateInfo, &m_pPSO);
当 PSO 对象绑定到管道时,引擎会调用所有 API 特定的命令来设置对象指定的所有状态。
绑定着色器资源
Direct3D11 和 OpenGL 利用细粒度资源绑定模型,应用程序将单个缓冲区和纹理绑定到特定的着色器或程序资源绑定槽。Direct3D12 使用一种非常不同的方法,其中资源描述符被分组到表中,应用程序可以通过在命令上下文中设置表来一次绑定表中的所有资源。Diligent Engine 中的资源绑定模型旨在利用这种新方法。它引入了一个名为“着色器资源绑定”的新对象,该对象封装了特定管道状态中所有着色器所需的所有资源绑定。它还根据预期的变化频率对接色器变量进行分类,这有助于引擎在底层将它们分组到表中。
- 静态变量 (
SHADER_RESOURCE_VARIABLE_TYPE_STATIC
) 是预期只设置一次的变量。一旦资源绑定到变量,它们就不能更改。此类变量旨在保存全局常量,例如相机属性或全局光照属性常量缓冲区。 - 可变变量 (
SHADER_RESOURCE_VARIABLE_TYPE_MUTABLE
) 定义了预期以每个材质频率更改的资源。示例可能包括漫反射纹理、法线贴图等。 - 动态变量 (
SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC
) 预期会频繁且随机地更改。
着色器变量类型必须在 PSO 创建期间指定,方法是准备一个 ShaderResourceVariableDesc
结构体数组并初始化 PSODesc.ResourceLayout.Variables
和 PSODesc.ResourceLayout.NumVariables
成员(参见上面的示例)。
一旦资源绑定到变量,静态变量就不能更改。它们直接绑定到 PSO 对象。例如,阴影贴图纹理在创建后预期不会更改,因此可以直接绑定到着色器:
PSO->GetStaticShaderVariable( SHADER_TYPE_PIXEL, "g_tex2DShadowMap" )->Set( pShadowMapSRV );
可变和动态变量通过新的着色器资源绑定对象 (SRB) 绑定,该对象由管道状态 (IPipelineState::CreateShaderResourceBinding()
) 创建。
m_pPSO->CreateShaderResourceBinding(&m_pSRB, true);
SRB 与具有相同资源布局的任何管道状态兼容。SRB 对象继承 PSO 的所有静态绑定,但不能更改它们。
可变资源只能针对着色器资源绑定的每个实例设置一次。此类资源旨在定义特定的材质属性。例如,特定材质的漫反射纹理在材质定义后预期不会更改,并且可以在 SRB 对象创建后立即设置。
m_pSRB->GetVariable(SHADER_TYPE_PIXEL, "tex2DDiffuse")->Set(pDiffuseTexSRV);
在某些情况下,每次调用绘图命令时,都需要将新资源绑定到变量。此类变量应标记为动态,这将允许通过同一 SRB 对象多次设置它们。
m_pSRB->GetVariable(SHADER_TYPE_VERTEX, "cbRandomAttribs")->Set(pRandomAttrsCB);
在底层,当创建 SRB 对象时,引擎会为静态和可变资源预先分配描述符表。动态资源的空间在运行时动态分配。因此,静态和可变资源效率更高,应尽可能使用。
如您所见,Diligent Engine 不会公开资源如何绑定到着色器变量的底层细节。一个原因是这些细节对于不同的 API 来说差异很大。另一个原因是使用底层绑定方法极易出错:很容易忘记绑定某些资源,或者绑定不正确的资源,例如将缓冲区绑定到实际上是纹理的变量,尤其是在着色器开发过程中,当一切变化很快时。Diligent Engine 转而依赖着色器反射系统来自动查询所有着色器变量的列表。根据上述三种类型对变量进行分组,使引擎能够创建优化的布局,并承担将资源与 API 特定资源位置、寄存器或描述符在表中匹配的繁重工作。
这篇文章提供了关于 Diligent Engine 中资源绑定模型的更多细节。
设置管道状态并提交着色器资源
在调用任何绘制或计算命令之前,需要将管道状态绑定到上下文。
m_pContext->SetPipelineState(m_pPSO);
在底层,引擎在命令列表中设置内部 PSO 对象,或者调用所有必需的原生 API 函数来正确配置所有管道阶段。
下一步是将所有必需的着色器资源绑定到 GPU 管道,这通过 IDeviceContext::CommitShaderResources()
方法完成。
m_pContext->CommitShaderResources(m_pSRB, COMMIT_SHADER_RESOURCES_FLAG_TRANSITION_RESOURCES);
该方法接受一个指向着色器资源绑定对象的指针,并使该对象持有的所有资源对着色器可用。对于 D3D12,这只需在命令列表中设置适当的描述符表。对于旧版 API,这通常需要单独设置所有资源。
下一代 API 要求应用程序跟踪每个资源的状态,并明确告知系统所有状态转换。例如,如果纹理之前用作渲染目标,而下一个绘图命令将它用作着色器资源,则需要执行一个转换屏障。Diligent Engine 承担了状态跟踪的繁重工作。当调用 CommitShaderResources()
方法并带有 COMMIT_SHADER_RESOURCES_FLAG_TRANSITION_RESOURCES
标志时,引擎会同时提交并将资源转换到正确状态。请注意,转换资源会引入一些开销。引擎会跟踪每个资源的状态,如果状态已经正确,它将不会发出屏障。但是检查资源状态是一种开销,有时可以避免。引擎提供了 IDeviceContext::TransitionShaderResources()
方法,该方法仅转换资源。
m_pContext->TransitionShaderResources(m_pPSO, m_pSRB);
在某些情况下,一次性转换资源,然后只提交它们效率更高。
调用绘制命令
最后一步是设置不属于 PSO 的状态,例如渲染目标、顶点缓冲区和索引缓冲区。Diligent Engine 使用 Direct3D11 风格的 API,该 API 在底层转换为其他原生 API 调用。
ITextureView *pRTVs[] = {m_pRTV};
m_pContext->SetRenderTargets(_countof( pRTVs ), pRTVs, m_pDSV,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
// Clear render target and depth buffer
const float zero[4] = {0, 0, 0, 0};
m_pContext->ClearRenderTarget(nullptr, zero,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pContext->ClearDepthStencil(nullptr, CLEAR_DEPTH_FLAG, 1.f,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
// Set vertex and index buffers
IBuffer *buffer[] = {m_pVertexBuffer};
Uint32 offsets[] = {0};
m_pContext->SetVertexBuffers(0, 1, buffer, offsets, SET_VERTEX_BUFFERS_FLAG_RESET,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pContext->SetIndexBuffer(m_pIndexBuffer, 0,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
请注意,所有可能需要执行资源状态转换的方法都将 RESOURCE_STATE_TRANSITION_MODE
enum
作为参数。该 enum
定义了以下模式:
RESOURCE_STATE_TRANSITION_MODE_NONE
- 不执行状态转换。RESOURCE_STATE_TRANSITION_MODE_TRANSITION
- 将所有资源转换为命令所需的状态。RESOURCE_STATE_TRANSITION_MODE_VERIFY
- 不转换,但验证状态是否正确。
不同的原生 API 使用不同的函数集来执行绘制命令,具体取决于命令的细节(命令是否带索引、是否实例化、两者兼有、源缓冲区中的偏移量等)。例如,Direct3D11 中有 5 个绘制命令,OpenGL 中有超过 9 个命令,像 glDrawElementsInstancedBaseVertexBaseInstance
这样的命令并不少见。Diligent Engine 使用单个 IDeviceContext::Draw()
方法隐藏了所有细节,该方法以 DrawAttribs
结构体作为参数。该结构体成员定义了执行命令所需的所有属性(图元拓扑、顶点或索引数量、绘制调用是否带索引、绘制调用是否实例化、绘制调用是否间接等)。例如:
DrawAttribs attrs;
attrs.IsIndexed = true;
attrs.IndexType = VT_UINT16;
attrs.NumIndices = 36;
attrs.Flags = DRAW_FLAG_VERIFY_STATES;
pContext->Draw(attrs);
DRAW_FLAG_VERIFY_STATES
标志指示引擎验证绘图命令使用的顶点和索引缓冲区是否处于正确状态。除非在多线程环境中存在竞态条件,否则建议始终使用此标志。
对于计算命令,有一个 IDeviceContext::DispatchCompute()
方法,它接受一个定义计算网格维度的 DispatchComputeAttribs
结构体。
源代码
完整的引擎源代码可在 GitHub 上免费下载和使用。该存储库包含教程、示例应用程序、小行星性能基准测试以及一个使用 DiligentEngine
作为原生插件的 Unity 示例项目。
特点
- 跨平台
- 所有支持的平台和渲染后端使用完全相同的客户端代码
- 无
#if defined(_WIN32)
...#elif defined(LINUX)
...#elif defined(ANDROID)
... - 无
#if defined(D3D11)
...#elif defined(D3D12)
...#elif defined(OPENGL)
...
- 无
- 完全相同的 HLSL 着色器在所有平台和所有后端上运行
- 所有支持的平台和渲染后端使用完全相同的客户端代码
- 模块化设计
- 组件在逻辑和物理上清晰分离,可按需使用。
- 只取项目所需(不想在代码库中保留示例和教程?只需移除 Samples 子模块。只需要核心功能?只使用 Core 子模块)
- 没有 15000 行代码的文件
- 清晰的基于对象的接口
- 无全局状态
- 主要图形功能
- 旨在利用下一代渲染 API 的自动着色器资源绑定
- 多线程命令缓冲区生成
- 使用 D3D12 后端,50,000 次绘制调用,300 fps
- 描述符、内存和资源状态管理
- 现代 C++ 功能使代码快速可靠
教程
以下是目前已实现的教程列表。
教程 | 描述 | |
![]() | 这个基本教程展示了如何使用 Diligent Engine API 渲染一个简单的三角形。 | |
![]() | 本教程演示如何渲染实际的 3D 对象——一个立方体。它展示了如何从文件加载着色器,以及如何创建和使用顶点、索引和统一缓冲区。 | |
![]() | 本教程演示了如何将纹理应用于 3D 对象。它展示了如何从文件加载纹理,创建着色器资源绑定对象以及如何在着色器中采样纹理。 | |
03_Texturing-C | ![]() | 本教程与 Tutorial03 相同,但使用 C API 实现。 |
03_Texturing-DotNet | ![]() | 本教程演示了如何在 .NET 应用程序中使用 Diligent Engine API。 |
![]() | 本教程展示了如何使用实例化来渲染一个对象的多个副本,每个副本使用唯一的变换矩阵。 | |
![]() | 本教程演示了如何将实例化与纹理数组结合使用,为每个实例使用唯一的纹理。 | |
![]() | 本教程展示了如何从多个线程并行生成命令列表。 | |
07_GeometryShader | ![]() | 本教程展示了如何使用几何着色器渲染平滑的线框。 |
08_Tessellation | ![]() | 本教程展示了如何使用硬件曲面细分来实现简单的自适应地形渲染算法。 |
![]() | 本教程展示了如何渲染多个 2D 四边形,频繁切换纹理和混合模式。 | |
![]() | 本教程展示了使用 MAP_FLAG_DISCARD 和 MAP_FLAG_DO_NOT_SYNCHRONIZE 标志的动态缓冲区映射策略,以高效地将不同数量的数据流式传输到 GPU。 | |
![]() | 本教程演示了在 Diligent Engine 中更新缓冲区和纹理的不同方法,并解释了与每种方法相关的重要内部细节和性能影响。 | |
![]() | 本教程演示了如何将 3D 纹理立方体渲染到离屏渲染目标,并进行简单的失真后处理效果。 | |
![]() | 本教程演示了如何使用阴影贴图渲染基本阴影。 | |
![]() | 本教程展示了如何使用计算着色器实现简单的粒子模拟系统。 | |
| ![]() | 本教程演示了如何使用 Diligent Engine 渲染到多个窗口。 |
![]() | 本教程展示了如何实现无绑定资源,该技术利用下一代 API 启用的动态着色器资源索引功能,显著提高渲染性能。 | |
![]() | 本教程演示了如何使用多重采样抗锯齿(MSAA)使几何边缘看起来更平滑、更稳定。 | |
![]() | 本教程演示了如何使用查询来检索关于 GPU 操作的各种信息,例如渲染的图元数量、命令处理持续时间等。 | |
![]() | 本教程演示了如何使用渲染通道 API 实现简单的延迟着色。 | |
Tutorial20_MeshShader | ![]() | 本教程演示了如何使用现代 GPU 的新型可编程阶段——放大着色器和网格着色器,在 GPU 上实现视锥体剔除和 LOD 计算。 |
Tutorial21_RayTracing | ![]() | 本教程演示了 Diligent Engine 中的光线追踪 API 如何用于模拟场景中基于物理的光传输,以渲染柔和阴影、多次反弹反射和折射以及色散。 |
Tutorial22_HybridRendering | ![]() | 本教程演示了如何实现一个简单的混合渲染器,它将光栅化与光线追踪相结合。 |
Tutorial23_CommandQueues | ![]() | 本教程演示了如何使用多个命令队列并行执行渲染以及复制和计算操作。 |
Tutoria24_VRS | ![]() | 本教程演示了如何使用可变速率着色来减少像素着色负载。 |
Tutoria25_StatePackager | ![]() | 本教程展示了如何使用渲染状态打包器离线工具创建和存档管道状态 |
![]() | 本教程扩展了上一个教程中实现的路径追踪技术,并演示了如何使用渲染状态缓存来保存运行时创建的管道状态,并在应用程序启动时加载它们。 | |
![]() | 本教程演示了如何使用 DiligentFX 模块中的后处理效果(屏幕空间反射、环境光遮蔽、景深等)。 |
示例
大气光散射示例实现了基于物理的大气光散射模型,并演示了 Diligent Engine 如何用于完成各种渲染任务:从文件加载纹理、使用复杂着色器、渲染到纹理、使用计算着色器和无序访问视图等。
GLFW 迷你游戏
GLFW 迷你游戏展示了如何将 Diligent Engine 与流行的窗口和输入处理库 GLFW 结合使用。
GLTF 查看器
GLTF 查看器展示了如何使用 GLTF 资源加载器和 基于物理的渲染器来加载和渲染 GLTF 模型。
阴影
Dear Imgui 演示
该示例演示了引擎与 Dear Imgui UI 库的集成。
Nuklear 演示
本示例演示了引擎与 nuklear UI 库的集成。
Hello AR
此示例演示了如何在基本的 Android AR 应用程序中使用 Diligent Engine。
小行星基准测试
该引擎还包括基于 Intel 开发的此演示的 小行星性能基准测试。它渲染 50,000 个独特的纹理小行星,并比较 D3D11、D3D12 和 Vulkan 实现的性能。每个小行星都是 1000 个独特的网格和 10 个独特纹理之一的组合。
Unity 集成示例
该存储库还包含一个 示例项目,展示了如何将 Diligent Engine 与 Unity 集成。
未来工作
该引擎正在积极开发中。目前它支持所有主要平台(Windows 桌面、通用 Windows、Linux、MacOS、Android 和 iOS)。Direct3D11、Direct3D12、OpenGL/GLES 和 Vulkan 后端现已完成。正在计划开发一些高级功能,例如光线追踪支持、多个命令队列、渲染通道。
相关文章
- 管理 Direct3D12 资源的生命周期
- 管理 Direct3D12 中的描述符堆
- 使用 Direct3D12 实现动态资源
- Diligent Engine 中 GPU 资源更新策略的比较
- Unity 图形模拟器,用于原生插件开发
- 更明确的 Diligent Engine:掌握 Direct3D12 和 Vulkan 中的状态管理
版本历史
- 2024年9月1日 - Diligent 现已支持 WebGPU,并添加了在浏览器中运行示例的链接
- 2024年5月5日 - 添加教程 27 - 后期处理
- 2023年8月9日 - 添加了 Dot Net 版本的教程 03
- 2022年12月4日 - 添加了高级路径追踪/渲染状态缓存教程
- 2022年8月29日 - 添加了路径追踪/状态打包器教程
- 2021年10月11日 - 添加了可变速率着色教程
- 2021年8月16日 - 添加了命令队列教程
- 2021年6月6日 - 添加了混合渲染教程
- 2021年1月3日 - 添加了光线追踪教程
- 2020年5月14日 - 添加了 Hello AR 示例
- 2020年2月4日 - 启用了 C API 并添加了教程 03 的 C 版本
- 2020年1月14日 - 添加了教程 18
- 2019年11月11日 - 添加了教程 17 和 Nuklear 演示
- 2019年10月13日 - 添加了教程 14 和 15 以及 Dear Imgui 演示。
- 2019年7月24日 - 添加了阴影示例。
- 2019年5月7日 - 添加了 GLTF Viewer 示例和教程 12 - 渲染目标。
- 2019年4月3日 - Diligent Engine 在 iOS 上支持 Vulkan;将静态图片替换为动画。
- 2019年3月9日 - 更新了文章以匹配 v2.4.b 版本