Deferred Shading

Deferred Shading

到目前为止,本书中所使用的渲染方法都是forward rendering,本质上来说是指独立渲染场景中的每一个objects,对于每一个object都要使用lights,textures以及shadows计算最终的pixels。另外,我们还探讨了single-pass rendering(用于一个object的所有光照都在一个shader中进行计算)和multipass rendering(通过执行多次绘制操作组合成object的最终pixel值)的概念。在任何使用forward rendering的系统中,我们都会受限于影响object的光源数量。影响object的光源越多,系统的计算量越大,那么应用程序的运行速度就越慢。但是另一种渲染方法deferred shading,支持在场景中使用大量的光源而不会显著降低性能。

注意
Deferred shading也称为deferred rendering。只不过deferred rendering在图表学术语中已经被使用了。特别是在Microsoft的产品中,该术语表示buffering commands,用于在其他时间段进行“回放”(意思是把渲染指令缓冲起来,等到需要的时候再进行渲染,因此可以称为延迟渲染)。这种延迟渲染的方法与本节所到讨论的deferred shading完全不同。

在使用deferred shading的系统中,首先渲染场景中的所有objects,而不包含光照。而且这些objects并不是被渲染到back buffer中,而是使用multiple render targets(MRT)渲染到一些off-screen buffers中(离线的bufffers)。在这些render targets中存储了场景的geometry数据,包括object的position,normal,color(一般是从一个diffuse color texture中采样得到),specular power和specular intensity。前面已经讲过render targets其实就是2D textures,但是在这里这些geometry buffers(也称为G-buffer)并不是存储color值,而是存储影响objects的lighting passs数据用于计算场景中所有pixels的最终color值。

其中lighting pass类似于在第18章“Post Processing”中所讲的post-processing effect。对于场景中的每一种光源,都是从geometry buffers中采样数据,并计算被该光源影响的每一个pixel的直接和间接光照值。最后使用场景中所有光源产生的影响组合成最终的图像数据,并映射到一个full-screen quad中。

由于光照是在screen space中进行计算,渲染每一帧的计算成本主要由光源的数量以及受光源影响的pixels数量决定。Geometry数据是每一帧渲染一次,并且只对真正受到光源影响的geometry才会执行高成本的光照运算。除了具有高性能的优点之外,deferred shading还能简单一个渲染引擎的架构设计,因为使用这种方法场景中的所有objects可以共用一个shader。经常切换shaders会产生较大的性能开销,因此使用forward rendering方法的渲染引擎通常会批量处理使用相同materials的objects。如果不使用deferred shading系统,就需要一个更复杂的引擎架构。

也就是说,使用deferred shading只能共用一种materials。此外,deferred shading还有很多不足之处,这时仍然需要使用forward rendering。第一,geometry buffers需要占用大量的内存。第二,deferred shading无法处理transparency(透明),透明效果通常由一个排序步骤单独处理(from back to front,也就是距离camera的距离由远到近),并对透明的objects使用forward rendering pass。另外一个不足的地方是,把geometry和lighting阶段分开计算,就无法使用硬件的anti-aliasing功能,因此需要使用一个额外的post-processing technique处理边缘的光滑度。

最后在一个现代的渲染引擎中,通常都会同时包含forward和deferred shading两种渲染方法。

Global Illumination

Global illumination是指在场景中增加更真实的光照效果的一组技术。具体地说,global illumination用于模拟indirect lighting(间接光照),意思是指照射到一个表面上的光线是由其他objects反射出来的。考虑一下在本书所编写的shaders中使用的ambient lighting(环境光),我们使用了一个表示光照强度的常量值,并统一用于整个object的表面。显然,这种方式并不是真正的ambient lighting。Ambient light是由场景中objects之间各种光线的相互反射的结果;一个表面被遮挡的越多(相对于其他的反射表面)接收到的间接光照就越少,因此就会显得越暗。

有多种流行的技术用于模拟间接光照,从实现上可以在物理准确性和程序运行速度之间进行折衷。Ambient occlusion描述了一组渲染技术用于模拟照射到一个表面的光照数量。Ambient occlusion还可以通过提前计算,并像涂画一样添加到textures中。在实现开发中这是一种常用的方法,因为这种方法可以增加光照的真实感而不会影响程序运行时的性能。但是,这种textures无法用于动态的光照,如果光照改变了或者周围的环境被修改了,就会揭示出这种静态的光照textures。一个被广泛采用的实时光照技术是screen-space ambient occlusion(SSAO)。SSAO主要在screen space执行(作为一个post-processing步骤),因此独立于场景的复杂运算。具体地说,先把场景渲染到一个depth buffer中,然后采样一个pixel周围的深度值计算一个遮挡因子。此外,SSAO可以完全使用GPU实现,并且能很好的支持场景中动态的objects。关于SSAO的详细讲解超出了本书的范围,我们把这个主题留给读者自己研究。

