DirectX11 With Windows SDK--26 计算着色器:入门

前言

现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

概述

GPU通常被设计为从一个位置或连续的位置读取并处理大量的内存数据(即流操作),而CPU则被设计为专门处理随机内存的访问。

由于顶点数据和像素数据可以分开处理,GPU架构使得它能够高度并行,在处理图像上效率非常高。但是一些非图像应用程序也能够利用GPU强大的并行计算能力以获得效益。GPU用在非图像用途的应用程序可以称之为:通用GPU(GPGPU)编程。

GPU需要数据并行的算法才能从GPU的并行架构中获得优势,并不是所有的算法都适合用GPU来实现。对于大量的数据,我们需要保证它们都进行相似的操作以确保并行处理。比如顶点着色器都是对大量的顶点数据进行处理,而像素着色器也是对大量的像素片元进行处理。

对于GPGPU编程,用户通常需要从显存中获取运算结果,将其传回CPU。这需要从显存将结果复制到内存中,这样虽然速度会慢一些,但起码还是比直接在CPU进行运算会快很多。如果是用于图形编程的话倒是可以省掉数据传回CPU的时间,比如说我们要对渲染好的场景再通过计算着色器来进行一次模糊处理。

在Direct3D中,计算着色器也是一个可编程着色器,它并不属于渲染管线的一个直接过程。我们可以通过它对GPU资源进行读写操作,运行的结果通常会保存在Direct3D的资源中,我们可以将它作为结果显示到屏幕,可以给别的地方作为输入使用,甚至也可以将它保存到本地。

线程和线程组

在GPU编程中,我们编写的着色器程序会同时给大量的线程运行,可以将这些线程按网格来划分成线程组。一个线程组由一个多处理器来执行,如果你的CPU有16个多处理器,你会想要把问题分解成至少16个线程组以保证每个多处理器都工作。为了获取更好的性能,你看你会想要每个多处理器来处理至少2个线程组,这样当一个线程组在等待别的资源时就可以先去考虑完成另一个线程组的工作。

每个线程组都会获得共享内存,这样每个线程都可以访问它。但是不同的线程组不能相互访问对方获得的共享内存。

线程同步操作可以在线程组中的线程之间进行,但处于不同线程组的两个线程无法被同步。事实上,我们没有办法控制不同线程组的处理顺序,毕竟线程组可以在不同的多处理器上执行。

一个线程组由N个线程组成。硬件实际上会将这些线程划分成一系列warps(一个warp包含32个线程),并且一个warp由SIMD32中的多处理器进行处理(32个线程同时执行相同的指令)。在Direct3D中,你可以指定一个线程组不同维度下的大小使得它不是32的倍数,但是出于性能考虑,最好还是把线程组的维度大小设为warp的倍数。

将线程组的大小设为256看起来是个比较好的选择,它适用于大量的硬件情况。修改线程组的大小意味着你还需要修改需要调度的线程组数目。

注意:NVIDIA硬件中,每个warp包含32个线程。而ATI则是每个wavefront包含64个线程。warp或者wavefront的大小可能随后续硬件的升级有所修改。

ID3D11DeviceContext::Dispatch方法--调度线程组执行计算着色器程序

方法如下:

void ID3D11DeviceContext::Dispatch(
    UINT ThreadGroupCountX,     // [In]X维度下线程组数目
    UINT ThreadGroupCountY,     // [In]Y维度下线程组数目
    UINT ThreadGroupCountZ);    // [In]Z维度下线程组数目

可以看到上面列出了X, Y, Z三个维度,说明线程组本身是可以3维的。当前例子的一个线程组包含了8x8x1个线程,而线程组数目为3x2x1,即我们进行了这样的调用:

md3dDeviceContext->Dispatch(3, 2, 1);

第一份计算着色器程序

现在我们有这两张图片,我想要将它混合并将结果输出到一张图片:

下面的这个着色器负责对两个纹理的像素颜色进行分量乘法运算。

Texture2D gTexA : register(t0);
Texture2D gTexB : register(t1);

RWTexture2D<float4> gOutput : register(u0);

// 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
    gOutput[DTid.xy] = gTexA[DTid.xy] * gTexB[DTid.xy];
}

上面的代码有如下需要注意的:

  1. Texture2D仅能作为输入,但RWTexture2D<T>类型支持读写,在本样例中主要是用于输出
  2. RWTexture2D<T>使用时也需要指定寄存器,u说明使用的是无序访问视图寄存器
  3. [numthreads(X, Y, Z)]修饰符指定了一个线程组包含的线程数目,以及在3D网格中的布局
  4. 每个线程都会执行一遍该函数
  5. SV_DispatchThreadID是当前线程在3D网格中所处的位置

