1. 程式人生 > >Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第九章:貼圖

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第九章:貼圖

multi edit wrap ber oda asc 每一個 兩張 chm

原文:Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第九章:貼圖

代碼工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



學習目標

  1. 學習如何定義將一個紋理映射到一個三角形上;
  2. 學習如何創建和啟用紋理;
  3. 學習紋理如何被過濾後生產一個更加平滑的圖像;
  4. 學習如何將一個紋理通過地址模式展開多次;
  5. 學習如何將多個紋理合並成一個新貼圖和特殊效果;
  6. 學習一些基本的紋理動畫生成的效果。
    技術分享圖片


1 紋理和資源回顧

回顧我們已經在第四章使用過的紋理,深度緩存和後臺緩存都是一張2D紋理,它們由ID3D12Resource包含的值為D3D12_RESOURCE_DIMENSION_TEXTURE2D的D3D12_RESOURCE_DESC::Dimension屬性的接口來表示。
紋理和緩沖(buffer)不同,它不僅僅是保存數據,它還可以包含紋理細化等級(mipmap levels),並且GPU可以對它做特殊操作,比如應用過濾器(filters)和多重紋理映射。因為要支持這些特殊操作,所以它的格式有限制。格式通過DXGI_FORMAT枚舉來定義:
紋理格式可以定義完整的類型,比如:DXGI_FORMAT_R32G32B32_FLOAT,包含3個32位浮點數;也可以定義無類型格式,比如:DXGI_FORMAT_R8G8B8A8_TYPELESS。

根據DX11的文檔:定義完整類型的格式,可以進行運行時的優化。也就是說為了性能,只有當你真正需要無類型格式,否則都定義成完整類型的格式。

紋理可以綁定到渲染管線很多階段,重用的方式是做為一個渲染目標,或者著色器的資源。為了讓紋理用作渲染目標和著色器資源,我們需要創建2個描述:1、D3D12_DESCRIPTOR_HEAP_TYPE_RTV;2、D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV:

// Bind as render target.
CD3DX12_CPU_DESCRIPTOR_HANDLE rtv = …;
CD3DX12_CPU_DESCRIPTOR_HANDLE dsv = …;
cmdList->OMSetRenderTargets(1, &rtv, true, &dsv);

// Bind as shader input to root parameter.
CD3DX12_GPU_DESCRIPTOR_HANDLE tex = …;
cmdList->SetGraphicsRootDescriptorTable(rootParamIndex, tex);

資源描述本質上做了2件事情:1、告訴D3D該資源將如何被使用;2、如果資源定義時是無類型的,那麽需要定義它的類型。



2 紋理坐標

為了添加貼圖u,v坐標,修改定點結構如下:

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

我們可以將多個圖片放到一個大紋理中(texture atlas),然後應用於多個物體。這樣可以避免多次資源加載,減少DrawCall,優化性能。



3 紋理數據資源

對於實時圖形應用,DDS(DirectDraw Surface format)文件格式更好,GPU可以直接原生使用很多它的文件格式;並且它支持壓縮,可以讓GPU原生解壓縮。


3.1 DDS概覽

DDS是3D圖形更理想的格式是因為,它支持專門針對3D圖形設計的特殊格式和紋理類型。它本質上是針對GPU設計的圖像類型。比如DDS支持下面的特征:

  1. mipmaps;
  2. compressed formats that the GPU can natively decompress;
  3. texture arrays;
  4. cube maps;
  5. volume textures。

DDS文件支持DXGI_FORMAT枚舉中的部分類型(不是全部),對於非壓縮的數據可以使用:

  1. DXGI_FORMAT_B8G8R8A8_UNORM或者DXGI_FORMAT_B8G8R8X8_UNORM,用以low-dynamic-range圖像;
  2. DXGI_FORMAT_R16G16B16A16_FLOAT用以high-dynamic-range圖像。

