如果开始研究计算着色器了,说明读者已经有一定的D3D11基础,自己也跑过几个程序,那么我希望看完的人能够达到自己完成编写计算着色器文件,完成自己的项目任务。由于我学习D3D11是直接跳过其他着色器的(项目无关),所以有很多基础很差,本文也只提供计算着色器相关的内容,和如何使用计算着色器处理图像的一点经验
这篇笔记很短,主要记录了我在使用计算着色器的一些经验和我认为必须的点。
1. 计算着色器hlsl文件编写(以求解图像的暗通道为例)
2. 着色器文件的编译
3. 计算着色器资源设置
计算着色器HLSL文件编写
本文使用的代码是在X_JUN的代码上面修改的,他的源码可以在这里下载。
hlsl文件通过VS,在项目内创建一个着色器文件夹,直接创建源文件,然后修改后缀为hlsl即可。创建好文件后,记得修改属性页的入口点和hlsl文件主函数名一致,否则无法编译。选择着色器类型和模型,一般D3D11都支持5.0,不同的设备可以查询支持的模型然后选择。

下面是求解图像的暗通道的代码,hlsl代码主要由指定输入输出,线程声明,和主函数组成。
Texture2D g_TexA : register(t0); //输入纹理
RWTexture2D<float4> g_Output : register(u0); //输出纹理
输入输出
着色器的输入输出如上图所示,在这份程序中,输入输出是2D纹理,所以我们需要声明其类型及变量名。后面的resgiter,则是着色器文件的注册机制,如果是纹理(texture),则使用t加上输入序号,输入纹理1则为register(t0)。值得注意的是输出,尽管也是2D纹理,因为是可以读写的,所以使用了RWTexture2D,register(u0)则表示这是一个无序访问视图(可以修改的)类型。这些变量在C++代码中的声明和设置将在后面说道。
此外,如果有时候我们需要输入一些变量作为着色器文件的参数,可以使用 buffer。下面给出了一个简单的例子,这个变量是从CPU传到GPU供着色器使用的,注意传入的buffer的大小必须是16字节的整数倍,否则会报错,无法传入。
cbuffer CB: register(b0)
{
uint c1; // 参数
uint c2; //参数
uint useless1; // 未使用
uint useless2; //未使用
}
线程声明
下面是线程声明,numthread给出了一个线程组内,X,Y,Z方向各多少个线程,单个线程组的线程数不能大于1024(D3D11限制)。线程组个数通过C++中使用Dispatch函数给出,也是分配了XYZ方向有多少线程组(线程组最多为65535),同时Dispatch也是启动计算着色器的函数。线程数限制具体可以查找MSDN文档得到,不同的版本最大线程的限制不一样。
// 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(32, 32, 1)]
这里借一幅参考资料3中的图,来说明线程组和线程的关系。左边就是我们使用dispatch函数分配的线程组个数,其每个组对应了一个位置,右边则是一个线程组内的线程分布,这是由numthread所分配的。

