仅供我的学习使用,请勿转载。c++
咱们在第四章的时候就开始使用纹理了。特别是深度缓冲区和后台缓冲区,他们都是经过ID3D12Resource接口来表示的。为了便于参考,咱们将在这一节回顾一些和纹理相关的知识。数组
2D纹理是一种由特定数据元素所构成的矩阵,它的用处之一即是存储2D图像数据,纹理中的每个元素都对应着像素的颜色。可是存储2D图像数据并非它惟一的用途,好比在法线贴图中(后面会介绍),每个纹理元素存储的是一个3D向量而不是颜色数据。纹理就相似于由数据元素构成的1D、2D、3D数组,不管是1D仍是2D纹理,都是用泛型接口ID3D12Resource表示的。app
纹理不一样于缓冲区资源,缓冲区资源仅仅存储数据数组,而纹理能够拥有多个mipmap层级(后面会介绍),GPU能够根据mipmap层级进行相应的特殊操做。支持这些特殊操做的纹理会被限定为一些特定的数据格式,而缓冲区资源能够存储任意类型的数据,纹理所支持的数据格式由枚举类型DXGI_FOMAT表示,下面是一些格式示例:编辑器
格式名称 | 格式组成 |
---|---|
DXGI_FORMAT_R32G32B32_FLOAT | 每一个元素由3个32位浮点数份量组成 |
DXGI_FORMAT_R16G16B16A16_UNORM | 每一个元素由4个16位份量组成,每一个份量都会被映射到[0, 1] |
DXGI_FORMAT_R32G32_UINT | 每一个元素由2个32位无符号整数份量构成 |
DXGI_FORMAT_R8G8B8A8_UNORM | 每一个元素由4个8位无符号份量组成,每一个份量都会被映射到[0, 1] |
DXGI_FORMAT_R8G8B8A8_SNORM | 每一个元素都由4个8位有符号份量组成,每一个份量都会被映射到[-1, 1] |
DXGI_FORMAT_R8G8B8A8_SINT | 每一个元素都由4个8位有符号整数份量组成,每一个份量都会被映射到[-128, 127] |
DXGI_FORMAT_R8G8B8A8_UINT | 每一个元素都由4个8位有符号整数份量组成,每一个份量都会被映射到[0, 255] |
一个纹理能够绑定到渲染流水线的各个阶段,好比:一个纹理能够用做渲染目标,又能够把它用做着色器资源。要使纹理同时扮演着色器资源和渲染目标这两种角色,咱们须要为纹理资源建立两个描述符,一个存于渲染目标堆中,另外一个存放在着色器资源堆中。:工具
//绑定为渲染目标 CD3DX12_CPU_DESCRIPTOR_HANDLE rtv = ……; CD3DX12_CPU_DESCRIPTOR_HANDLE dsv = ……; cmdList->OMSetRenderTarget(1, &rtv, &dsv); //以着色器输入的名义绑定到根参数 CD3DX12_GPU_DESCRIPTOR_HANDLE tex = ……; cmdList->SetGrahicRootDescriptorTable(rootParameterIndex, tex);
资源描述符实际上就是通知Direct3D这些资源将被如何使用(咱们将资源绑定到渲染流水线的哪个阶段)布局
Direct3D所采用的纹理坐标系,是指由图像水平正方向u轴和指向图像垂直正方向的v轴所组成的(u轴正方向通常是水平向右,v轴正方向通常是垂直向下)。u和v的取值范围为0 - 1之间,坐标(u,v)标定的是一种称为纹素的纹理元素。由于对纹理坐标进行了归一化处理,因此Direct3D的工做能够摆脱具体纹理尺寸的影响。学习
设A、B、C为3D三角形的3个顶点,Q为3D三角形内的任意一点,他们分别对应于纹理坐标A一、B一、C1和Q1。则对于3D三角形上的任意一点,咱们均可以经过于3D三角形坐标插值所用的相同参数s和t,对顶点纹理坐标进行线性插值求得。 $$ Q = A + s(B - A) + t(C - A) $$动画
$$ Q1 = A1 + s(B1 - A1) + t(C1 - A1) $$spa
为了实现此计算过程,咱们须要为顶点结构体添加一个纹理坐标以表示纹理上的点。这样一来,每个3D顶点都有了与之对应的2D纹理顶点了。插件
//顶点结构体 struct Vertex{ DirectX::XMFLOAT3 Pos; DirectX::XMFLOAT3 Normal; DirectX::XMFLOAT2 TexC; } //输入布局描述 std::vector<D3D12_INPUT_ELEMENT_DESC> 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, };
注意:咱们能够将一个锐角三角形映射到一个直角三角形中,不过咱们要先进行拉伸操做,这会使贴图效果并不使人满意,因此咱们通常不作这种反人类的映射,除非贴图师但愿得到拉伸的效果故意而为之。
贴图师一般会使用一些图像编辑器为游戏制做纹理,最后将他们保存为某种格式的图像文件,好比BMP、DDS、TGA或者PNG等等,游戏应用程序会在加载期间将图像文件载入ID3D12Resource对象,对于实时图像应用程序来讲,DDS图像文件格式是最佳的选择。
DDS对于3D图形来讲是一种理想的格式,由于它本质是一种针对GPU而专门设计的一种图像格式,DDS纹理知足用于3D图形开发的如下特征:
DDS格式可以支持不一样的像素格式,像素格式由枚举类型DXGI_FORMAT中的成员表示,可是并非全部的格式都适用于DDS纹理,非压缩图像数据通常会采用下列格式:
随着虚拟场景中纹理数量的大量增加,对于GPU显存的需求也快速增长(全部的纹理通常都存放在显存中)。为了缓解显存的压力,咱们会采用压缩纹理格式,这种格式的优势是可让图像以压缩的形式存放在显存中,须要使用的时候对他们进行解压。这里就不列举经常使用的压缩纹理格式了,有兴趣能够自行百度。
下面介绍两种能够将经常使用的图像格式转换为DDS格式的方法:
一、使用Photoshop,Photoshop提供了一款能够将图像导出为DDS格式的插件,这里不过多介绍了
二、使用texconv的命令行工具,该工具能够将传统的图像格式转变为DDS文件,并且还能够调整图像大小,改变像素格式、生成mipmap等等。咱们能够在https://directxtex.codeplex.com/wikipage?title=Texassemble&referringTitle=Texconv找到它的文档和下载连接。
/* ** Summary:读取DDS文件的方法 ** Parameters: ** device:指向用于建立纹理资源的D3D设备 ** cmdList:提交GPU命令的命令列表 ** szFileName:图像文件名 ** texture:返回载有图像数据的纹理资源 ** textureUploadHeap:返回的纹理资源(一个用于将图像数据上传到默认堆中的上传堆) */ HRESULT CreateDDSTextureFromFile12(_In_ ID3D12Device* device, _In_ ID3D12GraphicsCommandList* cmdList, _In_z_ const wchar_t* szFileName, _Out_ Microsoft::WRL::ComPtr<ID3D12Resource>& texture, _Out_ Microsoft::WRL::ComPtr<ID3D12Resource>& textureUploadHeap, _In_ size_t maxsize = 0, _Out_opt_ DDS_ALPHA_MODE* alphaMode = nullptr );
下面的代码将展现如何用一个名为yaya.dds的图像来建立一个纹理资源:
struct Texture { // 为了便于查找而使用的惟一材质名 std::string Name; std::wstring Filename; Microsoft::WRL::ComPtr<ID3D12Resource> Resource = nullptr; Microsoft::WRL::ComPtr<ID3D12Resource> UploadHeap = nullptr; }; auto woodCreaTex = std::make_unique<Texture>(); woodCreaTex->Name = "yaya"; woodCreaTex->Filename = L"../../Textures/yaya.dds"; ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(md3dDevice.Get(), mCommandList.Get(), woodCreaTex->Filename.c_str(),woodCreaTex->Resource, woodCreaTex->UploadHeap));
建立了纹理资源以后,咱们还须要为纹理资源建立一个SRV(Shader Render View)描述符,并将其设置到一个根签名参数槽上,以供着色器程序使用。下面代码构建了一个能够容纳3个类型为CBV,SRV和UAV描述符的描述符堆。
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {}; srvHeapDesc.NumDescriptors = 3; 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)));
建立描述符堆以后,咱们即可以建立真正的描述符了。咱们经过填写D3D12_SHADER_RESOURCE_VIEW_DESC对象来建立SRV描述符,该结构体包含了资源的类型,格式、维数和mipmap数量等信息。
typedef struct D3D12_SHADER_RESOURCE_VIEW_DESC { //资源的格式 DXGI_FORMAT Format; //资源的维数 D3D12_SRV_DIMENSION ViewDimension; //在着色器进行采样时,它将会返回特定纹理坐标处的纹理数据向量 UINT Shader4ComponentMapping; 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_SHADER_RESOURCE_VIEW_DESC; typedef struct D3D12_TEX2D_SRV { //指定此视图中图像最详尽的mipmap层级的索引 UINT MostDetailedMip; //此视图的mipmap层级数量 UINT MipLevels; //平面切面的索引 UINT PlaneSlice; //指定能够访问的最小mipmap层级 FLOAT ResourceMinLODClamp; }D3D12_TEX2D_SRV;
接下来,让咱们构建3个资源描述符来填充上一节所建立的着色器资源视图堆
//假设已经建立了3个纹理资源 ComPtr<ID3D12Resource> bricksTex; ComPtr<ID3D12Resource> stoneTex; ComPtr<ID3D12Resource> tileTex; //获取指向描述符堆起始处的指针 CD3DX12_CPU_DESCRIPTOR_HANDLE hDescriptor(mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart()); D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; //资源的格式 srvDesc.Format = bricksTex->GetDesc().Format; //资源的维数 srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; //资源中细节最详尽的mipmap层级的索引 srvDesc.Texture2D.MostDetailedMip = 0; //资源中mipmap层级的数量 srvDesc.Texture2D.MipLevels = bricksTex->GetDesc().MipLevels; //指定能够访问的最小mipmap层级(设置为0.0f能够访问全部的mipmap层级) srvDesc.Texture2D.ResourceMinLODClamp = 0.0f; md3dDevice->CreateShaderResourceView(bricksTex.Get(), &srvDesc, hDescriptor); //偏移到堆中的下一个描述符处 hDescriptor.Offset(1, mCbvSrvUavDescriptorSize); //资源的格式 srvDesc.Format = stoneTex->GetDesc().Format; //资源的mipmap层级的数量 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);
在前面的演示程序中,咱们在每次绘制调用的时候所制定的材质都是由材质常量缓冲区来进行更新的。这就意味着在绘制调用的时候,咱们将不能动态的指定每个像素的数据。而纹理映射技术的想法就是使用纹理贴图(texturemap)来取代材质常量缓冲区以获取材质数据,这将使每一个像素的数据都是灵活多变的。
在本节中,咱们将添加漫反射反照率纹理图(diffuse albedo texture map)来指定漫反射反照率份量。影响材质的两个数值gFresnelR0和gRoughness将继续由材质常量缓冲区来指定。尽管咱们添加了漫反射反照率纹理图,咱们仍须要保留gDiffuseAlbedo份量。事实上,咱们将会在像素着色器中让漫反射反照率纹理图和gDiffuseAlbedo份量进行结合:
//从纹理中提取该像素的漫反射反照率 float4 texDiffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap,pin.TexC); //将纹理样本和常量缓冲区中的漫反射反照率相乘 float4 diffuseAlbedo = texDiffuseAlbedo * gDiffuseAlbedo;
注意:咱们一般将材质常量缓冲区中的gDiffuseAlbedo设置为(1,1,1,1),从而使texDiffuseAlbedo不会发生改变,不过咱们也会偶尔适当对gDiffuseAlbedo进行调整。
咱们想材质的定义中添加一个索引,借此引用和该材质相关的纹理描述符堆中的一个SRV
// 简单的结构体来表示咱们所演示的材料 struct Material { …… // 漫反射在SRV堆中的索引(在第九章的纹理贴图中会使用) int DiffuseSrvHeapIndex = -1; …… };
接下来咱们即可以将由着色器资源描述符构成的描述符表绑定到根签名上了。假设咱们将描述符表绑定到根签名的第0个槽位:
void CrateApp::DrawRenderItems(ID3D12GraphicsCommandList* cmdList, const std::vector<RenderItem*>& ritems) { // 常量缓冲区的大小必须为硬件最小分配空间(256B)的最小整数倍 UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)); UINT matCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(MaterialConstants)); // 当前帧资源对应的常量缓冲区 auto objectCB = mCurrFrameResource->ObjectCB->Resource(); auto matCB = mCurrFrameResource->MaterialCB->Resource(); for(size_t i = 0; i < ritems.size(); ++i) { auto ri = ritems[i]; cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView()); cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView()); cmdList->IASetPrimitiveTopology(ri->PrimitiveType); // 获取指向描述符堆起始处的指针并进行偏移 CD3DX12_GPU_DESCRIPTOR_HANDLE tex(mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart()); tex.Offset(ri->Mat->DiffuseSrvHeapIndex, mCbvSrvDescriptorSize); D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress() + ri->ObjCBIndex*objCBByteSize; D3D12_GPU_VIRTUAL_ADDRESS matCBAddress = matCB->GetGPUVirtualAddress() + ri->Mat->MatCBIndex*matCBByteSize; // 将描述符表设置到根签名的第0个槽位 cmdList->SetGraphicsRootDescriptorTable(0, tex); cmdList->SetGraphicsRootConstantBufferView(1, objCBAddress); cmdList->SetGraphicsRootConstantBufferView(3, matCBAddress); cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0); } }
注意:纹理资源能够用于任何着色器,好比顶点着色器、几何着色器或者像素着色器,而咱们暂时只将他应用于像素着色器。