為了讓紋理能夠快速應用,我們需要將它們放到GPU內存中。為了解決這些需求,D3D支持壓縮紋理格式:BC1, BC2, BC3, BC4, BC5, BC6, 和 BC7:

  1. BC1 (DXGI_FORMAT_BC1_UNORM):一個支持三個顏色通道和一個1bit alpha組件的壓縮格式;
  2. BC2 (DXGI_FORMAT_BC2_UNORM):一個支持三個顏色通道和一個4bit alpha組件的壓縮格式;
  3. BC3 (DXGI_FORMAT_BC3_UNORM):一個支持三個顏色通道和一個8bit alpha組件的壓縮格式;
  4. BC4 (DXGI_FORMAT_BC4_UNORM):一個支持完整一個顏色通道的壓縮格式;
  5. BC5 (DXGI_FORMAT_BC5_UNORM):一個支持2個完整顏色通道的壓縮格式;
  6. BC6 (DXGI_FORMAT_BC6_UF16):一個支持壓縮HDR圖像數據的壓縮格式;
  7. BC7 (DXGI_FORMAT_BC7_UNORM):一個支持高質量RGBA的壓縮格式,它主要用以減少壓縮法相貼圖導致的錯誤。

一個壓縮的紋理只能用以著色器輸入參數,不能用以渲染目標。
因為塊壓縮算法是以4x4像素塊來工作的,所以紋理的大小必須是4的倍數。

再次聲明,使用這個格式的好處是它們可以壓縮保存在GPU內存,並且當要使用的時候可以直接被GPU解壓縮;另一個好處是可以節省你的硬盤空間。


3.2 創建DDS文件

下面是兩種可以將傳統文件(.bmp .png等)轉換成DDS文件的方法:

  1. NVIDIA有一個PS的插件可以支持在PS中導出DDS文件:https://developer.nvidia.com/nvidiatexture-tools-adobe-photoshop,它可以支持設置DXGI_FORMAT格式和生成mipmaps;
  2. 微軟提供了一個叫texconv的命令行工具,可以轉換成DDS文件。它還可以用來改變圖像大小,修改像素格式生成mipmaps等,你可以找到對應文檔和下載鏈接在:https://directxtex.codeplex.com/wikipage?title=Texconv&referringTitle=Documentation

下面的命令就是一個例子,輸入一個bricks.bmp文件,導出一個bricks.dds文件並設置格式為BC3_UNORM並且創建10 mipmaps:

texconv -m 10 -f BC3_UNORM treeArray.dds

微軟提供了另一個叫texassemble的命令行工具,可以用來創建保存了texture arrays, volume maps, and cube maps的DDS文件,它的文檔和下載鏈接:https://directxtex.codeplex.com/wikipage?title=Texassemble&referringTitle=Documentation

VS2015有一個內置的圖像編輯器可以支持DDS文件。你可以直接拖拽DDS文件到VS中查看。



4 創建和使用一個紋理


4.1 加載DDS文件

微軟提供了一個輕量級源代碼來加載DDS文件:
https://github.com/Microsoft/DirectXTK/wiki/DDSTextureLoader
但是在寫本書的時候,這個代碼只能支持DX11。我們需要修改DDSTextureLoader.h/.cpp文件來支持DX12:

HRESULT DirectX::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);
  1. device:指向要創建紋理的D3D設備;
  2. cmdList:向GPU提交命令的命令列表;
  3. szFileName:需要加載的文件名稱;
  4. texture:返回加載好數據的紋理資源;
  5. textureUploadHeap:返回一個使用為將資源賦值到默認堆的上傳堆的紋理資源,這個資源在GPU指向完命令前不能被銷毀。

為了創建加載一個叫WoodCreate01.dds的紋理,我們可以這樣寫:

struct Texture
{
	// Unique material name for lookup.
	std::string Name;
	std::wstring Filename;
	Microsoft::WRL::ComPtr<ID3D12Resource> Resource = nullptr;
	Microsoft::WRL::ComPtr<ID3D12Resource> UploadHeap = nullptr;
};

auto woodCrateTex = std::make_unique<Texture>();
woodCrateTex->Name = "woodCrateTex";
woodCrateTex->Filename = L"Textures/WoodCrate01.dds";

ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(
	md3dDevice.Get(), mCommandList.Get(),
	woodCrateTex->Filename.c_str(),
	woodCrateTex->Resource, 
	woodCrateTex->UploadHeap));

4.2 SRV堆