如果使用1D纹理,线程修饰符通常为[numthreads(X, 1, 1)][numthreads(1, Y, 1)]

如果使用2D纹理,线程修饰符通常为[numthreads(X, Y, 1)],即第三维度为1

2D纹理X和Y的值会影响你在调度线程组时填充的参数

纹理输出与无序访问视图

留意上面着色器代码中的类型RWTexture2D<T>,你可以对他进行像素写入,也可以从中读取像素。不过模板参数类型填写就比较讲究了。我们需要保证纹理的数据格式和RWTexture2D<T>的模板参数类型一致,这里使用下表来进行描述比较常见的类型:

DXGI_FORMAT HLSL类型
DXGI_FORMAT_R32_FLOAT float
DXGI_FORMAT_R32G32_FLOAT float2
DXGI_FORMAT_R32G32B32A32_FLOAT float4
DXGI_FORMAT_R32_UINT uint
DXGI_FORMAT_R32G32_UINT uint2
DXGI_FORMAT_R32G32B32A32_UINT uint4
DXGI_FORMAT_R32_SINT int
DXGI_FORMAT_R32G32_SINT int2
DXGI_FORMAT_R32G32B32A32_SINT int4
DXGI_FORMAT_R16G16B16A16_FLOAT float4
DXGI_FORMAT_R8G8B8A8_UNORM unorm float4
DXGI_FORMAT_R8G8B8A8_SNORM snorm float4

此外,UAV不支持DXGI_FORMAT_B8G8R8A8_UNORM

其中unorm float表示的是一个32位无符号的,规格化的浮点数,可以表示范围0到1
而与之对应的snorm float表示的是32位有符号的,规格化的浮点数,可以表示范围-1到1

从上表可以得知DXGI_FORMAT枚举值的后缀要和HLSL的类型对应(浮点型对应浮点型,整型对应整型,规格化浮点型对应规格化浮点型),否则可能会引发下面的错误(这里举DXGI_FORMATunormHLSL类型为float的例子):

D3D11 ERROR: ID3D11DeviceContext::Dispatch: The resource return type for component 0 declared in the shader code (FLOAT) is not compatible with the resource type bound to Unordered Access View slot 0 of the Compute Shader unit (UNORM). This mismatch is invalid if the shader actually uses the view (e.g. it is not skipped due to shader code branching). [ EXECUTION ERROR #2097372: DEVICE_UNORDEREDACCESSVIEW_RETURN_TYPE_MISMATCH]

由于DXGI_FORMAT的部分格式比较紧凑,HLSL中能表示的最小类型通常又比较大。比如DXGI_FORMAT_R16G16B16A16_FLOATfloat4,个人猜测HLSL的类型为了能传递给DXGI_FORMAT,允许做丢失精度的同类型转换。

现在我们回到C++代码,现在需要创建一个2D纹理,然后在此基础上再创建无序访问视图作为着色器输出。

bool GameApp::InitResource()
{

    HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\flare.dds",
        nullptr, mTextureInputA.GetAddressOf()));
    HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\flarealpha.dds",
        nullptr, mTextureInputB.GetAddressOf()));

    // 创建用于UAV的纹理,必须是非压缩格式
    D3D11_TEXTURE2D_DESC texDesc;
    texDesc.Width = 512;
    texDesc.Height = 512;
    texDesc.MipLevels = 1;
    texDesc.ArraySize = 1;
    texDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    texDesc.SampleDesc.Count = 1;
    texDesc.SampleDesc.Quality = 0;
    texDesc.Usage = D3D11_USAGE_DEFAULT;
    texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE |
        D3D11_BIND_UNORDERED_ACCESS;
    texDesc.CPUAccessFlags = 0;
    texDesc.MiscFlags = 0;

    HR(md3dDevice->CreateTexture2D(&texDesc, nullptr, mTextureOutputA.GetAddressOf()));

    // 创建无序访问视图
    D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
    uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
    uavDesc.Texture2D.MipSlice = 0;
    HR(md3dDevice->CreateUnorderedAccessView(mTextureOutputA.Get(), &uavDesc,
        mTextureOutputA_UAV.GetAddressOf()));

    // 创建计算着色器
    ComPtr<ID3DBlob> blob;
    HR(CreateShaderFromFile(L"HLSL\\TextureMul_R32G32B32A32_CS.cso",
        L"HLSL\\TextureMul_R32G32B32A32_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()));
    HR(md3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTextureMul_R32G32B32A32_CS.GetAddressOf()));
}   

