我们其实很早就接触过纹理了,之前的深度缓冲区与后台缓冲区,它们都是通过ID3D12Resource接口表示,并以D3D12_RESOURCE_DESC::Dimension成员中D3D12_RESOURCE_DIMENSION_TEXTURE2D类型来描述2D纹理对象。
2D纹理是一种由特定数据元素所构成的矩阵,或者说存有纹理数据元素的2D数组,同理还有1D和3D纹理,就是1D数组和3D数组。
但纹理不同于缓冲区资源,因为缓冲区资源仅存储数据数组,而纹理却可以具有多个mipmap层级,GPU会基于这个层级进行相应的特殊操作,比如使用过滤器以及多重采样。支持这些特殊操作的纹理资源会被限定为一些特定的数据格式(缓冲区则没有这些限制,可以存储任意类型)。
纹理所支持的数据格式由枚举类型DXGI_FORMAT来表示:
RGBA表示各分量,但不一定要存储颜色信息。
还有无类型格式,DXGI_FORMAT_R8G8B8A8_TYPELESS,只有在不得已的情况下才使用。
DirectX 11 SDK 文档中提到,以某种具体类型创建的资源,其格式是不能改变的。这将使该资源在运行时的访问得以优化。
一个纹理可以绑定到渲染流水线的不同阶段,一个常见的例子是将一个纹理用作渲染目标[RTV](D3D中的渲染到纹理技术),也可以充当着色器资源[SRV],但是不能身兼数职。将数据渲染到一个纹理后,再用它作为着色器资源,这种方法称为渲染到纹理。-- 需要纹理扮演者两种角色,需要创建对应的RTV和SRV
// 绑定为渲染目标
CD3DX12_CPU_DESCRIPTOR_HANDLE rtv = ...;
CD3DX12_CPU_DESCRIPTOR_HANDLE dsv = ...;
cmdList->OMSetRenderTargets(1, &rtv, true, &dsv);
// 以着色器输入的名义绑定到根参数
CD3DX12_GPU_DESCRIPTOR_HANDLE = tex = ...;
cmdList->SetGraphicsRootDescriptorTable(rootParamIndex, tex);
Direct3D所采用的纹理坐标系,是由指向图像水平正方向的u轴与指向图像垂直正方向的v轴所组成的。取值范围为0≤u,v≤1的坐标(u,v)标定的是一种称为纹素(texel) -- 采用归一化坐标区间[0,1]。
注意:纹理坐标系的v轴正方向是从上往下!
对于每个3D三角形来说,我们希望在将要映射于其上的纹理中定义出与之对应的三角形。设p0、p1、p2为3D三角形的3个顶点,它们分别对应于纹理坐标q0、q1、q2。针对3D三角形上任意一点(x,y,z)处的纹理坐标(u,v),我们都可以通过与3D三角形坐标插值所用的相同参数s、t,对顶点纹理坐标进行线性插值来求得:
当s≥0,t≥0,s+t≤1时,那么:
更新顶点结构体:
struct Vertex{
DirectX::XMFLOAT3 Pos;
DirectX::XMFLOAT3 Normal;
DirectX::XMFLOAT2 TexC; // tex coordinate 纹理坐标 (u, v)
};
// 对应输入布局
std::vector mInputLayout =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
};
我们可以选择性的映射部分纹理,也可以将几张毫无关联的纹理合为一个大的纹理图(被称为:纹理图集,texture atlas),再将它应用于若干不同的物体。
贴图师通常会借助Photoshop或一些其他的图像编辑器为游戏制作纹理,最后再将他们保存为某种格式的图像文件,比如BMP、DDS、TGA或PNG等。随后,游戏应用程序会在加载期间将图像文件载入ID3D12Resource对线。对于实时图形应用程序来说,DDS图像文件格式(DirectDraw图面格式,DirectDraw Surface format, DDS)是一种尚佳的选择:除了支持GPU可原生处理的各种图像格式,他还支持一些GPU自身可解压的压缩图像格式。
贴图师们不宜将DDS格式当作工作过程中所用的图像格式,而是应当用他们认为更顺手的格式来保存工作进程。待纹理最终完成后,再为游戏应用程序把它导出为DDS格式。
①DDS格式概述:
DDS对于3D图形来说是一种理想的格式,支持专用于3D图形的特殊格式以及纹理类型。从本质上来讲,它是一种针对GPU而专门设计的图像格式。DDS纹理满足于3D图形开发的以下特征:
DDS格式能够支援不同的像素格式。像素格式由枚举类型DXGI_FORMAT中的成员来表示,但是并非所有的格式都适用于DDS纹理。非压缩图像数据一般采取下列格式:
随着虚拟场景中纹理数量的大幅增长,对GPU端显存的需求也迅速增加(所有纹理都位于GPU显存中)。为了缓解这些内存的需求压力,D3D支持下列几种压缩纹理格式(也称为块压缩, block compression):
②创建DDS文件:
①加载DDS文件:
微软公司提供了一组用来加载DDS文件的轻量级源代码,龙书编写时,这段轻量级代码只支持DX11,所以龙书作者在Common文件夹中修改了DDSTextureLoader.h/.cpp
HRESULT DirectX::CreateDDSTextureFromFile12(
_In_ ID3D12Device* device, // 指向用于创建纹理资源的D3D设备的指针
_In_ ID3D12GraphicsCommandList* cmdList,
_In_z_ const wchar_t* szFileName, // 欲加载图像文件名
_Out_ ComPtr& texture, // 返回载有图像数据的纹理资源
_Out_ ComPtr& textureUploadHeap, // 返回的纹理资源,在此,将它当作一个上传堆,用于将图像数据复制到默认堆中的纹理资源。GPU执行复制命令前,上传堆不能摧毁
_In_ size_t maxsize,
_Out_opt_ DDS_ALPHA_MODE* alphaMode
);
使用该函数:
struct Texture
{
// Unique material name for lookup.
std::string Name;
std::wstring Filename;
Microsoft::WRL::ComPtr Resource = nullptr;
Microsoft::WRL::ComPtr UploadHeap = nullptr;
};
void CrateApp::LoadTextures()
{
auto woodCrateTex = std::make_unique();
woodCrateTex->Name = "woodCrateTex";
woodCrateTex->Filename = L"../../Textures/WoodCrate01.dds";
ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(md3dDevice.Get(),
mCommandList.Get(), woodCrateTex->Filename.c_str(),
woodCrateTex->Resource, woodCrateTex->UploadHeap));
mTextures[woodCrateTex->Name] = std::move(woodCrateTex);
}
②着色器资源视图堆:srv heap
创建纹理资源后,还需要创建SRV描述符,并将其设置到一个根签名参数槽(root signature parameter slot),以供着色器程序使用。
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = 3; // 该堆能存储3个描述符(CBV/SRV/UAV)
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&mSrvDescriptorHeap)));
③创建着色器资源视图描述符:srv
typedef struct D3D12_SHADER_RESOURCE_VIEW_DESC
{
DXGI_FORMAT Format; // 资源格式 DXGI_FORMAT -- 如果之前指定的typeless,则在此处必须指定具体类型
D3D12_SRV_DIMENSION ViewDimension; // 资源的维数
UINT Shader4ComponentMapping; // 对纹理进行采样得到的结果中分量进行重新排序(比如R、G分量互换) -- D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING(不改变分量顺序)
union // 我们重点关心union联合体
{
D3D12_BUFFER_SRV Buffer;
D3D12_TEX1D_SRV Texture1D;
D3D12_TEX1D_ARRAY_SRV Texture1DArray;
D3D12_TEX2D_SRV Texture2D;
D3D12_TEX2D_ARRAY_SRV Texture2DArray;
D3D12_TEX2DMS_SRV Texture2DMS;
D3D12_TEX2DMS_ARRAY_SRV Texture2DMSArray;
D3D12_TEX3D_SRV Texture3D;
D3D12_TEXCUBE_SRV TextureCube;
D3D12_TEXCUBE_ARRAY_SRV TextureCubeArray;
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_SRV RaytracingAccelerationStructure;
} ;
} D3D12_SHADER_RESOURCE_VIEW_DESC;
// 2D纹理:
typedef struct D3D12_TEX2D_SRV
{
UINT MostDetailedMip; // 指出此视图中图像细节最详尽的Mipmap层级的索引 -- 取值范围0~MipLevels-1
UINT MipLevels; // 自MostDetailedMip算起,待创建视图的mipmap层级数量 -- 另外,可以设置为-1表示自MostDetailedMip开始到最后一个Mipmap层级之间所有的mipmap层级
UINT PlaneSlice; // 平面切片的索引
FLOAT ResourceMinLODClamp; // 指定可以访问的最小mipmap层级 -- 设置为0.0表示可以访问所有mipmap层级
} D3D12_TEX2D_SRV;
--------------------------------------------------------------------------------------
// ViewDimension:
常见资源维数:①D3D12_SRV_DIMENSION_TEXTURE2D②当然还有1D、3D③D3D12_SRV_DIMENSION_TEXTURECUBE 立方体纹理(cube texture)
// MostDetailedMip、MipLevels:
通过这两个成员,可以指定此视图mipmap层级的连续子范围
示例,创建三个srv:
// 获取指向描述符堆的起始处句柄:
CD3DX12_CPU_DESCRIPTOR_HANDLE hDescriptor(mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
// auto woodCrateTex = mTextures["woodCrateTex"]->Resource;
// 或者假设有3个纹理资源: brickTex stoneTex tileTex
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = brickTex->GetDesc().Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = woodCrateTex->GetDesc().MipLevels;
srvDesc.Texture2D.ResourceMinLODClamp = 0.0f;
md3dDevice->CreateShaderResourceView(brickTex.Get(), &srvDesc, hDescriptor);
// 偏移到下一个描述符处:
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = stoneTex->GetDesc().Format();
srvDesc.Texture2D.MipLevels = stoneTex->GetDesc().MipLevels;
md3dDevice->CreateShaderResourceView(stoneTex.Get(), &srvDesc, hDescriptor);
// 偏移到下一个描述符处:
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = tileTex->GetDesc().Format();
srvDesc.Texture2D.MipLevels = tileTex->GetDesc().MipLevels;
md3dDevice->CreateShaderResourceView(tileTex.Get(), &srvDesc, hDescriptor);
④将纹理绑定到流水线:
我们之前绘制调用使用的材质,都是通过程序的材质常量缓冲区来进行更新的,这对程序是一种极大的限制。纹理映射技术的想法是用纹理图来去带材质常量缓冲区以获取材质数据。
比如,我们将添加漫反射反照率纹理图(diffuse albedo texture map),以此来给出材质的漫反射反照率分量。影响材质的两个数据gFresnelR0与gRoughness仍然将每次绘制调用时由材质常量缓冲区来指定。第十九章将借助纹理在像素层级指定粗糙度。
注意,使用纹理贴图时,我们仍然需要在材质常量缓冲区中保留gDiffuseAlbedo分量。我们通常将常量缓冲区中gDiffuseAlbedo设置为(1,1,1,1),然而,可以通过对该数据进行调整以避免制作新的纹理。
// PS:像素着色器中,将纹理漫反射反照率数据与DiffuseAlbedo相组合
float4 texDiffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC);
// 纹理样本与常量缓冲区中的反照率相乘
float4 diffuseAlbedo = texDiffuseAlbedo * gDiffuseAlbedo;
更新Material:
struct Material
{
...
int DiffuseSrvHeapIndex = -1; // diffuseAlbedo纹理 -- 对应SRV在heap中的索引
};
接下来,根签名处,比如创建一个描述符表,将SRV都放置在表中,通过句柄和偏移找到SRV位置,再通过SetGraphicsRootDescriptorTable来向根签名填入实参。
事实上,纹理资源可以运用于任何着色器,但此处我们之将其作用于像素着色器。其实,纹理在本质上就是一种支持GPU特殊操作的特别数组。
纹理图集可以在一次绘制调用中渲染多个几何体,可以通过将所有几何体放置在一个渲染项中,降低渲染过程的开销。
①放大:
我们可以将纹理图中的元素看作从连续图像中采集的离散颜色样本,但并不应认为它们是有着特定面积大小的矩形。所以,当前的疑问是:如果在纹理坐标(u,v)处没有与之对应的纹素点究竟会发生什么?
比如,当玩家慢慢靠近场景中的一堵墙壁时,便产生了纹理放大的概念,用少量纹素来覆盖大量的像素。对三角形中的顶点纹理进行插值时,会得到特定的纹理坐标,但该纹理坐标处可能没有对应的纹素。对此,我们可以对纹素之间的颜色数据进行插值,有线性插值和常数插值等方法。线性插值更为普遍。在纹理这一语境中,常数插值求得纹理数据称为点过滤,而采用线性插值则称为线性过滤。
1D插值 -- 最近邻点采样:通过最近的点构建分段常量函数来求出纹素点之间的某处的近似值
2D插值(又称为双线性插值 bilinear interpolation):给出临近的4个点,先对水平方向u上进行两次1D插值,再对垂直方向上进行一次1D内插。-- 在uv平面上进行插值
回顾:插值公式
1D插值:
纹理放大是一个无法避免的问题,当与目标保持特定距离时,纹理可能看上去还不错,但随着观察点逐渐接近目标,其效果就开始惨不忍睹了。解决方案:①对虚拟视角过于接近物体表面的行为进行了限制,避免过度放大处理②通过更高分辨率的纹理。
②缩小:
在缩小的过程中,大量纹素将被映射到少数纹理之上。考虑这种情况,当游戏人物远离墙壁时,墙会越来越小。同理,也会出现纹理坐标没有对应的纹素的情况,所以也需要常熟插值过滤器与线性插值过滤器。然而,执行纹理缩小操作还有更多的工作要做:mipmap技术
在初始化期间,通过对图像下采样来创建mipmap链便可制作出缩小版的纹理。
在运行时,图形硬件将根据程序员的设定,从以下两种不同的执行方案中择一而行:
PS或texconv程序可以基于原始的图像数据,创建mipmap:利用采样算法来生成更低的mipmap层级图像。但有时候,这些算法不能保留所希望的图像细节,贴图师还需要亲手编辑更低mipmap级别的图像。
texconv下载与使用方法:
texconv下载以及使用命令 - HONT - 博客园 (cnblogs.com)
③各向异性过滤:anisotropic filtering
各向异性过滤器有助于缓解当多边形法向量与摄影机观察向量之间夹角过大所导致的失真现象 -- 比如当多边形正交于观察窗口。这种过滤器开销最大,但是其矫正失真的效果对得起它所消耗的资源。
可以将经过常数插值或线性插值的纹理定义为一个返回向量值的函数,即给定纹理坐标,D3D允许我们采用下列四种不同的方式(即寻址模式,address mode)来扩充此函数的定义域:
通过寻址模式可以提升纹理的分辨率。执行平铺的时候,纹理是否是无缝衔接的也是一个重点。
在D3D中,寻址模式由枚举类型D3D12_TEXTURE_ADDRESS_MODE来表示:
typedef enum D3D12_TEXTURE_ADDRESS_MODE
{
D3D12_TEXTURE_ADDRESS_MODE_WRAP = 1,
D3D12_TEXTURE_ADDRESS_MODE_MIRROR = 2,
D3D12_TEXTURE_ADDRESS_MODE_CLAMP = 3,
D3D12_TEXTURE_ADDRESS_MODE_BORDER = 4,
D3D12_TEXTURE_ADDRESS_MODE_MIRROR_ONCE = 5
} D3D12_TEXTURE_ADDRESS_MODE;
前面两个小节中讨论的纹理过滤以及寻址模式,都是由采样器对象来定义的。
①创建采样器:
采样器会被着色器使用,为了将采样器绑定到着色器上,需要为采样器对象绑定描述符。
// 设置根签名:
// 根签名第二个槽位是一个描述符表,放置一个采样器描述符
CD3DX12_DESCRIPTOR_RANGE descRange[3];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLE, 1, 0);
descRange[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
CD3DX12_ROOT_PARAMETER rootParameters[3];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[2].InitAsDescriptorTable(1, &descRange[2], D3D12_SHADER_VISIBILITY_ALL);
CD3DX12_ROOT_SIGNATURE_DESC descRootSignature;
descRootSignature.Init(3, rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
采样器描述符需要一个采样器堆。
// 描述符堆:存放采样器描述符
D3D12_DESCRIPTOR_HEAP_DESC descHeapSampler = {};
descHeapSampler.NumDescriptors = 1;
descHeapSampler.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
descHeapSampler.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
ComPtr mSamplerDescriptorHeap;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapSampler, __uuidof(ID3D12DescriptorHeap), (void**)&mSamplerDescriptorHeap));
创建采样器描述符,填写D3D12_SAMPLER_DESC对象:
typedef struct D3D12_SAMPLER_DESC
{
D3D12_FILTER Filter;
D3D12_TEXTURE_ADDRESS_MODE AddressU;
D3D12_TEXTURE_ADDRESS_MODE AddressV;
D3D12_TEXTURE_ADDRESS_MODE AddressW;
FLOAT MipLODBias;
UINT MaxAnisotropy;
D3D12_COMPARISON_FUNC ComparisonFunc;
FLOAT BorderColor[4];
FLOAT MinLOD;
FLOAT MaxLOD;
} D3D12_SAMPLE_DESC;
// Filter:
D3D12_FILTER枚举类型的成员之一 -- 指定采样纹理时的过滤方式
该枚举类型的常用成员:
D3D12_FILTER_MIN_MAG_MIP_POINT:对纹理图与mipmap层级进行点过滤
D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT:对纹理图进行双线性过滤,但对mipmap进行点过滤
D3D12_FILTER_MIN_MAG_MIP_LINEAR:对纹理图和mipmap层级都进行(双)线性过滤 -- 这也被称为3线性过滤
D3D12_FILTER_ANISOTROPIC:对于纹理的放大、缩小以及mipmap均采用向异性过滤
// AddressU/V/W:
纹理在水平u轴/垂直v轴/深度w轴方向上的寻址模式 -- 深度w轴仅限于3D纹理
// MipLODBias:
设置mipmap层级的偏置值,如果设置为0.0,则mipmap层级保持不变;如果设置为2,而mipmap层级设置为3,那么将按照3+2进行采样
// MaxAnisotropy:
最大各项异性值,该参数的取值区间为[1,16],设置越大的值,渲染效果越好。
只有将Filter设置为D3D12_FILTER_ANISOTROPIC或D3D12_FILTER_COMPARISON_ANISOTROPIC之后该项才生效
// ComparisonFunc:
用于实现像阴影贴图(shadow mapping)这样一类特殊应用的高级选项,未解除阴影贴图这一章节前,暂时设置为D3D12_COMPARISON_FUNC_ALWAYS
// BorderColor:
用于指定D3D12_TEXTURE_ADDRESS_MODE_BORDER寻址模式下的边框颜色
// MinLOD/MaxLOD:
可供选择的最小/大mipmap层级
示例:创建一个采样器描述符,该采样器使用线性过滤与重复寻址模式,其他参数保留默认值
D3D12_SAMNPLER_DESC samplerDesc = {};
samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; // 两个双线性插值
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
md3dDevice->CreateSampler(&samplerDesc, mSamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
②静态采样器:
事实证明,图形应用程序通常不会使用过多的采样器。为此,D3D专门提供了一种特殊的方式来定义采样器数组,使用户可以在不创建采样器堆的情况下也能对它们进行配置。CD3DX12_ROOT_SIGNATURE_DESC类由两种参数不同的Init函数,用户可以借此为应用程序定义所用的静态采样器数组。静态采样器使用结构体D3D12_STATIC_SAMPLER_DESC来描述,与之前采样器结构略有区别:
enum D3D12_STATIC_BORDER_COLOR
{
D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK = 0,
D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK = (D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK + 1),
D3D12_STATIC_BORDER_COLOR_OPAQUE_WHITE = (D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK + 1)
} D3D12_STATIC_BORDER_COLOR;
我们的演示程序预先创建6种静态采样器,即使不用将其预留在此也行:
使用CD3DX12_STATIC_SAMPLER_DESC结构快捷构建采样器结构
std::array CrateApp::GetStaticSamplers()
{
const CD3DX12_STATIC_SAMPLER_DESC pointWrap(
0, // shaderRegister
D3D12_FILTER_MIN_MAG_MIP_POINT, // filter
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_WRAP); // addressW
const CD3DX12_STATIC_SAMPLER_DESC pointClamp(
1, // shaderRegister
D3D12_FILTER_MIN_MAG_MIP_POINT, // filter
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_CLAMP); // addressW
const CD3DX12_STATIC_SAMPLER_DESC linearWrap(
2, // shaderRegister
D3D12_FILTER_MIN_MAG_MIP_LINEAR, // filter
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_WRAP); // addressW
const CD3DX12_STATIC_SAMPLER_DESC linearClamp(
3, // shaderRegister
D3D12_FILTER_MIN_MAG_MIP_LINEAR, // filter
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_CLAMP); // addressW
const CD3DX12_STATIC_SAMPLER_DESC anisotropicWrap(
4, // shaderRegister
D3D12_FILTER_ANISOTROPIC, // filter
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_WRAP, // addressW
0.0f, // mipLODBias
8); // maxAnisotropy
const CD3DX12_STATIC_SAMPLER_DESC anisotropicClamp(
5, // shaderRegister
D3D12_FILTER_ANISOTROPIC, // filter
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressU
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressV
D3D12_TEXTURE_ADDRESS_MODE_CLAMP, // addressW
0.0f, // mipLODBias
8); // maxAnisotropy
return {
pointWrap, pointClamp,
linearWrap, linearClamp,
anisotropicWrap, anisotropicClamp };
}
创建根签名时:
void CrateApp::BuildRootSignature()
{
CD3DX12_DESCRIPTOR_RANGE texTable;
texTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
// Perfomance TIP: Order from most frequent to least frequent.
slotRootParameter[0].InitAsDescriptorTable(1, &texTable, D3D12_SHADER_VISIBILITY_PIXEL);
slotRootParameter[1].InitAsConstantBufferView(0);
slotRootParameter[2].InitAsConstantBufferView(1);
slotRootParameter[3].InitAsConstantBufferView(2);
// ---------------------------------------------------------------------
// ⭐:不同之处:这里创建采样器,并填入根签名结构中
auto staticSamplers = GetStaticSamplers();
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(4, slotRootParameter,
(UINT)staticSamplers.size(), staticSamplers.data(),
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// ---------------------------------------------------------------------
ComPtr serializedRootSig = nullptr;
ComPtr errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());
if(errorBlob != nullptr)
{
::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(mRootSignature.GetAddressOf())));
}
HLSL语法:定义纹理对象,并分配给指定的纹理寄存器
Texture2D gDiffuseMap : register(t0);
HLSL语法:定义多个采样器对象,并分配到特定的采样器寄存器
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
对纹理进行采样:Texture2D::Sample方法
Texture2D gDiffuseMap : register(t0);
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD;
};
...
float4 PS(VertexOut pin) : SV_Target
{
// Sample方法返回的就是纹理图在点(u,v)处的插值颜色
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;
...
}
设置纹理:纹理被创建后,SRV存于描述符堆中,那么我们只要把对应的SRV设置到根签名坑位处即可绑定到渲染流水线
CD3DX12_GPU_DESCRIPTOR_HANDLE tex(
mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart()
);
tex.Offset(ri->Mat->DiffuseSrvHeapIndex, mCbvSrvDescriptorSize);
...
cmdList->SetGraphicsRootDescriptorTable(0, tex);
Default.hlsl文件:
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif
#include "LightingUtil.hlsl"
Texture2D gDiffuseMap : register(t0);
SamplerState gsamLinear : register(s0);
// 每一帧都有变化的常量数据
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform; // 纹理变换
};
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight;
Light gLights[MaxLights];
};
cbuffer cbMaterial : register(b2)
{
float4 gDiffuseAlbedo;
float3 gFresnelR0;
float gRoughness;
float4x4 gMatTransform; // 材质变换
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD; // 纹理坐标
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD; // 纹理坐标
};
VertexOut VS(VertexIn vin)
{
VertexOut vout = (VertexOut)0.0f;
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
vout.PosH = mul(posW, gViewProj);
// 为了对三角形进行插值操作而输出的顶点属性
// 纹理坐标 * gTexTransform * gMatTransform
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, gMatTransform).xy;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
// 纹理采样
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamLinear, pin.TexC) * gDiffuseAlbedo;
pin.NormalW = normalize(pin.NormalW);
float3 toEyeW = normalize(gEyePosW - pin.PosW);
float4 ambient = gAmbientLight*diffuseAlbedo;
const float shininess = 1.0f - gRoughness;
Material mat = { diffuseAlbedo, gFresnelR0, shininess };
float3 shadowFactor = 1.0f;
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
pin.NormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
litColor.a = diffuseAlbedo.a;
return litColor;
}
上述程序中常量缓冲区变量gTexTransform与gMatTransform还未进行讨论。纹理坐标表示的是纹理平面中的2D点,也就是说这些点也能进行缩放、平移与旋转。比如:先对纹理进行拉伸后再贴在模型上,或者天空的纹理按时间平移变换,或一些特效比如火球纹理旋转
gTexTransform:关于材质的纹理变换(针对像水那样的动态材质)
gMatTransform:关于物体属性的纹理变换
我们只关心前两个坐标轴的变换情况,所以令z为0.0(使z坐标平移与否不影响结果),令w令为1.0(保证平移操作)
⭐课后习题:火球纹理旋转:
XMMATRIX trans = XMMatrixRotationZ(gt.TotalTime());
XMStoreFloat4x4(&mat->MatTransform, trans);
遇到的问题:纹理图案是按照原点进行旋转的,而不是火球的中心点。现在的贴图的旋转中心是(0, 0)点,我们需要将(0, 0)点设置到贴图中心,即UV取值范围改为(-0.5, 0.5)之间。我们先将UV取值范围改为(-0.5, 0.5),最后再平移图像,使得火球在正中间。
参考文章:DX12纹理篇:习题 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/165481502
VertexOut VS(VertexIn vin)
{
............
vin.TexCoord -= float2(0.5f, 0.5f);//将UV取值范围改为(-0.5, 0.5)
//计算UV坐标的静态偏移(相当于MAX中编辑UV)
float4 texCoord = mul(float4(vin.TexCoord, 0.0f, 1.0f), gTexTransform);
//配合时间函数计算UV坐标的动态偏移(UV动画)
vout.UV = mul(texCoord, gMatTransform).xy;
vout.UV += float2(0.5f, 0.5f);//u和v方向各平移0.5个单位
return vout;
}
通过修改uv坐标实现纹理的平移,而不用考虑当前mipmap层级中图像的分辨率!
uv限制在归一化区间[0,1]^2的操作可以让程序员忽略图像分辨率的具体大小。