最近由于换工作,上班较忙,以前的学习节奏被打乱了,导致DX12的学习进度落下挺多的,现正在逐渐适应并调整学习时间,争取尽快步入正轨。
言归正传,本篇我们要分享动态索引。本次案例将对纹理数组进行动态索引,所以我们将使用StructuredBuffer将材质数据上传到GPU,由每个渲染物体动态索引到对应材质,再在材质的基础上动态索引到对应的贴图。这样做的目的是将每次渲染时的描述符数量降到最低,降低程序开销。
1. 动态索引前的数据准备
首先我们取消使用MatConstants,而使用MaterialData(之后将作为StructureBuffer使用),将材质数据列入其中,并加入diffuseMapIndex字段,以此来索引材质所对应的贴图。
//作为StructureBuffer上传到GPU
struct MaterialData
{
XMFLOAT4 diffuseAlbedo = { 1.0f, 1.0f, 1.0f, 1.0f };//材质反照率
XMFLOAT3 fresnelR0 = { 0.01f, 0.01f, 0.01f };//RF(0)值,即材质的反射属性
float roughness = 0.25f;//材质的粗糙度
XMFLOAT4X4 matTransform = MathHelper::Identity4x4();//纹理动画位移矩阵
UINT diffuseMapIndex = 0;//纹理数组索引
//占位,向量数据打包时必须占满4位
UINT matPad0;
UINT matPad1;
UINT matPad2;
};
接着在ObjectConstants中加入materialIndex字段,使得渲染每个物体时,索引到对应的材质。
//单个物体的物体常量数据(不变的)
struct ObjectConstants
{
//初始化物体空间变换到裁剪空间矩阵,Identity4x4是单位矩阵,需要包含MathHelper头文件
XMFLOAT4X4 world = MathHelper::Identity4x4();
XMFLOAT4X4 texTransform = MathHelper::Identity4x4();//UV坐标变化矩阵
//不同物体去索引对应材质
UINT materialIndex = 0;//材质的索引
UINT objPad0; //占位
UINT objPad1; //占位
UINT objPad2; //占位
};
然后我们在帧资源中定义matSB的智能指针,并初始化构造函数,由于纹理和材质数据是根据物体不同而动态上传的,也就是我们需要运行时动态修改数据,所以使用UploadBuffer来存放数据。注意此时的matSB是属于StructuredBuffer,不属于ConstantBuffer,所以isConstant的Bool值为false
struct FrameResources
{
public:
............
//每帧都需要单独的资源缓冲区(此案例仅为3个常量缓冲区)
std::unique_ptr<UploadBufferResource<ObjectConstants>> objCB = nullptr;
std::unique_ptr<UploadBufferResource<PassConstants>> passCB = nullptr;
//std::unique_ptr<UploadBufferResource<MatConstants>> matCB = nullptr;
//指向结构化缓冲区MaterialData的智能指针
std::unique_ptr<UploadBufferResource<MaterialData>> matSB = nullptr;
............
};
FrameResources::FrameResources(ID3D12Device* device, UINT passCount, UINT objCount, UINT matCount)
{
............
matSB = std::make_unique<UploadBufferResource<MaterialData>>(device, matCount, false);
}
接着,将objCB、matSB、passCB以及texture资源分别绑定到跟签名上。matSB需绑定到SRV或者UAV上,我们选择SRV,由于texture也是绑定到SRV上的,所以我们让他们分别绑定到SRV寄存器的不同空见上,具体写法:InitAsShaderResourceView(/*寄存器槽号*/0, /*RegisterSpace*/ 1);
CD3DX12_DESCRIPTOR_RANGE srvTable;
srvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, //描述符类型
4, //表中的描述符数量(纹理数量)
0); //描述符所绑定的寄存器槽号
//根参数可以是描述符表、根描述符、根常量
CD3DX12_ROOT_PARAMETER slotRootParameter[4];
slotRootParameter[0].InitAsDescriptorTable(1,//Range数量
&srvTable, //Range指针
D3D12_SHADER_VISIBILITY_PIXEL); //该资源只能在像素着色器可读
slotRootParameter[1].InitAsConstantBufferView(0);//objCB绑定槽号为0的寄存器
//matSB绑定槽号为0的寄存器(和纹理公用一个SRV寄存器,但是不同Space)
//StructureBuffer必须使用SRV或者UAV来绑定
slotRootParameter[2].InitAsShaderResourceView(/*寄存器槽号*/0, /*RegisterSpace*/ 1);
slotRootParameter[3].InitAsConstantBufferView(1);//passCB绑定槽号为1的寄存器
需要注意的是,寄存器Space的用法只在ShaderModel5.1及以上才能使用,所以我们将ShaderModel改成5.1
vsBytecode = ToolFunc::CompileShader(L"Shaderscolor.hlsl", nullptr, "VS", "vs_5_1");
psBytecode = ToolFunc::CompileShader(L"Shaderscolor.hlsl", nullptr, "PS", "ps_5_1");
由于我们是在shader中动态索引贴图和材质,所以我们只需将texture和matSB一次性绑定到流水线上,所以在Draw函数直接设置一次即可,不用像之前那样,在DrawRenderItems函数中每次绘制都要设置一遍。而objCB还是在DrawRenderItems中设置。
void ShapesApp::Draw()
{
............
//设置描述符表,将纹理资源与流水线绑定(因为只绑定一次,所以不需要做地址偏移)
cmdList->SetGraphicsRootDescriptorTable(0, srvHeap->GetGPUDescriptorHandleForHeapStart());
//设置matSB的描述符(因为只绑定一次,所以不需要做地址偏移)
auto matSB = currFrameResources->matSB->Resource();
cmdList->SetGraphicsRootShaderResourceView(2,//根参数索引
matSB->GetGPUVirtualAddress());//子资源地址
//设置根描述符
auto passCB = currFrameResources->passCB->Resource();
cmdList->SetGraphicsRootConstantBufferView(3, //根参数索引
passCB->GetGPUVirtualAddress());
//绘制渲染项
DrawRenderItems();
............
}
然后更新objConstants中的materialIndex属性,通过当前渲染项所对应的材质ID赋值。这样就能索引到对应材质了,并通过材质中的纹理ID索引到对应纹理。
//将材质常量缓冲区中的matCBIndex赋值给新增在objConstants中的materialIndex字段
objConstants.materialIndex = e->mat->matCBIndex;
同时更新matSB中数据,此处函数名仍旧是UpdateMatCBs,由于我懒,所以没有改,其实已经是SB了。
void ShapesApp::UpdateMatCBs()
{
auto currMatSB = currFrameResources->matSB.get();
for (auto& e : materials)
{
Material* mat = e.second.get();//获得键值对的值,即Material指针(智能指针转普通指针)
if (mat->numFramesDirty > 0)
{
//将定义的材质属性传给常量结构体中的元素
matData.diffuseAlbedo = mat->diffuseAlbedo;
matData.fresnelR0 = mat->fresnelR0;
matData.roughness = mat->roughness;
XMMATRIX matTransform = XMLoadFloat4x4(&mat->matTransform);
XMStoreFloat4x4(&matData.matTransform, XMMatrixTranspose(matTransform));
matData.diffuseMapIndex = mat->diffuseSrvHeapIndex;//纹理在SRV堆中索引
//将材质常量数据复制到常量缓冲区对应索引地址处
currMatSB->CopyData(mat->matCBIndex, matData);
//更新下一个帧资源
mat->numFramesDirty--;
}
}
}
2. Shader中进行动态索引
来到shader中,首先定义MaterialData,用来接收matSB中数据
struct MaterialData
{
float4 gDiffuseAlbedo; //材质反照率
float3 gFresnelR0; //RF(0)值,即材质的反射属性
float gRoughness; //材质的粗糙度
float4x4 gMatTransform; //UV动画变换矩阵
uint gDiffuseMapIndex;//纹理数组索引
uint gMatPad0;
uint gMatPad1;
uint gMatPad2;
};
接着定义纹理数组。还记得之前我们是使用Texture2DArray数据类型来定义的纹理数组,但那要求将纹理预先打包成Texture2DArray数据类型,而此次是直接使用Texture2D类型的数组来定义纹理数组,如:Texture2D tex[4] ,而这种形式的纹理数组必须是ShaderModel5.1及以上才能使用。Texture2D与Texture2DArray类型数组不同的是,此数组中所存纹理的尺寸和格式可各不相同,这使它比一般纹理数组更为灵活。
//一种只有SM5.1才支持的纹理数组
//与Texture2DArray类型数组不同的是,此数组中所存纹理的尺寸和格式可各不相同
//这使它比一般纹理数组更为灵活
Texture2D gDiffuseMap[4] : register(t0);
然后定义结构化缓冲区,注意我们使用了space1空间来存放数据。
//材质数据的结构化缓冲区,使用t0的space1空间
StructuredBuffer<MaterialData> gMaterialData : register(t0, space1);
然后定义传入的objCB数据,增加gMaterialDataIndex字段。
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform; //UV顶点变换矩阵
uint gMaterialDataIndex;
uint gObjPad0;
uint gObjPad1;
uint gObjPad2;
};
passCB以及采样器类型数据我们就不贴了,和之前代码是一样的。
然后是顶点着色器代码。StructuredBuffer本身就是一个数组,所以它能通过材质索引来找到当前顶点所对应的材质属性,比如gMatTransform
VertexOut VS(VertexIn vin)
{
VertexOut vout;
//使用结构化缓冲区数组(结构化缓冲区是由若干类型数据所组成的数组)
MaterialData matData = gMaterialData[gMaterialDataIndex];
............
//配合时间函数计算UV坐标的动态偏移(UV动画)
vout.UV = mul(texCoord, matData.gMatTransform).xy;
return vout;
}
最后是片段着色器,同理我们将材质中需要用到的diffuseAlbedo, fresnelR0, roughness动态传递给光照函数,以便计算最终光照。我们还使用了纹理数组来动态调用纹理,并采样返回albedo
float4 PS(VertexOut pin) : SV_Target
{
//获取材质数据(需要点出来,和CB使用不太一样)
MaterialData matData = gMaterialData[gMaterialDataIndex];
float4 diffuseAlbedo = matData.gDiffuseAlbedo;
float3 fresnelR0 = matData.gFresnelR0;
float roughness = matData.gRoughness;
uint diffuseTexIndex = matData.gDiffuseMapIndex;
//在数组中动态地查找纹理
diffuseAlbedo *= gDiffuseMap[diffuseTexIndex].Sample(gSamAnisotropicWarp, pin.UV);
float3 worldNormal = normalize(pin.WorldNormal);
float3 worldView = normalize(gEyePosW - pin.WorldPos);
Material mat = { diffuseAlbedo, fresnelR0, roughness };
float3 shadowFactor = 1.0f;//暂时使用1.0,不对计算产生影响
//直接光照
float4 directLight = ComputerLighting(gLights, mat, pin.WorldPos, worldNormal, worldView, shadowFactor);
//环境光照
float4 ambient = gAmbientLight * diffuseAlbedo;
//漫反射光照
float4 diffuse = directLight * diffuseAlbedo;
//总光照
float4 finalCol = ambient + diffuse;
finalCol.a = diffuseAlbedo.a;
return finalCol;
}
编译运行,画面正常显示,但是这时候的材质和贴图都是在shader中动态调用的。
![3638f4092041a46115fe3e0f1aac8910.png](https://i-blog.csdnimg.cn/blog_migrate/5aec8fd9bb819ac9c71e6be162ad7f7f.jpeg)
本篇就分享就这里,下一篇我们将进行章后习题的分享,巩固本章学习,下篇见!