观察上面的代码,如果我们想要让纹理绑定到无序访问视图,就需要提供D3D11_BIND_UNORDERED_ACCESS绑定标签。

注意:如果你还为纹理创建了着色器资源视图,那么UAV和SRV不能同时绑定到渲染管线上。

ID3D11DeviceContext::CSSetUnorderedAccessViews--计算着色阶段设置无序访问视图

void ID3D11DeviceContext::CSSetUnorderedAccessViews(
    UINT                      StartSlot,                        // [In]起始槽,值与寄存器对应
    UINT                      NumUAVs,                          // [In]UAV数目
    ID3D11UnorderedAccessView * const *ppUnorderedAccessViews,  // [In]UAV数组
    const UINT                *pUAVInitialCounts                // [In]忽略
);

调度过程实现如下:

void GameApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);

    md3dImmediateContext->CSSetShaderResources(0, 1, mTextureInputA.GetAddressOf());
    md3dImmediateContext->CSSetShaderResources(1, 1, mTextureInputB.GetAddressOf());

    // DXGI Format: DXGI_FORMAT_R32G32B32A32_FLOAT
    // Pixel Format: A32B32G32R32
    md3dImmediateContext->CSSetShader(mTextureMul_R32G32B32A32_CS.Get(), nullptr, 0);
    md3dImmediateContext->CSSetUnorderedAccessViews(0, 1, mTextureOutputA_UAV.GetAddressOf(), nullptr);
    md3dImmediateContext->Dispatch(32, 32, 1);

    HR(SaveDDSTextureToFile(md3dImmediateContext.Get(), mTextureOutputA.Get(), L"Texture\\flareoutputA.dds"));

    SendMessage(MainWnd(), WM_DESTROY, 0, 0);
}

由于我们的位图是512x512x1大小,一个线程组的线程布局为16x16x1,线程组的数目自然就是32x32x1了。如果调度的线程组宽度或高度不够,输出的位图也不完全。而如果提供了过宽或过高的线程组并不会影响运行结果,只是提供的线程组资源过多有些浪费而已。

最后通过ScreenGrab库将纹理保存到文件,就可以结束程序了。

运行结束后,可以打开flareoutputA.dds查看结果(建议使用DxTex打开):

那么问题来了,如果我想要输出DXGI_FORMAT_R8G8B8A8_UNORM的纹理,那应该怎么做呢?

  1. 将纹理创建时使用的DXGI_FORMAT换成DXGI_FORMAT_R8G8B8A8_UNORM,连同UAV的Format也要替换
  2. 计算着色器将RWTexture2D<float4>类型替换成RWTexture2D<unorm float4>类型

修改后的着色器代码如下:

Texture2D gTexA : register(t0);
Texture2D gTexB : register(t1);

RWTexture2D<unorm float4> gOutput : register(u0);

// 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
    gOutput[DTid.xy] = (unorm float4)(gTexA[DTid.xy] * gTexB[DTid.xy]);
}

注意:如果你使用了HLSL Tools For Visual Studio插件,它不认unorm类型,从而引发所谓的语法错误提示。你可以直接无视去编译项目,它还是能成功编译出着色器的。

运行的图片显示结果基本上是一样的,只是输出的纹理格式不太一样:

练习题

粗体字为自定义题目

  1. 尝试修改ID3D11DeviceContext::Dispatch的参数,观察运行结果
  2. 尝试利用计算着色器来计算出两张纹理的颜色差异值,并保存为图片观察结果

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

原文地址:https://www.cnblogs.com/X-Jun/p/10340652.html

时间: 2024-10-11 21:25:16

DirectX11 With Windows SDK--26 计算着色器:入门的相关文章

DirectX11 With Windows SDK--16 利用几何着色器可选的流输出阶段帮助绘制多种分形

前言 在上一章,我们知道了如何使用几何着色器来重新组装图元,比如从一个三角形分裂成三个三角形.但是为了实现更高阶的分形,我们必须要从几何着色器拿到输出的顶点.这里我们可以使用可选的流输出阶段来拿到顶点集合. 注意: 本章末尾有大量的GIF动图! DirectX11 With Windows SDK完整目录 Github项目源码 流输出阶段 现在我们知道GPU可以写入纹理(textures),例如深度/模板缓冲区以及后备缓冲区.当然,我们也可以通过渲染管线的流输出阶段让GPU将几何着色器输出的顶点

DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果

前言 上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区.接下来我们继续利用它来实现一些新的效果,在这一章,你将了解: 实现公告板效果 Alpha-To-Coverage 对GPU资源进行读/写操作 纹理数组 实现雾效 DirectX11 With Windows SDK完整目录 Github项目源码 实现雾效 虽然这部分与几何着色器并没有什么关系,但是雾的效果在该Demo中会用到,并且前面也没有讲过这部分内容,故先在这里提出来. 有时候我们需要在游戏中模拟一些特定的天

DirectX11 Windows Windows SDK--28 计算着色器:波浪(水波)

前言 有关计算着色器的基础其实并不是很多.接下来继续讲解如何使用计算着色器实现水波效果,即龙书中所实现的水波.但是光看代码可是完全看不出来是在做什么的.个人根据书中所给的参考书籍找到了对应的实现原理,但是里面涉及到比较多的物理公式.因此,要看懂这一章需要有高数功底(求导.偏导.微分方程),我会把推导过程给列出来. 本章演示项目还用到了其他的一些效果,在学习本章之前建议先了解如下内容: 章节内容 11 混合状态 17 利用几何着色器实现公告板效果(雾效部分) 26 计算着色器:入门 27 计算着色

nBodyCS&lt;I&gt;学习笔记之计算着色器

nBodyCS<I>学习笔记之计算着色器 Nvidia-SDK(1) 版权声明:本文为博主原创文章,未经博主允许不得转载. DirectX一直是Windows上图形和游戏开发的核心技术.DirectX提供了一种在显卡上运行的程序--着色器(Shader).从DirectX11开始,DirectX增加了一种计算着色器(Compute Shader),它是专门为与图形无关的通用计算设计的.因此DirectX就变成了一个通用GPU计算的平台.鉴于GPU拥有极其强大的并行运算能力,学习使用Direct

DirectX11 With Windows SDK--29 计算着色器:内存模型、线程同步;实现顺序无关透明度(OIT)

前言 由于透明混合在不同的绘制顺序下结果会不同,这就要求绘制前要对物体进行排序,然后再从后往前渲染.但即便是仅渲染一个物体(如上一章的水波),也会出现透明绘制顺序不对的情况,普通的绘制是无法避免的.如果要追求正确的效果,就需要对每个像素位置对所有的像素按深度值进行排序.本章将介绍一种仅DirectX11及更高版本才能实现的顺序无关的透明度(Order-Independent Transparency,OIT),虽然它是用像素着色器来实现的,但是用到了计算着色器里面的一些相关知识. 这一章综合性很

DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子

前言 到这里计算着色器的主线学习基本结束,剩下的就是再补充两个有关图像处理方面的应用.这里面包含了龙书11的图像模糊,以及龙书12额外提到的Sobel算子进行边缘检测.主要内容源自于龙书12,项目源码也基于此进行调整. 学习目标: 熟悉图像处理常用的卷积 熟悉高斯模糊.Sobel算子 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报. 图像卷积 在图像处理中,经常需要用到

DirectX11 With Windows SDK 24--Render-To-Texture(RTT)技术的应用、使用ScreenGrab保存纹理到文件

前言 尽管在上一章的动态天空盒中用到了Render-To-Texture技术,但那是针对纹理立方体的特化实现.考虑到该技术的应用层面非常广,在这里抽出独立的一章专门来讲有关它的通用实现以及各种应用.此外,这里还会讲到如何使用DirectXTex的ScreenGrab来保存纹理,可以说是干货满满了. 如果想要看Render-To-Texture在动态天空盒的应用,可以点此回顾: 章节 23 立方体映射:动态天空盒的实现 DirectX11 With Windows SDK完整目录 Github项目

WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码

原文:WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码 HLSL,High Level Shader Language,高级着色器语言,是 Direct3D 着色器模型所必须的语言.WPF 支持 Direct3D 9,也支持使用 HLSL 来编写着色器.你可以使用任何一款编辑器来编写 HLSL,但 Shazzam Shader Editor 则是专门为 WPF 实现像素着色器而设计的一款编辑器,使用它来编写像素着色器,可以省去像素着色器接入

粒子系统与雨的效果 (DirectX11 with Windows SDK)

前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用Windows SDK来实现. 顺手安利一波:我最近在学DX11 with WindowSDK 教程 博客地址: https://www.cnblogs.com/X-Jun/p/9028764.html 所以下面的都是基于这个教程和上面提到的那本书来写的,推荐看到教程17章和书里第15章之后再来看这篇博客,有助