當一個紋理資源被創建以後,我們需要創建一個可以讓我們把它設置到根簽名參數槽來讓著色器程序使用的SRV描述。為了達到這個目的,我們需要先使用ID3D12Device::CreateDescriptorHeap創建一個描述堆來保存SRV描述。下面的代碼創建了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)));

4.3 創建SRV描述

當我們創建後SRV對後,我們需要創建真正的描述。一個SRV描述是通過填D3D12_SHADER_RESOURCE_VIEW_DESC對象來創建:

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
{
	UINT MostDetailedMip;
	UINT MipLevels;
	UINT PlaneSlice;
	FLOAT ResourceMinLODClamp;
} D3D12_TEX2D_SRV;

對於2D紋理,我們只關心共用體中的3D12_TEX2D_SRV部分:

  1. Format:資源的格式:如果在創建的時候設置的是有類型的,直接設置為DXGI_FORMAT枚舉中的類型;如果在創建的時候設置的是無類型的,那麽必須設置成DXGI_FORMAT枚舉中有類型的類型。
  2. ViewDimension:我們使用的是2D紋理,所以設置成D3D12_SRV_DIMENSION_TEXTURE2D,其他格式包括TEXTURE1D、TEXTURE3D和TEXTURECUBE;
  3. Shader4ComponentMapping:當一個紋理映射到著色器程序中時,它會返回一個紋理數據的vector,這個值可以用來重新排序向量的組件;本書中直接設置為D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,代表我們不重置4個組件的順序。
  4. MostDetailedMip:指定最多細節的mipmap等級的索引,範圍是從0到MipCount-1;
  5. MipLevels:mipmap levels的數量,從MostDetailedMip開始。該值和MostDetailedMip可以定義mipmap等級的子區間,設置-1就是從MostDetailedMip減少到最少mipmap等級;
  6. PlaneSlice:平面的索引;
  7. ResourceMinLODClamp:指定可以訪問的最小的mipmap等級,0.0代表所有的都可以訪問,3.0代表從等級3.0到MipCount-1可以訪問。

下面的代碼為3個資源創建了描述:

// Suppose the following texture resources are already created.
// ID3D12Resource* bricksTex;
// ID3D12Resource* stoneTex;
// ID3D12Resource* tileTex;
// Get pointer to the start of the heap.
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;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = bricksTex->GetDesc().MipLevels;
srvDesc.Texture2D.ResourceMinLODClamp = 0.0f;

md3dDevice->CreateShaderResourceView(bricksTex.Get(), &srvDesc, hDescriptor);

// offset to next descriptor in heap
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = stoneTex->GetDesc().Format;
srvDesc.Texture2D.MipLevels = stoneTex->GetDesc().MipLevels;
md3dDevice->CreateShaderResourceView(stoneTex.Get(), &srvDesc, hDescriptor);

// offset to next descriptor in heap
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
srvDesc.Format = tileTex->GetDesc().Format;
srvDesc.Texture2D.MipLevels = tileTex->GetDesc().MipLevels;
md3dDevice->CreateShaderResourceView(tileTex.Get(), &srvDesc, hDescriptor);

4.4 綁定紋理到渲染管線

目前為止,我們通過逐繪制調用更新材質常量緩沖來指定材質。這代表當前繪制調用中幾何體都會使用一種材質。這個就限制了我們不能逐像素的改變材質,所以我們的場景就缺少細節。解決想法是通過紋理映射的紋理貼圖來獲取材質數據而不是常量緩沖。這就可以進行逐像素的改變材質來增加細節和真實感。
本章中,我們增加了一個diffuse albedo紋理貼圖來定義材質的diffuse albedo組件。FresnelR0和Roughness材質值將繼續逐繪制調用的在常量緩沖中更新,但是在Normal Mapping章中,我們將介紹如何通過紋理貼圖來進行逐像素定義。我們將合並紋理的diffuse albedo值在像素著色器中:

// Get diffuse albedo at this pixel from texture.
float4 texDiffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC);

// Multiple texture sample with constant buffer albedo.
float4 diffuseAlbedo = texDiffuseAlbedo * gDiffuseAlbedo;

