DirectX12 3D 遊戲開發與實戰第九章內容(上)

僅供我的學習使用,請勿轉載。c++

九、紋理貼圖

學習目標:

  1. 學習如何將局部紋理映射到網格三角形上
  2. 探究如何建立和啓用紋理
  3. 學會如何經過紋理過濾來建立更加平滑的圖像
  4. 探索如何使用尋址模式來進行屢次紋理貼圖
  5. 探索如何將多個紋理進行組合,從而建立出新的紋理和特效
  6. 學習如何經過紋理動畫來建立一些基本效果

9.一、紋理與資源的回顧

咱們在第四章的時候就開始使用紋理了。特別是深度緩衝區和後臺緩衝區,他們都是經過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這些資源將被如何使用(咱們將資源綁定到渲染流水線的哪個階段)佈局

9.二、紋理座標

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,
};

注意:咱們能夠將一個銳角三角形映射到一個直角三角形中,不過咱們要先進行拉伸操做,這會使貼圖效果並不使人滿意,因此咱們通常不作這種反人類的映射,除非貼圖師但願得到拉伸的效果故意而爲之。

9.三、紋理數據源

貼圖師一般會使用一些圖像編輯器爲遊戲製做紋理,最後將他們保存爲某種格式的圖像文件,好比BMP、DDS、TGA或者PNG等等,遊戲應用程序會在加載期間將圖像文件載入ID3D12Resource對象,對於實時圖像應用程序來講,DDS圖像文件格式是最佳的選擇。

9.3.一、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.二、建立DDS文件

下面介紹兩種能夠將經常使用的圖像格式轉換爲DDS格式的方法:

一、使用Photoshop,Photoshop提供了一款能夠將圖像導出爲DDS格式的插件,這裏不過多介紹了

二、使用texconv的命令行工具,該工具能夠將傳統的圖像格式轉變爲DDS文件,並且還能夠調整圖像大小,改變像素格式、生成mipmap等等。咱們能夠在https://directxtex.codeplex.com/wikipage?title=Texassemble&referringTitle=Texconv找到它的文檔和下載連接。

9.4建立以及啓用紋理

9.4.一、加載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.二、着色器資源視圖堆

建立了紋理資源以後,咱們還須要爲紋理資源建立一個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.三、建立着色器資源視圖描述符

建立描述符堆以後,咱們即可以建立真正的描述符了。咱們經過填寫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.四、將紋理綁定到渲染流水線

在前面的演示程序中,咱們在每次繪製調用的時候所制定的材質都是由材質常量緩衝區來進行更新的。這就意味着在繪製調用的時候,咱們將不能動態的指定每個像素的數據。而紋理映射技術的想法就是使用紋理貼圖(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);
    }
}

注意:紋理資源能夠用於任何着色器,好比頂點着色器、幾何着色器或者像素着色器,而咱們暫時只將他應用於像素着色器。

相關文章
相關標籤/搜索