在现在渲染技术中另一个常用于global illumination的主题是spherial harmonic lighting(SH lighting)(同样,关于SH lighting详细讲解也超出了本书的范围)。虽然SH lighting涉及大量的数学计算,但是可以概括为一组技术用于提前计算场景环境的作用(比如全局光照环境)得到spherial harmonic coefficients(系数)。我们所讨论的所有光照技术都可以看作rendering equation的简化,rendering equation是指一套系统用于生成完全基于物理运动的图像。另外,rendering equation是在各个方向半球表面的积分,无法支持实时渲染。SH lighting使用spherical函数替换rendering equation中的部分函数。这样就可以把积分计算简化为SH系数的dot product运算,而不用在运行时计算rendering equation。使用SH lighting可以实时地产生非常直接的画面,但是场景中objects的位置和形式必须保持不变,或者每一个object都有一组独立的SH系数。

Compute Shaders

在DirectX发布的同时,Microsoft引入了DirectCompute API,该库支持基于GPU的通用编程(而不仅仅是图形编程)。Compute shaders允许你在实际编程中,把任何计算都转移到GPU中,旨在减少GPU的计算工作并充分利于显卡的大规模并行运算的架构。

Computer shader(CS)阶段位于正常的渲染管线之外,但是可以读写GPU的数据。尽管computer shaders可以支持任意数量的通用程序,但是用于图形程序中能够生成一些特别有趣的效果。例如,我们可以把compute shader用于处理deferred shading lighing pass,执行deferred shading的边缘光滑运算,depth-of-field blurring(根据深度值进行局部区域模糊),或者通用的post-processing。从compute shader阶段输出的数据可以直接与渲染管线的输入进行绑定,而不用把这些输出数据再传递到CPU中。

Threads

Compute shaders可以使用一个thread group,在多个线程上并行的运行。一个thread group包含一组线程,这些线程可以访问同一块共享内存区域。Thread groups被创建为一个三维列表,并在执行compute shader时通过调用ID3D11DeviceContext::Dispatch(UINT threadGroupCountX, UINT threadGroupCountY, UINT threadGroupCountZ)函数指定列表的大小。另外,使用compute shader包含的numthreads(UINT x, UINT y, UINT z)属性指定一个group中所包含的线程数量。例如:

[numthreads(32, 32, 1)]
void compute_shader(uint3 threadID : SV_DispatchThreadID)
{
    // Shader body
}

通过计算thread groups数量与每一个group包含的线程数量的乘积,可以得到总线程数量。例如,以这些参数调用ID3D11DeviceContext::Dispatch(32, 24, 1),结合上面的compute shader就会总共产生(32×32, 24×32, 1×1) = 1024×768×1个线程。在GPU中根据处理器的数量在这些线程中分配对应的计算工作。

A Simpler Compute Shader

编写复杂的compute shaders超出了本节的范围,但是列表22.4中列出一个简单的compute shader用于向一个2D texture中写入颜色值。
列表22.4 A Compute Shader That Writes to a 2D Texture

RWTexture2D<float4> OutputTexture;

cbuffer CBufferPerFrame
{
    float2 TextureSize;
    float BlueColor;
};

[numthreads(32, 32, 1)]
void compute_shader(uint3 threadID : SV_DispatchThreadID)
{
    OutputTexture[threadID.xy] = float4((threadID.xy / TextureSize), BlueColor, 1);
}

technique11 compute
{
    pass p0
    {
        SetVertexShader(NULL);
        SetGeometryShader(NULL);
        SetPixelShader(NULL);
        SetComputeShader(CompileShader(cs_5_0, compute_shader()));
    }
}

在该shader中首先声明了一个可读写(RW)的2D textures(OutputTexture),用于接受compute shader的写入。这种数据类型允许多个线程同时向texture中写入数据。通过访问一个使用数组样式规则的2D纹理坐标,可以获取一个指定的texel。

在该compute shader中包含有一个输入参数threadID,并使用SV_DisaptchThreadID semantic进行标记。该参数包含了compute shader运行时所在线程的唯一标识符。在列表22.4所示的shader使用这个标识符作为访问output texture的索引值。使用这种方法的情况下,每一次执行compute shader时都会写入一个特定的texel。如果没有指定足够多的线程数(在本例中)用于覆盖整个texture,那么部分texels就会被写入颜色的数据。

需要注意的是,与vertex和pixel shaders一样,在compute shader也可以声明并访问constant buffer数据。在列表22.4所示的例子中,变量TextureSize用于格式化线程ID(ID值的类型为无符号整形),变量BlueColor用于指定texel的blue通道值。