主函数
主程序就是每个线程要要处理的程序。首先看这里我们可以获得的线程的坐标,即主函数输入的参数。这里的ID就是上图中可以看到的坐标。对于要处理图像的任务来说,这让我们可以读出每个输入2D纹理的像素值,可以写出到输出2D纹理的位置。需要注意的是(在我才开始接触的时候混淆的一点),这里的线程坐标和图像坐标并不是同一个。一个线程是处理一个子任务,我们可以让一个线程去处理图像一个4*4微元的值,只要我们知道相应的对应关系,即哪一个线程处理哪一块的图像,我们就可以在hlsl文件中编写相应的功能。
这几个坐标中,使用的较多主要是SV_DispatchThreadID,这是一个三维的向量对应xyz坐标。如代码片段中,用DTid的xy坐标,就可以索引对应纹理的像素值。
基础的着色器语言可以查看参考资料3这本书,有简单介绍,基本和C++相似,但有些不同,比如代码中的float4这种变量。
Texture2D g_TexA : register(t0); //输入纹理
RWTexture2D<float4> g_Output : register(u0); //输出纹理
// 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(32, 32, 1)]
//每个线程的处理程序,也可以对应每个像素
void CS( uint3 Gid : SV_GroupID, //线程组ID
uint3 DTid : SV_DispatchThreadID, //当前线程对应的全局ID
uint3 GTid : SV_GroupThreadID, //线程组内的线程ID
uint GI : SV_GroupIndex //线程组内将线程一维展开后,对应的索引
)
{
//求解暗通道(即求解RGB通道里面的最小值)
float4 outV;
float min;
min = g_TexA[DTid.xy][0];
if (min > g_TexA[DTid.xy][1])
min = g_TexA[DTid.xy][1];
if (min > g_TexA[DTid.xy][2])
min = g_TexA[DTid.xy][2];
outV[0] = min;
outV[1] = 0;
outV[2] = 0;
outV[3] = g_TexA[DTid.xy][3];
//求暗通道里面的最大值
g_Output[DTid.xy] = outV;
}
着色器文件的编译、着色器创建和使用
编译着色器文件参考资料2的博客文章给出了详细的说明。我主要用到了以下函数。
//读取编译好的cso(complied shader object)文件
HRESULT WINAPI
D3DReadFileToBlob(_In_ LPCWSTR pFileName,
_Out_ ID3DBlob** ppContents);
//编译hlsl文件
HRESULT WINAPI
D3DCompileFromFile(_In_ LPCWSTR pFileName,
_In_reads_opt_(_Inexpressible_(pDefines->Name != NULL)) CONST D3D_SHADER_MACRO* pDefines,
_In_opt_ ID3DInclude* pInclude,
_In_ LPCSTR pEntrypoint,
_In_ LPCSTR pTarget,
_In_ UINT Flags1,
_In_ UINT Flags2,
_Out_ ID3DBlob** ppCode,
_Always_(_Outptr_opt_result_maybenull_) ID3DBlob** ppErrorMsgs);
以上函数输出的变量是ID3DBlob **类型,我的理解是我们要使用的着色器对象,使用这个变量我们就可以创建对应文件的着色器。利用以下函数即可创建着色器,其中m_DarkChannel_CS是预先定义的着色器,类型为ID3D11ComputeShader。这里用到了D3D11设备这个对象m_pd3dDevice创建着色器。
/*函数原型
virtual HRESULT STDMETHODCALLTYPE CreateComputeShader(
/* [annotation] */
_In_reads_(BytecodeLength) const void *pShaderBytecode,
/* [annotation] */
_In_ SIZE_T BytecodeLength,
/* [annotation] */
_In_opt_ ID3D11ClassLinkage *pClassLinkage,
/* [annotation] */
_COM_Outptr_opt_ ID3D11ComputeShader **ppComputeShader)
*/
m_pd3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr,
m_DarkChannel_CS.GetAddressOf())
计算着色器资源设置
着色器文件的输入输出都是在C++代码中指定的。
输入通过XXSetShaderResource()来设置(XX是指着色器类型,计算着色器即为CS)。需要注意的是设置着色器资源必须要使用纹理对应的ShaderResourceView类型的资源,所以在C++代码中,必须要要给纹理创建一个着色器资源才能作为着色器的输入。以下代码给出了该函数的定义和使用方法。第一个参数是输入的第几个资源,对应着色器文件中的t0。第二个参数一般是设为1就行;第三个参数是纹理的指针地址,m_pTextureInputA是着色器资源类型的指针。调用方法是通过上下文对象调用函数来绑定到对应着色器对象上。
////着色器输入资源设置
/*
virtual void STDMETHODCALLTYPE CSSetShaderResources(
/* [annotation] */
_In_range_( 0, D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT - 1 ) UINT StartSlot,
/* [annotation] */
_In_range_( 0, D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT - StartSlot ) UINT NumViews,
/* [annotation] */
_In_reads_opt_(NumViews) ID3D11ShaderResourceView *const *ppShaderResourceViews)
*/
//设置计算着色器输入资源
m_pd3dImmediateContext->CSSetShaderResources(0, 1, m_pTextureInputA);
输出是通过调用以下无序视图函数来设置的,着色器输出必须是无序访问视图类型,需要提前建立对应的变量。
/*设置计算着色器输出资源,为无序访问视图
virtual void STDMETHODCALLTYPE CSSetUnorderedAccessViews(
/* [annotation] */
_In_range_( 0, D3D11_1_UAV_SLOT_COUNT - 1 ) UINT StartSlot,
/* [annotation] */
_In_range_( 0, D3D11_1_UAV_SLOT_COUNT - StartSlot ) UINT NumUAVs,
/* [annotation] */
_In_reads_opt_(NumUAVs) ID3D11UnorderedAccessView *const *ppUnorderedAccessViews,
/* [annotation] */
_In_reads_opt_(NumUAVs) const UINT *pUAVInitialCounts)
*/
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1,
m_pTextureOutputA_UAV.GetAddressOf(), nullptr)
完成以上的计算着色器文件编写,D3D初始化,着色器创建,创建需要用的的着色器资源视图和无序访问视图,我们就可以调用着色器时,只需要使用几句C++代码即可完成。
//设置着色器
m_pd3dImmediateContext->CSSetShader(m_DarkChannel_CS.Get(), nullptr, 0);
//设置计算着色器输入资源(着色器资源视图)
m_pd3dImmediateContext->CSSetShaderResources(0, 1, m_pTextureInputA.GetAddressOf());
//设置计算着色器输出资源(无序访问视图)
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1,
m_pTextureOutputA_UAV.GetAddressOf(), nullptr);
//分配线程组
m_pd3dImmediateContext->Dispatch(64, 32, 1);
通过以上代码,就可以实现利用D3D11的计算着色器求出图像暗通道图像,如下图所示。


参考资料:
2. 计算着色器:入门
3. Practical Rendering and Computation with Direct3D 11(非常重要,这本书前面还讲了一些HLSL语言的基础,值得一看。)

本文深入探讨D3D11计算着色器的使用,从HLSL文件编写到资源设置,详细讲解如何处理图像的暗通道。通过实际案例,帮助读者掌握计算着色器在图像处理中的应用。
1765

被折叠的 条评论
为什么被折叠?