通常情況下我們設置DiffuseAlbedo=(1,1,1,1),不去改變texDiffuseAlbedo。但是有些特殊效果需要調整這個值(比如,磚塊調整成淡藍色)。
我們在材質中增加一個索引,代表對應SRV在堆中的索引:

struct Material
{
	…
	// Index into SRV heap for diffuse texture.
	int DiffuseSrvHeapIndex = -1;
	…
};

然後假設根簽名已經定義了一個SRV綁定到0槽上,我們就可以使用紋理繪制物體通過下面的代碼:

void CrateApp::DrawRenderItems(
	ID3D12GraphicsCommandList* cmdList,
	const std::vector<RenderItem*>& ritems)
{
	UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
	UINT matCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(MaterialConstants));
	
	auto objectCB = mCurrFrameResource->ObjectCB->Resource();
	auto matCB = mCurrFrameResource->MaterialCB->Resource();
	
	// For each render item…
	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;
			
		**cmdList->SetGraphicsRootDescriptorTable(0, tex);**
		cmdList->SetGraphicsRootConstantBufferView(1, objCBAddress);
		cmdList->SetGraphicsRootConstantBufferView(3, matCBAddress);
		
		cmdList->DrawIndexedInstanced(ri->IndexCount,
			1, ri->StartIndexLocation,
			ri->BaseVertexLocation, 0);
	}
}

紋理圖集可以增加性能,因為可以繪制多個幾何體在同一個繪制調用中。即使在DX12中繪制調用的開銷已經明顯較少了,但是減少繪制調用還是非常值得的。



5 濾波器


5.1 放大

放大有兩種方式:常量差值(下圖a)和線性差值(下圖b)。
技術分享圖片
2D線性差值叫做雙線性差值,如下圖所示:
技術分享圖片
下圖展示了使用真實數據資源,兩種差值方式的對比,線性差值要更平滑一些,但是比起原本的資源,看起來效果也不是那麽好:
技術分享圖片
常量差值也叫點濾波器(point filtering),線性差值也叫線性濾波器(linear filtering),它們是D3D的術語。


5.2 縮小

常量和線性濾波器依然適用於縮小。還有一種叫做多級漸進紋理的技術可以高效模擬縮小操作,但是會占用更多的內存。在運行時,程序員需要通過圖形硬件會基於mipmap做兩件不同的事情:

  1. 為紋理選擇更適用於當前屏幕分辨率的mipmap等級,應用需要的常量或者線性插值。你只需要選擇最接近的mipmap等級-----這個叫點濾波器(point filtering)mipmaps;
  2. 為紋理選擇2個更適用於當前屏幕分辨率的mipmap等級,然後應用需要的常量或者線性插值,計算出兩個紋理的顏色,然後線性插值這兩個紋理的顏色-----這個叫線性濾波器(linear filtering)mipmaps;
    技術分享圖片
    利用PS、texconv或者其它軟件生成mipmap chain的時候不能保證能夠保留重要的細節,有時候需要美術在縮小的紋理上重新處理細節。

5.3 各向異性濾波器

這個濾波器幫助較少當幾何體的法向量和攝像機看向的方向夾角變大時,產生的紋理變形和扭曲。這個濾波器的運算成本是最大的。
技術分享圖片



6 地址模式

D3D允許我們通過4種方式來擴展紋理獲取顏色的方式:包裹(wrap)、邊緣色彩(border color)、clamp和鏡像(mirror)。

  1. 包裹擴展是讓紋理方程在每一個整數那裏重復圖像:
    技術分享圖片
  2. 邊緣色彩擴展是然超出[0,1]2[0, 1]^2範圍的地方使用程序員設置的顏色:
    技術分享圖片
  3. clamp擴展是超出[0,1]2[0, 1]^2範圍的地方保留最接近紋理位置的顏色:
    技術分享圖片
  4. 鏡像擴展是在每一個整數位置那裏鏡像圖像:
    技術分享圖片

地址模式的默認值是wrap。貼圖如果是無縫的,wrap就可以得到很好的效果。
地址模式在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;


7 采樣對象(SAMPLER OBJECTS)

