DirectX12 3D 游戏开发与实战第九章内容(上)

仅供个人学习使用,请勿转载。

9、纹理贴图

学习目标:

  1. 学习如何将局部纹理映射到网格三角形上
  2. 探究如何创建和启用纹理
  3. 学会如何通过纹理过滤来创建更加平滑的图像
  4. 探索如何使用寻址模式来进行多次纹理贴图
  5. 探索如何将多个纹理进行组合,从而创建出新的纹理和特效
  6. 学习如何通过纹理动画来创建一些基本效果

9.1、纹理与资源的回顾

我们在第四章的时候就开始使用纹理了。特别是深度缓冲区和后台缓冲区,他们都是通过ID3D12Resource接口来表示的。为了便于参考,我们将在这一节回顾一些和纹理相关的知识。

2D纹理是一种由特定数据元素所构成的矩阵,它的用处之一便是存储2D图像数据,纹理中的每一个元素都对应着像素的颜色。但是存储2D图像数据并不是它唯一的用途,比如在法线贴图中(后面会介绍),每一个纹理元素存储的是一个3D向量而不是颜色数据。纹理就类似于由数据元素构成的1D、2D、3D数组,无论是1D还是2D纹理,都是用泛型接口ID3D12Resource表示的。

纹理不同于缓冲区资源,缓冲区资源仅仅存储数据数组,而纹理可以拥有多个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这些资源将被如何使用(我们将资源绑定到渲染流水线的哪一个阶段)

9.2、纹理坐标

Direct3D所采用的纹理坐标系,是指由图像水平正方向u轴和指向图像垂直正方向的v轴所组成的(u轴正方向一般是水平向右,v轴正方向一般是垂直向下)。u和v的取值范围为0 - 1之间,坐标(u,v)标定的是一种称为纹素的纹理元素。因为对纹理坐标进行了归一化处理,所以Direct3D的工作可以摆脱具体纹理尺寸的影响。

设A、B、C为3D三角形的3个顶点,Q为3D三角形内的任意一点,他们分别对应于纹理坐标A1、B1、C1和Q1。则对于3D三角形上的任意一点,我们都可以通过于3D三角形坐标插值所用的相同参数s和t,对顶点纹理坐标进行线性插值求得。

\[Q = A + s(B - A) + t(C - A) \]

\[ Q1 = A1 + s(B1 - A1) + t(C1 - A1) \]

为了实现此计算过程,我们需要为顶点结构体添加一个纹理坐标以表示纹理上的点。这样一来,每一个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,
};

注意:我们可以将一个锐角三角形映射到一个直角三角形中,不过我们要先进行拉伸操作,这会使贴图效果并不令人满意,所以我们一般不做这种反人类的映射,除非贴图师希望获得拉伸的效果故意而为之。

9.3、纹理数据源

贴图师通常会使用一些图像编辑器为游戏制作纹理,最后将他们保存为某种格式的图像文件,比如BMP、DDS、TGA或者PNG等等,游戏应用程序会在加载期间将图像文件载入ID3D12Resource对象,对于实时图像应用程序来说,DDS图像文件格式是最佳的选择。

9.3.1、DDS格式概述

DDS对于3D图形来说是一种理想的格式,因为它本质是一种针对GPU而专门设计的一种图像格式,DDS纹理满足用于3D图形开发的以下特征:

  1. mipmap
  2. GPU能自行解压的压缩格式
  3. 纹理数组
  4. 立方体贴图
  5. 立体纹理

DDS格式能够支持不同的像素格式,像素格式由枚举类型DXGI_FORMAT中的成员表示,但是并不是所有的格式都适用于DDS纹理,非压缩图像数据一般会采用下列格式:

  1. DXGI_FORMAT_B8G8R8A8_UNORM或者DXGI_FORMAT_B8G8R8X8_UNORM:适用于低动态范围(low dynamic range)图像
  2. DXGI_FORMAT_R16G16B16A16_FLOAT:适用于高动态范围(high dynamic range)图像

随着虚拟场景中纹理数量的大量增长,对于GPU显存的需求也快速增加(所有的纹理一般都存放在显存中)。为了缓解显存的压力,我们会采用压缩纹理格式,这种格式的优点是可以让图像以压缩的形式存放在显存中,需要使用的时候对他们进行解压。这里就不列举常用的压缩纹理格式了,有兴趣可以自行百度。

9.3.2、创建DDS文件

下面介绍两种可以将常用的图像格式转换为DDS格式的方法:

1、使用Photoshop,Photoshop提供了一款可以将图像导出为DDS格式的插件,这里不过多介绍了

2、使用texconv的命令行工具,该工具可以将传统的图像格式转变为DDS文件,而且还可以调整图像大小,改变像素格式、生成mipmap等等。我们可以在https://directxtex.codeplex.com/wikipage?title=Texassemble&referringTitle=Texconv找到它的文档和下载链接。

9.4创建以及启用纹理

9.4.1、加载DDS文件

/*
**	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));

9.4.2、着色器资源视图堆

创建了纹理资源之后,我们还需要为纹理资源创建一个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)));

9.4.3、创建着色器资源视图描述符

创建描述符堆之后,我们便可以创建真正的描述符了。我们通过填写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);

9.4.4、将纹理绑定到渲染流水线

在前面的演示程序中,我们在每次绘制调用的时候所制定的材质都是由材质常量缓冲区来进行更新的。这就意味着在绘制调用的时候,我们将不能动态的指定每一个像素的数据。而纹理映射技术的想法就是使用纹理贴图(texturemap)来取代材质常量缓冲区以获取材质数据,这将使每个像素的数据都是灵活多变的。

在本节中,我们将添加漫反射反照率纹理图(diffuse albedo texture map)来指定漫反射反照率分量。影响材质的两个数值gFresnelR0gRoughness将继续由材质常量缓冲区来指定。尽管我们添加了漫反射反照率纹理图,我们仍需要保留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);
    }
}

注意:纹理资源可以用于任何着色器,比如顶点着色器、几何着色器或者像素着色器,而我们暂时只将他应用于像素着色器。

posted @ 2019-11-29 00:25  风雅yaya  阅读(741)  评论(0编辑  收藏  举报