在该示例对应的CPU端程序中,使用一个unordered access view(UAV)与OutputTexture变量进行绑定。一个UVA支持多个线程随机访问(读写)一个resource。具体地说就是,可以使用多个线程同时读写resource而不用担心内存冲突问题。列表22.5中列出了创建UAV的初始化代码。另外,还使用同一个texture创建了一个shader resource view(SRV),并与一个pxiel shader进行绑定用于渲染compute shader的输出颜色值。
列表22.5 Initializing a UAV and an SRV for the Compute Shader Demo

D3D11_TEXTURE2D_DESC textureDesc;
ZeroMemory(&textureDesc, sizeof(textureDesc));
textureDesc.Width = mGame->ScreenWidth();
textureDesc.Height = mGame->ScreenHeight();
textureDesc.MipLevels = 1;
textureDesc.ArraySize = 1;
textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
textureDesc.SampleDesc.Count = 1;
textureDesc.SampleDesc.Quality = 0;
textureDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;

HRESULT hr;
ID3D11Texture2D* texture = nullptr;
if (FAILED(hr = mGame->Direct3DDevice()->CreateTexture2D(&textureDesc, nullptr, &texture)))
{
    throw GameException("IDXGIDevice::CreateTexture2D() failed.", hr);
}

D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
ZeroMemory(&uavDesc, sizeof(uavDesc));
uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D.MipSlice = 0;

if (FAILED(hr = mGame->Direct3DDevice()->CreateUnorderedAccessView(texture, &uavDesc, &mOutputTexture)))
{
    ReleaseObject(texture);
    throw GameException("IDXGIDevice::CreateUnorderedAccessView() failed.", hr);
}

D3D11_SHADER_RESOURCE_VIEW_DESC resourceViewDesc;
ZeroMemory(&resourceViewDesc, sizeof(resourceViewDesc));
resourceViewDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
resourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
resourceViewDesc.Texture2D.MipLevels = 1;

if (FAILED(hr = mGame->Direct3DDevice()->CreateShaderResourceView(texture, &resourceViewDesc, &mColorTexture)))
{
    ReleaseObject(texture);
    throw GameException("IDXGIDevice::CreateShaderResourceView() failed.", hr);
}

ReleaseObject(texture);

注意其中是如何把mOutputTexture(UAV)和mColorTexture(SRV)引用同一个texture。

该示例的绘制操作主要是,更新compute shader对应的material数据,调度compute shader的线程,最后把compute shader输出的texture渲染到一个full-screen quad中。列表22.6中列出了ComputeShaderDemo::Draw()函数的代码。
列表22.6 The Draw() Method of the Compute Shader Demo

void ComputeShaderDemo::Draw(const GameTime& gameTime)
{
    ID3D11DeviceContext* direct3DDeviceContext = mGame->Direct3DDeviceContext();

    // Update the compute shader's material
    mMaterial->TextureSize() << XMLoadFloat2(&mTextureSize);
    mMaterial->BlueColor() << mBlueColor;
    mMaterial->OutputTexture() << mOutputTexture;
    mComputePass->Apply(0, direct3DDeviceContext);

    // Dispatch the compute shader
    direct3DDeviceContext->Dispatch(mThreadGroupCount.x, mThreadGroupCount.y, 1);

    // Unbind the UAV from the compute shader, so we can bind the same underlying resource as an SRV
    static ID3D11UnorderedAccessView* emptyUAV = nullptr;
    direct3DDeviceContext->CSSetUnorderedAccessViews(0, 1, &emptyUAV, nullptr);

    // Draw the texture written by the compute shader
    mFullScreenQuad->Draw(gameTime);
    mGame->UnbindPixelShaderResources(0, 1);

    mRenderStateHelper.SaveAll();
    mSpriteBatch->Begin();

    std::wostringstream helpLabel;
    helpLabel << std::setprecision(2) << "Color Offset: " << mBlueColor;

    mSpriteFont->DrawString(mSpriteBatch, helpLabel.str().c_str(), mTextPosition);

    mSpriteBatch->End();
    mRenderStateHelper.RestoreAll();
}

其中,通过调用ID3D11DeviceContext::CSSetUnorderedAccessViews()函数解除了UAV与compute shder阶段的绑定。之所以要解除绑定是因为在full-screen quad的Draw()函数中使用了同一个resource。尤其是full-screen quad与ComputeShaderDemo::UpdateRenderingMaterial()回调函数进行关联,用于设置material的ColorTexture变量值。

在该示例中,使用以下的方法在每一帧更新shader变量BlueColor:

void ComputeShaderDemo::Update(const GameTime& gameTime)
{
    mBlueColor = (0.5f) * static_cast<float>(sin(gameTime.TotalGameTime())) + 0.5f;
}

图22.2中显示了compute shader示例程序的输出截图。
Output of the compute shader demo
图22.2 Output of the compute shader demo.

本节只是简单的介绍了compute shaders。DirectCompute可以为应用程序带来惊人的计算能力。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值