使用什麽濾波器和地址模式是通過采樣對象來定義的,一個應用程序一般都需要多個采樣對象。


7.1 創建采樣器

為了綁定采樣器到著色器程序,我們需要描述到采樣對象,下面的代碼是一個例子:

CD3DX12_DESCRIPTOR_RANGE descRange[3];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 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_

如果我們要設置一個采樣器描述,我們需要一個采樣器堆。采樣器堆是通過填充一個D3D12_DESCRIPTOR_HEAP_DESC結構示例也定義的:

D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER:
D3D12_DESCRIPTOR_HEAP_DESC descHeapSampler = {};
descHeapSampler.NumDescriptors = 1;
descHeapSampler.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
descHeapSampler.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;

ComPtr<ID3D12DescriptorHeap> 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_SAMPLER_DESC;
  1. Filter:D3D12_FILTER枚舉中的一個類型;
  2. AddressU:水平方向的地址模式;
  3. AddressV:豎直方向的地址模式;
  4. AddressW:深度方向上的地址模式(3D紋理);
  5. MipLODBias:偏移的mipmap等級,0.0位不偏移;
  6. MaxAnisotropy:最大的各向異性值,在1~16之間(包含1和16),這個只對D3D12_FILTER_ANISOTROPIC和D3D12_FILTER_COMPARISON_ANISOTROPIC有效,越大的值開銷越大,但是可以給出更好的效果。
  7. ComparisonFunc:高級應用,可以實現一些特殊效果,比如陰影貼圖,目前就直接設置成D3D12_COMPARISON_FUNC_ALWAYS;
  8. BorderColor:地址模式為D3D12_TEXTURE_ADDRESS_MODE_BORDER時的四周顏色;
  9. MinLOD:最小mipmap等級;
  10. MaxLOD:最大mipmap等級;

下面是一些常用的D3D12_FILTER類型:

  1. D3D12_FILTER_MIN_MAG_MIP_POINT:點濾波器,和點濾波器mipmap;
  2. D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT:雙線性濾波器,和點濾波器mipmap;
  3. D3D12_FILTER_MIN_MAG_MIP_LINEAR:雙線性濾波器,和在兩個最接近的mipamap等級之間的線性濾波器,也叫三線性濾波器。
  4. D3D12_FILTER_ANISOTROPIC:各向異性濾波器。

你可以從這些例子中找到其他的排列,或者查看SDK文檔中的D3D12_FILTER枚舉。
下面的代碼是一個如何創建采樣器描述的例子:

D3D12_SAMPLER_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());

下面的代碼展示了如何綁定一個采樣器描述到根簽名參數槽中:

commandList->SetGraphicsRootDescriptorTable(1,
	samplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart());

7.2 靜態采樣器

一個應用程序通常只使用少數采樣器,所以,D3D提供了一個特殊捷徑來定義一個采樣器數組,並且不需要再創建采樣器堆。CD3DX12_ROOT_SIGNATURE_DESC類的Init函數有兩個參數,可以讓你定義一個叫做靜態采樣器的數組。靜態采樣器通過D3D12_STATIC_SAMPLER_DESC結構來描述,這個結構和D3D12_SAMPLER_DESC結構非常相似,但是有下面的一些例外:

  1. 邊緣顏色有一些限制,只能使用下面的值:
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;
  1. 它包含了其他內容來定義著色器寄存器,寄存器空間和著色器可見度(正常情況下需要定義在堆中)。

並且你只能定義2032個靜態采樣器,一般情況下應用程序也不需要這麽多;如果你真需要更多,可以通過采樣器堆來使用非靜態采樣器。
在我們的Demo中我們使用靜態采樣器,下面的代碼就是使用靜態采樣器的例子。我們會定義很多靜態采樣器,但是Demo中可能不需要用到,對於多出來的采樣器,不會造成太多的問題:

