DirectX-DeferredContext与multipass rendering

DirectX11中有两种渲染方式,Immediate Rendering与Deferred Rendering。这里的延迟渲染与 实时渲染中的延迟渲染不是同一种概念,DX中的Deferred Context是在图像api层面绘制指令DrawCall的延迟,而实时渲染中的Deferred Rendering是利用GBuffer对渲染对象进行延迟着色。Deferred Context可以用来实现多线程渲染,最大程度上利用CPU与GPU的性能。

多线程渲染

多线程渲染在我看来强调的是CPU层面的多线程,而GPU的渲染指令都是顺序执行的,而渲染本身由于GPU并行计算的架构始终是多线程的。在游戏运行的过程中,每一帧都有大量繁重的计算,CPU端的逻辑预算和GPU端的绘制。而游戏运行的帧率取决于最慢的一方。对于渲染的流程来说,CPU将绘制的图元数据传递到GPU然后GPU收到绘制指令开始绘制。这一流程是有先后顺序的。每个CPU调用绘制或者是设置渲染状态的命令都需要等待GPU完成后才能执行下一个命令。

CPU端的多线程在于可以开启多个线程同时创建好一系列的指令并且做好CPU端的所有准备工作。然后再将这些指令列表顺序提交给GPU。这样就可以减少调用图像指令的同步时间,以减少CPU的时间消耗。

D3D11中可以创建多个DeferredContext, 可以对DeferredContext调用所有的渲染管线状态设置以及绘制的命令。然后将其储存在一个Command List中。 最后在执行真正的绘制时使用ImmediateContext执行CommandList。就可以一次性把一批次指令提交GPU。这里需要注意的是ImmediateContext本身不是线程安全的。

类似于Unity中的CommandBuffer可以将一系列绘制命令提交给GPU执行,但是在Unity中CommandBuffer只是在上层应用层提供渲染管线扩展的能力。多线程渲染的另一个用处是可以将所有的绘制分割成不同的渲染任务,每个任务在各自命令列表中进行调用,维护独立的渲染管线状态。避免上一个绘制指令的管线状态影响到后续的绘制。

Multipass渲染

Multipass(多通道渲染,不知道是不是比较准确的翻译)渲染是一个更加上层的概念。在图像API层面并没有多通道渲染的概念。OpenGL D3D等等底层API没有相应的对象和接口,就像我们在游戏引擎里面看到的材质Material,也是应用层抽象出来的概念。

在Unity ShaderLab的语法中我们可以这样定义一个Shader。Shader中支持MultiPass,几乎所有的游戏引擎都支持MultiPass。

1
2
3
4
5
6
7
8
9
10
Shader{
SubShader{
Pass{
//Pass 1
}
Pass{
//Pass
}
}
}

那么为什么需要多通道渲染呢?之前和老大讨论之后,有了一些更深的理解。

多通道渲染的实现

多通道渲染是对图像API的组织结构,例如在DX11中渲染一个对象,通常的做法如下

1
2
3
4
5
6
7
deviceContext->SetVertexBuffer();
deviceContext->SetIndicesBuffer();
deviceContext->SetVertexShader();
deviceContext->SetPixelShader();
deviceContext->SetPipelineState();

deviceContext->DrawIndices(); //draw call

先设置绘制的buffer数据然后进行设置shader最后调用draw call。而MultiPassShader的流程是

1
2
3
4
5
6
7
8
9
10
11
12
deviceContext->SetVertexBuffer();
deviceContext->SetIndicesBuffer();

deviceContext->SetVertexShader(); //Pass 1
deviceContext->SetPixelShader(); //Pass 1
deviceContext->SetPipelineState(); //Pass 1 State
deviceContext->DrawIndices(); //Pass 1 draw call

deviceContext->SetVertexShader(); //Pass 2
deviceContext->SetPixelShader(); //Pass 2
deviceContext->SetPipelineState(); //Pass 2 State
deviceContext->DrawIndices(); //Pass 2 draw call

每个Pass可以认为是一个多层材质的一层,在渲染时共用模型数据。大部分情况下每个Pass可以在DeferredContext中绘制最后依次将CommandList进行绘制。(在调用DrawCall的时候Pass之间是一定是有先后顺序的,在Unity的ShaderLab中使用的是Queue这个Tag对Pass的调用进行排序。)

之前思考过上每个Pass可以设置不同的RenderTarget,如果两个Pass之间没有先后的依赖关系,GPU应该可以同时进行绘制。但是这层的优惠应该是在驱动层做的优化,GPU在进行一个Shader绘制的时候是不是会使用掉所有的Streaming Multiprocessor,多个Pass能不能并行计算。可能需要了解更多GPU架构方面的知识。

为什么需要多通道渲染

上面描述了多通道渲染大概的实现方式,通常情况下的着色器效果其实在一个Pass里就可以完成。但是很多复杂的效果可能需要对渲染对象进行多次的渲染最后才能计算出最终的颜色。例如一些光照计算,阴影贴图实现阴影等等效果。在多通道渲染的每个Pass之间是可以使用Blend混合,Stencil模板以及自定义深度测试和计算。这样在不同Pass之间就可以依次把每个通道计算的颜色深度和模板进行了混合。不需要耗费额外的DrawCall来把多次渲染的结果进行一个Merge(类似Unity中的Graphics.Bilt操作)

多通道渲染的设计方式使得多个DrawCall绘制结果的混合更加简便,实际上并不会在图像API层面提升性能和运算量。