std::array<const CD3DX12_STATIC_SAMPLER_DESC, 6> TexColumnsApp::GetStaticSamplers()
{
	// Applications usually only need a handful of samplers. So just define them
	// all up front and keep them available as part of the root signature.
	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 TexColumnsApp::BuildRootSignature()
{
	CD3DX12_DESCRIPTOR_RANGE texTable;
	texTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
	
	// Root parameter can be a table, root descriptor or root constants.
	CD3DX12_ROOT_PARAMETER slotRootParameter[4];
	slotRootParameter[0].InitAsDescriptorTable(1, &texTable, D3D12_SHADER_VISIBILITY_PIXEL);
	slotRootParameter[1].InitAsConstantBufferView(0);
	slotRootParameter[2].InitAsConstantBufferView(1);
	slotRootParameter[3].InitAsConstantBufferView(2);
	auto staticSamplers = GetStaticSamplers();
	
	// A root signature is an array of root parameters.
	CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(4,
		slotRootParameter,
		(UINT)staticSamplers.size(),
		staticSamplers.data(),
		D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_

	// create a root signature with a single slot which points to a
	// descriptor range consisting of a single constant buffer
	ComPtr<ID3DBlob> serializedRootSig = nullptr;
	ComPtr<ID3DBlob> 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())));
}


8 在著色器中的紋理采樣

一個紋理對象在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
{
	float4 diffuseAlbedo =
	gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) *
	gDiffuseAlbedo;
	…

這個方法返回了差值後的顏色值。



9 板條箱Demo

現在我們復習一下,添加紋理到一個箱子上的主要的點。


9.1 定義紋理坐標

GeometryGenerator::CreateBox方法創建了紋理坐標,所以紋理可以覆蓋到整個箱子上。

GeometryGenerator::MeshData
GeometryGenerator::CreateBox( float width, float height, float depth, uint32 numSubdivisions)
{
	MeshData meshData;
	Vertex v[24];
	float w2 = 0.5f*width;
	float h2 = 0.5f*height;
	float d2 = 0.5f*depth;
	
	// Fill in the front face vertex data.
	v[0] = Vertex(-w2, -h2, -d2, …, 0.0f, 1.0f);
	v[1] = Vertex(-w2, +h2, -d2, …, 0.0f, 0.0f);
	v[2] = Vertex(+w2, +h2, -d2, …, 1.0f, 0.0f);
	v[3] = Vertex(+w2, -h2, -d2, …, 1.0f, 1.0f);
	
	// Fill in the back face vertex data.
	v[4] = Vertex(-w2, -h2, +d2, …, 1.0f, 1.0f);
	v[5] = Vertex(+w2, -h2, +d2, …, 0.0f, 1.0f);
	v[6] = Vertex(+w2, +h2, +d2, …, 0.0f, 0.0f);
	v[7] = Vertex(-w2, +h2, +d2, …, 1.0f, 0.0f);
	
	// Fill in the top face vertex data.
	v[8] = Vertex(-w2, +h2, -d2, …, 0.0f, 1.0f);
	v[9] = Vertex(-w2, +h2, +d2, …, 0.0f, 0.0f);
	v[10] = Vertex(+w2, +h2, +d2, …, 1.0f, 0.0f);
	v[11] = Vertex(+w2, +h2, -d2, …, 1.0f, 1.0f);

9.2 創建紋理

我們在初始化的時候創建紋理:

// Helper structure to group data related to the texture.
struct Texture
{
	// Unique material name for lookup.
	std::string Name;
	std::wstring Filename;
	Microsoft::WRL::ComPtr<ID3D12Resource> Resource = nullptr;
	Microsoft::WRL::ComPtr<ID3D12Resource> UploadHeap = nullptr;
};

std::unordered_map<std::string, std::unique_ptr<Texture>> mTextures;
void CrateApp::LoadTextures()
{
	auto woodCrateTex = std::make_unique<Texture>();
	woodCrateTex->Name = "woodCrateTex";
	woodCrateTex->Filename = L"Textures/WoodCrate01.dds";
	ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(md3dDevice.mCommandList.Get(), 
		woodCrateTex->Filename.c_str(),
		woodCrateTex->Resource, woodCrateTex->UploadHeap));
	mTextures[woodCrateTex->Name] = std::move(woodCrateTex);
}

9.3 設置紋理

當一個紋理創建好,並且一個SRV被創建到一個描述堆的時候,就可以綁定紋理到渲染管線:

// Get SRV to texture we want to bind.
CD3DX12_GPU_DESCRIPTOR_HANDLE tex(
	mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
	
tex.Offset(ri->Mat->DiffuseSrvHeapIndex, mCbvSrvDescriptorSize);
…
// Bind to root parameter 0. The root parameter description specifies which
// shader register slot this corresponds to.
cmdList->SetGraphicsRootDescriptorTable(0, tex);

9.4 更新HLSL

// Defaults for number of lights.
#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 structures and functions for lighting.
#include "LightingUtil.hlsl"

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

// Constant data that varies per frame.
cbuffer cbPerObject : register(b0)
{
	float4x4 gWorld;
	**float4x4 gTexTransform;**
};

// Constant data that varies per material.
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;
	
	// Indices [0, NUM_DIR_LIGHTS) are directional lights;
	// indices [NUM_DIR_LIGHTS,
	NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
	
	// indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS,
	//
	NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
	
	// are spot lights for a maximum of MaxLights per object.
	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;
	
	// Transform to world space.
	float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
	vout.PosW = posW.xyz;
	
	// Assumes nonuniform scaling; otherwise, need to use
	// inverse-transpose of world matrix.
	vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
	
	// Transform to homogeneous clip space.
	vout.PosH = mul(posW, gViewProj);
	
	**// Output vertex attributes for interpolation across triangle.
	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(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;**

	// Interpolating normal can unnormalize it, so renormalize it.
	pin.NormalW = normalize(pin.NormalW);
	
	// Vector from point being lit to eye.
	float3 toEyeW = normalize(gEyePosW - pin.PosW);
	
	// Light terms.
	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;
	
	// Common convention to take alpha from diffuse albedo.
	litColor.a = diffuseAlbedo.a;
	
	return litColor;
}


10 變換紋理

有兩個常量緩沖的變量我們目前還沒有討論:gTexTransform和gMatTransform。它們是頂點著色器用來變換紋理坐標的:

// Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, gMatTransform).xy;

紋理坐標表示2D紋理上的點,所以我們可以移動,旋轉,縮放它們。
使用4 x 4矩陣變換2D坐標,我們需要把坐標增加到4D:

vin.TexC ---> float4(vin.Tex, 0.0f, 1.0f)

變換過後,需要把坐標變回到2D:

vout.TexC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform).xy;

我們之所以分開為2個變換矩陣:gTexTransform和gMatTransform是因為有些時候材質需要變換紋理(比如流動的水流),但是有些時候紋理變換是物體對象的一個屬性。



11 具有紋理的山和水Demo

技術分享圖片


11.1 創建網格紋理坐標

根據下圖可以看出,網格的紋理坐標為:
技術分享圖片
技術分享圖片
創建代碼如下:

GeometryGenerator::MeshData GeometryGenerator::CreateGrid(float width, float depth, uint32 m, uint32 n)
{
	MeshData meshData;
	uint32 vertexCount = m*n;
	uint32 faceCount = (m-1)*(n-1)*2;
	float halfWidth = 0.5f*width;
	float halfDepth = 0.5f*depth;
	float dx = width / (n-1);
	float dz = depth / (m-1);
	**float du = 1.0f / (n-1);
	float dv = 1.0f / (m-1);**
	meshData.Vertices.resize(vertexCount);
	
	for(uint32 i = 0; i < m; ++i)
	{
		float z = halfDepth - i*dz;
		for(uint32 j = 0; j < n; ++j)
		{
			float x = -halfWidth + j*dx;
			meshData.Vertices[i*n+j].Position = XMFLOAT3(x, 0.0f, z);
			meshData.Vertices[i*n+j].Normal = XMFLOAT3(0.0f, 1.0f, 0.0f);
			meshData.Vertices[i*n+j].TangentU = XMFLOAT3(1.0f, 0.0f, 0.0f);
			
			**// Stretch texture over grid.
			meshData.Vertices[i*n+j].TexC.x = j*du;
			meshData.Vertices[i*n+j].TexC.y = i*dv;**
		}
	}

11.2 紋理鋪展

為了將紋理鋪到整個網格,我們選擇地址模式和縮放紋理坐標:

void TexWavesApp::BuildRenderItems()
{
	auto gridRitem = std::make_unique<RenderItem> ();
	gridRitem->World = MathHelper::Identity4x4();
	XMStoreFloat4x4(&gridRitem->TexTransform, XMMatrixScaling(5.0f, 5.0f, 1.0f));
	…
}

11.3 紋理動畫

為了讓水可以流動,我們添加一個AnimateMaterials函數,我們使用wrap地址模式在一個無縫紋理上。下面的代碼就是一個例子:

void TexWavesApp::AnimateMaterials(const GameTimer& gt)
{
	// Scroll the water material texture coordinates.
	auto waterMat = mMaterials["water"].get();
	float& tu = waterMat->MatTransform(3, 0);
	float& tv = waterMat->MatTransform(3, 1);
	
	tu += 0.1f * gt.DeltaTime();
	tv += 0.02f * gt.DeltaTime();
	
	if(tu >= 1.0f)
		tu -= 1.0f;
	if(tv >= 1.0f)
		tv -= 1.0f;
		
	waterMat->MatTransform(3, 0) = tu;
	waterMat->MatTransform(3, 1) = tv;
	
	// Material has changed, so need to update cbuffer.
	waterMat->NumFramesDirty = gNumFrameResources;
}


12 本章總結

  1. 紋理坐標用來將紋理上的三角形映射到3D三角形;
  2. 遊戲中最普遍的紋理創建方式是,美術通過PS等軟件創建好資源,然後遊戲中加載成ID3D12Resource對象;但是對於實時圖形應用程序,DDS格式文件更好,它支持多種GPU本地支持的格式,而且可以被GPU解壓縮;
  3. 有兩種方法可以將傳統圖像格式文件轉換為DDS文件:在圖像編輯器中導出DDS文件;使用微軟命令行工具:texconv。
  4. 我們可以通過CreateDDSTextureFromFile12方法創建紋理並加載硬盤上的文件,該方法實現在Common/DDSTextureLoader.h/.cpp文件中;
  5. GPU原生可以支持3種濾波器:放大,縮小和mipmap;
  6. 地址模式定義了D3D對超出[0, 1]範圍的紋理坐標如何處理;
  7. 紋理坐標可以被移動,縮放,旋轉,就類似於其他頂點。


13 練習題

1. 修改Crate Demo達到不同的尋址模式和濾波器效果:

代碼在https://github.com/jiabaodan/Direct12BookReadingNotes中的Chapter9_Exercises_1_CrateTexMode工程
技術分享圖片

修改renderitem中的TexTransform或者材質中的MatTransform:

// 對紋理放大5倍
XMStoreFloat4x4(&boxRitem->TexTransform, XMMatrixScaling(5.0f, 5.0f, 1.0f));

然後修改像素著色器中的采樣器

float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicClamp, pin.TexC) * gDiffuseAlbedo;

2. 使用DirectX Texture Tool創建mipmap,每個等級使用不同的顏色,然後在Crate Demo中增加控制距離,觀察在點和線性濾波器下的mipmap的變化:

代碼在https://github.com/jiabaodan/Direct12BookReadingNotes中的Chapter9_Exercises_2_CrateMipmap工程
技術分享圖片
根據要求創建好mipmap問題,然後加載進去即可

3&4. 用乘法合並兩張紋理,並旋轉:

代碼在https://github.com/jiabaodan/Direct12BookReadingNotes中的Chapter9_Exercises_4_CrateTexAnim工程
技術分享圖片
根簽名裏加個槽,然後把紋理放進去,然後修改Shader:

VS:
// 按照中心點旋轉
vin.TexC -= float2(0.5, 0.5);

// Output vertex attributes for interpolation across triangle.
   float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
   vout.TexC = mul(texC, gMatTransform).xy;

// 按照中心點旋轉
vout.TexC += float2(0.5, 0.5);

PS:
float4 diffuseAlbedo = (gDiffuseMapFront.Sample(gsamAnisotropicWrap, pin.TexC) 
	* gDiffuseMapBack.Sample(gsamAnisotropicWrap, pin.TexC)) * gDiffuseAlbedo;

6. 在之前的LitColumns Demo裏加上材質:

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第九章:貼圖