DirectX11 With Windows SDK--09 紋理映射與採樣器狀態

前言

在之前的DirectX SDK中,紋理的讀取使用的是D3DX11CreateShaderResourceViewFromFile函數,現在在Windows SDK中已經沒有這些函數,我們需要找到DDSTextureLoaderWICTextureLoader這兩個庫來讀取DDS位圖和WIC位圖

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

紋理座標系

紋理座標系和屏幕、圖片座標系的有些相似,它們的U軸都是水平朝右,V軸豎直向下。但是紋理的X和Y的取值範圍都爲[0.0, 1.0],分別映射到[0, Width][0, Height]

對於一個3D的三角形,通過給這三個頂點額外的紋理座標信息,那麼三個紋理座標就可以映射到紋理指定的某片三角形區域。

這樣的話已知三個頂點的座標p0,p1p2以及三個紋理座標q0,q1q2,就可以求出頂點座標映射與紋理座標的對應關係:

(x,y,z)=p0+s(p1p0)+t(p2p0)(x, y, z) = \mathbf{p_0} + s(\mathbf{p_1} - \mathbf{p_0}) + t(\mathbf{p_2} - \mathbf{p_0})

(u,v)=q0+s(q1q0)+t(q2q0)(u, v) = \mathbf{q_0} + s(\mathbf{q_1} - \mathbf{q_0}) + t(\mathbf{q_2} - \mathbf{q_0})

並且還需要滿足s>=0,t>=0,s+t<=1s >= 0, t >= 0, s + t <= 1

所以頂點結構體的內容會有所變化:

struct VertexPosNormalTex
{
	DirectX::XMFLOAT3 pos;
	DirectX::XMFLOAT3 normal;
	DirectX::XMFLOAT2 tex;
	static const D3D11_INPUT_ELEMENT_DESC inputLayout[3];
};

對應的每個輸入元素的描述爲:

const D3D11_INPUT_ELEMENT_DESC VertexPosNormalTex::inputLayout[3] = {
	{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
	{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0}
};

紋理讀取

DDS位圖和WIC位圖

DDS是一種圖片格式,是DirectDraw Surface的縮寫,它是DirectX紋理壓縮(DirectX Texture Compression,簡稱DXTC)的產物。由NVIDIA公司開發。大部分3D遊戲引擎都可以使用DDS格式的圖片用作貼圖,也可以製作法線貼圖。

WIC(Windows Imaging Component)是一個可以擴展的平臺,爲數字圖像提供底層API,它可以支持bmp、dng、ico、jpeg、png、tiff等格式的位圖。

DDSTextureLoader和WICTextureLoader庫

要使用這兩個庫,有兩種方案。

第一種:在DirectXTex中找到DDSTextureLoader文件夾和WICTextureLoader文件夾中分別找到對應的頭文件和源文件(不帶12的),並加入到你的項目中

第二種:將DirectXTK庫添加到你的項目中,這裏不再贅述

這之後就可以包含DDSTextureLoader.hWICTextureLoader.h進項目中了。

CreateDDSTextureFromFile函數–從文件讀取DDS紋理

現在讀取DDS紋理的操作變得更簡單了:

HRESULT CreateDDSTextureFromFile(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    const wchar_t* szFileName,              // [In]dds圖片文件名
    ID3D11Resource** texture,               // [Out]輸出一個指向資源接口類的指針,也可以填nullptr
    ID3D11ShaderResourceView** textureView, // [Out]輸出一個指向着色器資源視圖的指針,也可以填nullptr
    size_t maxsize = 0,                     // [In]忽略
    DDS_ALPHA_MODE* alphaMode = nullptr);  // [In]忽略

下面是一個調用的例子:

// 初始化木箱紋理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));

CreateWICTextureFromFile函數–從文件讀取WIC紋理

函數原型如下:

HRESULT CreateWICTextureFromFile(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    const wchar_t* szFileName,              // [In]wic所支持格式的圖片文件名
    ID3D11Resource** texture,               // [Out]輸出一個指向資源接口類的指針,也可以填nullptr
    ID3D11ShaderResourceView** textureView, // [Out]輸出一個指向着色器資源視圖的指針,也可以填nullptr
    size_t maxsize = 0);                     // [In]忽略

下面是一個調用的例子:

// 初始化火焰紋理
WCHAR strFile[40];
m_pFireAnims.resize(120);
for (int i = 1; i <= 120; ++i)
{
	wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i);
	HR(CreateWICTextureFromFile(m_pd3dDevice.Get(), strFile, nullptr, m_pFireAnims[i - 1].GetAddressOf()));
}

這裏我們只需要創建着色器資源視圖,而不是紋理資源。原因在後面會提到。

過濾器

圖片的放大

圖片在經過放大操作後,除了圖片原有的像素被拉伸,還需要對其餘空缺的像素位置選用合適的方式來進行填充。比如一個2x2位圖被拉伸成8x8的,除了角上4個像素,還需要對其餘60個像素進行填充。下面介紹幾種方法

常量插值法

對於2x2位圖,它的寬高表示範圍都爲[0,1],而8x8位圖的都爲[0,7],且只允許取整數。那麼對於放大後的像素點(1, 4)就會映射到(1/7, 4/7)上。

常量插值法的做法十分簡單粗暴,就是對X和Y值都進行四捨五入操作,然後取鄰近像素點的顏色。比如對於映射後的值如果落在[20.5, 21.5)的範圍,最終都會取21。根據上面的例子,最終會落入到像素點(0, 1)上,然後取該像素點的顏色。

線性插值法

現在只討論一維情況,已知第20個像素點的顏色p0和第21個像素點的顏色p1,並且經過拉伸放大後,有一個像素點落在範圍(20, 21)之間,我們就可以使用線性插值法求出最終的顏色(t取(0,1)):

p=tp1+(1t)p0\mathbf{p} = t\mathbf{p_1} + (1 - t)\mathbf{p_0}

對於二維情況,會有三種使用線性插值法的情況:

  1. X方向使用常量插值法,Y方向使用線性插值法
  2. X方向使用線性插值法,Y方向使用常量插值法
  3. X和Y方向均使用線性插值法

下圖展示了雙線性插值法的過程,已知4個相鄰像素點,當前採樣的紋理座標在這四個點內,則首先根據x方向的紋理座標進行線性插值,然後根據y方向的紋理座標再進行一遍線性插值:

而下圖則演示了兩種插值法的效果,其中左邊使用了常量插值法,右邊使用了二維線性插值法

圖片的縮小

圖片在經過縮小操作後,需要拋棄掉一些像素。但顯然每次繪製都按實際寬高來進行縮小會對性能有很大影響。 在d3d中可以使用mipmapping技術,以額外犧牲一些內存代價的方式來獲得高效的擬合效果。

這裏估計使用的是金字塔下采樣的原理。一張256x256的紋理,通過不斷的向下採樣,可以獲得256x256、128x128、64x64…一直到1x1的一系列位圖,這些位圖構建了一條mipmap鏈,並且不同的紋理標註有不同的mipmap等級

其中mipmap等級爲0的紋理即爲原來的紋理,等級爲1的紋理所佔內存爲等級爲0的1/4,等級爲2的紋理所佔內存爲等級爲1的1/4…以此類推我們可以知道包含完整mipmap的紋理佔用的內存空間不超過原來紋理的

limn+1(1(14)n)114=1114=43\lim_{n \to +\infty} \frac{1(1-(\frac{1}{4})^n)}{1-\frac{1}{4}} = \frac{1}{1-\frac{1}{4}} = \frac{4}{3}

接下來會有兩種情況:

  1. 選取mipmap等級對應圖片和縮小後的圖片大小最接近的一張,然後進行線性插值法或者常量插值法,這種方式叫做點過濾(point filtering)
  2. 選取兩張mipmap等級相鄰的圖片,使得縮小後的圖片大小在那兩張位圖之間,然後對這兩張位圖進行常量插值法或者線性插值法分別取得顏色結果,最後對兩個顏色結果進行線性插值,這種方式叫做線性過濾(linear filtering)。

各向異性過濾

Anisotropic Filtering可以幫助我們處理那些不與屏幕平行的平面,需要額外使用平面的法向量和攝像機的觀察方向向量。雖然使用該種過濾器會有比較大的性能損耗,但是能誕生出比較理想的效果。

下面左圖使用了線性過濾法,右邊使用的是各向異性過濾,可以看到頂面紋理比左邊的更加清晰

對紋理進行採樣

所謂採樣,就是根據紋理座標取出紋理對應位置最爲接近的像素,在HLSL的寫法如下:

g_Tex.Sample(g_SamLinear, pIn.Tex);

但大多數時候繪製出的紋理會比所用的紋理大或小,這樣就還涉及到了採樣器使用什麼方式(如常量插值法、線性插值法、各向異性過濾)來處理圖片放大、縮小的情況。

HLSL代碼的變動

Basic.hlsli代碼如下:

#include "LightHelper.hlsli"

Texture2D gTex : register(t0);
SamplerState gSamLinear : register(s0);


cbuffer VSConstantBuffer : register(b0)
{
    matrix g_World; 
    matrix g_View;  
    matrix g_Proj;  
    matrix g_WorldInvTranspose;
}

cbuffer PSConstantBuffer : register(b1)
{
    DirectionalLight g_DirLight[10];
    PointLight g_PointLight[10];
    SpotLight g_SpotLight[10];
    Material g_Material;
	int g_NumDirLight;
	int g_NumPointLight;
	int g_NumSpotLight;
    float g_Pad1;

    float3 g_EyePosW;
    float g_Pad2;
}


struct VertexPosNormalTex
{
	float3 PosL : POSITION;
    float3 NormalL : NORMAL;
	float2 Tex : TEXCOORD;
};

struct VertexPosTex
{
    float3 PosL : POSITION;
    float2 Tex : TEXCOORD;
};

struct VertexPosHWNormalTex
{
	float4 PosH : SV_POSITION;
    float3 PosW : POSITION;     // 在世界中的位置
    float3 NormalW : NORMAL;    // 法向量在世界中的方向
	float2 Tex : TEXCOORD;
};

struct VertexPosHTex
{
    float4 PosH : SV_POSITION;
    float2 Tex : TEXCOORD;
};

Basic_VS_2D.hlsl的代碼:

// Basic_VS_2D.hlsl
#include "Basic.hlsli"

// 頂點着色器(2D)
VertexPosHTex VS_2D(VertexPosTex vIn)
{
    VertexPosHTex vOut;
    vOut.PosH = float4(vIn.PosL, 1.0f);
    vOut.Tex = vIn.Tex;
    return vOut;
}

Basic_PS_2D.hlsl的代碼:

// Basic_PS_2D.hlsl
#include "Basic.hlsli"

// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
    return g_Tex.Sample(g_SamLinear, pIn.Tex);
}

Basic_VS_3D.hlsl的代碼:

// Basic_VS_3D.hlsl
#include "Basic.hlsli"

// 頂點着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    matrix viewProj = mul(g_View, g_Proj);
    float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);

    vOut.PosH = mul(posW, viewProj);
    vOut.PosW = posW.xyz;
    vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
    vOut.Tex = vIn.Tex;
    return vOut;
}


Basic_PS_3D.hlsl的代碼:

// Basic_PS_3D.hlsl
#include "Basic.hlsli"

// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
    // 標準化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 頂點指向眼睛的向量
    float3 toEyeW = normalize(g_EyePosW - pIn.PosW);

    // 初始化爲0 
    float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
    int i;
	// 強制展開循環以減少指令數
	[unroll]
    for (i = 0; i < g_NumDirLight; ++i)
    {
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
	[unroll]
    for (i = 0; i < g_NumPointLight; ++i)
    {
        ComputePointLight(g_Material, g_PointLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
	[unroll]
    for (i = 0; i < g_NumSpotLight; ++i)
    {
        ComputeSpotLight(g_Material, g_SpotLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    

    float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);
    float4 litColor = texColor * (ambient + diffuse) + spec;
    litColor.a = texColor.a * g_Material.Diffuse.a;
	
    return litColor;
}

其中Texture2D類型保存了2D紋理的信息,在這是全局變量。而register(t0)對應起始槽索引0.

SamplerState類型確定採樣器應如何進行採樣,同樣也是全局變量,register(s0)對應起始槽索引0.

上述兩種變量都需要在C++應用層中初始化和綁定後才能使用。

Texture2D類型擁有Sample方法,需要提供採樣器狀態和2D紋理座標方可使用,然後返回一個包含RGBA信息的float4向量。

[unroll]屬性用於展開循環,避免不必要的跳轉,但可能會產生大量的指令

除此之外,上面的HLSL代碼允許每種燈光最多10盞,然後還提供了2D和3D版本的頂點/像素着色器供使用。

注意Basic.hlsliLightHelper.hlsli是不參與生成的。其餘着色器文件需要按照第2章的方式去設置好。

ID3D11DeviceContext::*SSetShaderResources方法–設置着色器資源

需要注意的是,紋理並不能直接綁定到着色器中,需要爲紋理創建對應的着色器資源視圖才能夠給着色器使用。上面打*意味着渲染管線的所有可編程着色器階段都有該方法。

此外,着色器資源視圖不僅可以綁定紋理資源,還可以綁定緩衝區資源。有關緩衝區資源綁定到着色器資源視圖的應用,我們留到後面的章節再講。

目前在DDSTextureLoaderWICTextureLoader中,我們只需要用到紋理的着色器資源。這裏以ID3D11DeviceContext::PSSetShaderResources爲例:

void ID3D11DeviceContext::PSSetShaderResources(
	UINT StartSlot,	// [In]起始槽索引,對應HLSL的register(t*)
	UINT NumViews,	// [In]着色器資源視圖數目
	ID3D11ShaderResourceView * const *ppShaderResourceViews	// [In]着色器資源視圖數組
);

然後調用方法如下:

m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());

這樣在HLSL裏對應regisgter(t0)g_Tex存放的就是木箱表面的紋理了。

ID3D11Device::CreateSamplerState方法–創建採樣器狀態

在C++代碼層中,我們只能通過D3D設備創建採樣器狀態,然後綁定到渲染管線中,使得在HLSL中可以根據過濾器、尋址模式等進行採樣。

在創建採樣器狀態之前,需要先填充結構體D3D11_SAMPLER_DESC來描述採樣器狀態:

typedef struct D3D11_SAMPLER_DESC
{
    D3D11_FILTER Filter;                    // 所選過濾器
    D3D11_TEXTURE_ADDRESS_MODE AddressU;    // U方向尋址模式
    D3D11_TEXTURE_ADDRESS_MODE AddressV;    // V方向尋址模式
    D3D11_TEXTURE_ADDRESS_MODE AddressW;    // W方向尋址模式
    FLOAT MipLODBias;   // mipmap等級偏移值,最終算出的mipmap等級會加上該偏移值
    UINT MaxAnisotropy;                     // 最大各向異性等級(1-16)
    D3D11_COMPARISON_FUNC ComparisonFunc;   // 這節不討論
    FLOAT BorderColor[ 4 ];     // 邊界外的顏色,使用D3D11_TEXTURE_BORDER_COLOR時需要指定
    FLOAT MinLOD;   // 若mipmap等級低於MinLOD,則使用等級MinLOD。最小允許設爲0
    FLOAT MaxLOD;   // 若mipmap等級高於MaxLOD,則使用等級MaxLOD。必須比MinLOD大        
} 	D3D11_SAMPLER_DESC;

D3D11_FILTER部分枚舉含義如下:

枚舉值 縮小 放大 mipmap
D3D11_FILTER_MIN_MAG_MIP_POINT 點採樣 點採樣 點採樣
D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR 點採樣 點採樣 線性採樣
D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT 點採樣 線性採樣 點採樣
D3D11_FILTER_MIN_MAG_MIP_LINEAR 線性採樣 線性採樣 線性採樣
D3D11_FILTER_ANISOTROPIC 各向異性 各向異性 各向異性

D3D11_TEXTURE_ADDRESS_MODE是單個方向的尋址模式,有時候紋理座標會超過1.0或者小於0.0,這時候尋址模式可以解釋邊界外的情況,含義如下:

D3D11_TEXTURE_ADDRESS_WRAP是將指定紋理座標分量的值[t, t + 1], t ∈ Z映射到[0.0, 1.0],因此作用到u和v分量時看起來就像是把用一張貼圖緊密平鋪到其他位置上:

D3D11_TEXTURE_ADDRESS_MIRROR在每個整數點處翻轉紋理座標值。例如u在[0.0, 1.0]按正常紋理座標尋址,在[1.0, 2.0]內則翻轉,在[2.0, 3.0]內又回到正常的尋址,以此類推:

D3D11_TEXTURE_ADDRESS_CLAMP對指定紋理座標分量,小於0.0的值都取作0.0,大於1.0的值都取作1.0,在[0.0, 1.0]的紋理座標不變:

D3D11_TEXTURE_BORDER_COLOR對於指定紋理座標分量的值在[0.0, 1.0]外的區域都使用BorderColor進行填充

D3D11_TEXTURE_ADDRESS_MIRROR_ONCE相當於MIRROR和CLAMP的結合,僅[-1.0,1.0]的範圍內鏡像有效,若小於-1.0則取-1.0,大於1.0則取1.0,在[-1.0, 0.0]進行翻轉。

最後就是ID3D11Device::CreateSamplerState方法:

HRESULT ID3D11Device::CreateSamplerState( 
    const D3D11_SAMPLER_DESC *pSamplerDesc, // [In]採樣器狀態描述
    ID3D11SamplerState **ppSamplerState);   // [Out]輸出的採樣器

接下來演示瞭如何創建採樣器狀態;

// 初始化採樣器狀態描述
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));

ID3D11DeviceContext::PSSetSamplers方法–像素着色階段設置採樣器狀態

void ID3D11DeviceContext::PSSetSamplers(
    UINT StartSlot,     // [In]起始槽索引
    UINT NumSamplers,   // [In]採樣器狀態數目
    ID3D11SamplerState * const * ppSamplers);   // [In]採樣器數組  

根據前面的HLSL代碼,samLinear使用了索引爲0起始槽,所以需要這樣調用:

// 像素着色階段設置好採樣器
m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());

這樣HLSL中對應的採樣器狀態就可以使用了。

GameApp類的變動

GameApp::InitEffect的變動

現在我們需要編譯出4個着色器,2個頂點佈局,以區分2D和3D的部分。

bool GameApp::InitEffect()
{
	ComPtr<ID3DBlob> blob;

	// 創建頂點着色器(2D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader2D.GetAddressOf()));
	// 創建頂點佈局(2D)
	HR(m_pd3dDevice->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
		blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout2D.GetAddressOf()));

	// 創建像素着色器(2D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader2D.GetAddressOf()));

	// 創建頂點着色器(3D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader3D.GetAddressOf()));
	// 創建頂點佈局(3D)
	HR(m_pd3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
		blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout3D.GetAddressOf()));

	// 創建像素着色器(3D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader3D.GetAddressOf()));

	return true;
}

GameApp::InitResource的變化

雖然現在允許同時放入多盞燈光了,但在該項目我們只使用一盞點光燈,並且僅用於3D木盒的顯示。對於2D火焰動畫,實質上是由120張bmp位圖構成,我們需要按順序在每一幀切換下一張位圖來達到火焰在動的效果。

bool GameApp::InitResource()
{
	// 初始化網格模型並設置到輸入裝配階段
	auto meshData = Geometry::CreateBox();
	ResetMesh(meshData);

	// ******************
	// 設置常量緩衝區描述
	D3D11_BUFFER_DESC cbd;
	ZeroMemory(&cbd, sizeof(cbd));
	cbd.Usage = D3D11_USAGE_DYNAMIC;
	cbd.ByteWidth = sizeof(VSConstantBuffer);
	cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	// 新建用於VS和PS的常量緩衝區
	HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[0].GetAddressOf()));
	cbd.ByteWidth = sizeof(PSConstantBuffer);
	HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[1].GetAddressOf()));

	// ******************
	// 初始化紋理和採樣器狀態
	
	// 初始化木箱紋理
	HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));
	// 初始化火焰紋理
	WCHAR strFile[40];
	m_pFireAnims.resize(120);
	for (int i = 1; i <= 120; ++i)
	{
		wsprintf(strFile, L"Texture\\FireAnim\\Fire%03d.bmp", i);
		HR(CreateWICTextureFromFile(m_pd3dDevice.Get(), strFile, nullptr, m_pFireAnims[i - 1].GetAddressOf()));
	}
		
	// 初始化採樣器狀態
	D3D11_SAMPLER_DESC sampDesc;
	ZeroMemory(&sampDesc, sizeof(sampDesc));
	sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
	sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
	sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
	sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
	sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
	sampDesc.MinLOD = 0;
	sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
	HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));

	
	// ******************
	// 初始化常量緩衝區的值

	// 初始化用於VS的常量緩衝區的值
	m_VSConstantBuffer.world = XMMatrixIdentity();			
	m_VSConstantBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
		XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
	));
	m_VSConstantBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
	m_VSConstantBuffer.worldInvTranspose = XMMatrixIdentity();
	
	// 初始化用於PS的常量緩衝區的值
	// 這裏只使用一盞點光來演示
	m_PSConstantBuffer.pointLight[0].Position = XMFLOAT3(0.0f, 0.0f, -10.0f);
	m_PSConstantBuffer.pointLight[0].Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
	m_PSConstantBuffer.pointLight[0].Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
	m_PSConstantBuffer.pointLight[0].Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
	m_PSConstantBuffer.pointLight[0].Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
	m_PSConstantBuffer.pointLight[0].Range = 25.0f;
	m_PSConstantBuffer.numDirLight = 0;
	m_PSConstantBuffer.numPointLight = 1;
	m_PSConstantBuffer.numSpotLight = 0;
	m_PSConstantBuffer.eyePos = XMFLOAT4(0.0f, 0.0f, -5.0f, 0.0f);	// 這裏容易遺漏,已補上
	// 初始化材質
	m_PSConstantBuffer.material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
	m_PSConstantBuffer.material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
	m_PSConstantBuffer.material.Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 5.0f);
	// 注意不要忘記設置此處的觀察位置,否則高亮部分會有問題
	m_PSConstantBuffer.eyePos = XMFLOAT4(0.0f, 0.0f, -5.0f, 0.0f);

	// 更新PS常量緩衝區資源
	D3D11_MAPPED_SUBRESOURCE mappedData;
	HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
	memcpy_s(mappedData.pData, sizeof(PSConstantBuffer), &m_PSConstantBuffer, sizeof(PSConstantBuffer));
	m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);

	// ******************
	// 給渲染管線各個階段綁定好所需資源
	// 設置圖元類型,設定輸入佈局
	m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
	// 默認綁定3D着色器
	m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
	// VS常量緩衝區對應HLSL寄存於b0的常量緩衝區
	m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffers[0].GetAddressOf());
	// PS常量緩衝區對應HLSL寄存於b1的常量緩衝區
	m_pd3dImmediateContext->PSSetConstantBuffers(1, 1, m_pConstantBuffers[1].GetAddressOf());
	// 像素着色階段設置好採樣器
	m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());
	m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
	m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
	
	// 像素着色階段默認設置木箱紋理
	m_CurrMode = ShowMode::WoodCrate;

	return true;
}

GameApp::UpdateScene的變化

該項目可以選擇播放3D木箱或2D火焰動畫,則需要有個當前正在播放內容的狀態值,並隨之進行更新。

其中ShowMode是一個枚舉類,可以選擇WoodCrate或者FireAnim

void GameApp::UpdateScene(float dt)
{

	Keyboard::State state = m_pKeyboard->GetState();
	m_KeyboardTracker.Update(state);	

	// 鍵盤切換模式
	if (m_KeyboardTracker.IsKeyPressed(Keyboard::D1))
	{
		// 播放木箱動畫
		m_CurrMode = ShowMode::WoodCrate;
		m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
		auto meshData = Geometry::CreateBox();
		ResetMesh(meshData);
		m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
		m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
		m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
	}
	else if (m_KeyboardTracker.IsKeyPressed(Keyboard::D2))
	{
		m_CurrMode = ShowMode::FireAnim;
		m_CurrFrame = 0;
		m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout2D.Get());
		auto meshData = Geometry::Create2DShow();
		ResetMesh(meshData);
		m_pd3dImmediateContext->VSSetShader(m_pVertexShader2D.Get(), nullptr, 0);
		m_pd3dImmediateContext->PSSetShader(m_pPixelShader2D.Get(), nullptr, 0);
		m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[0].GetAddressOf());
	}

	if (m_CurrMode == ShowMode::WoodCrate)
	{
		static float phi = 0.0f, theta = 0.0f;
		phi += 0.00003f, theta += 0.00005f;
		XMMATRIX W = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
		m_VSConstantBuffer.world = XMMatrixTranspose(W);
		m_VSConstantBuffer.worldInvTranspose = XMMatrixInverse(nullptr, W);	// 兩次轉置抵消

		// 更新常量緩衝區,讓立方體轉起來
		D3D11_MAPPED_SUBRESOURCE mappedData;
		HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[0].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
		memcpy_s(mappedData.pData, sizeof(VSConstantBuffer), &m_VSConstantBuffer, sizeof(VSConstantBuffer));
		m_pd3dImmediateContext->Unmap(m_pConstantBuffers[0].Get(), 0);
	}
	else if (m_CurrMode == ShowMode::FireAnim)
	{
		// 用於限制在1秒60幀
		static float totDeltaTime = 0;

		totDeltaTime += dt;
		if (totDeltaTime > 1.0f / 60)
		{
			totDeltaTime -= 1.0f / 60;
			m_CurrFrame = (m_CurrFrame + 1) % 120;
			m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[m_CurrFrame].GetAddressOf());
		}		
	}
}

最終的顯示效果如下:

練習題

粗體字爲自定義題目

  1. 自己動手將過濾器修改爲常量插值法、線性插值法、各向異性過濾,觀察立方體盒的效果
  2. 嘗試在使用Geometry::MeshData創建的立方體網格數據(不能對其修改)的基礎上,讓立方體的六個面使用不同的紋理來繪製,可以使用魔方項目裏的紋理
  3. 使用教程項目第26章Texture文件夾中的flare.dds和flarealpha.dds,在着色器中通過分量乘法實現

    然後讓紋理在立方體的表面旋轉(考慮對紋理座標的變換),紋理採樣器的尋址模式使用BORDER_COLOR,邊界色爲黑色。效果如下:

    着色器需要提供兩個紋理,並在一個頂點着色器、一個像素着色器完成任務。
    提示:需通過旋轉矩陣(4x4矩陣)對紋理座標進行變換來實現。
  4. 如果你閱讀過"深入理解與使用2D紋理資源"這篇,那麼嘗試用一個紋理數組存儲所有的火焰紋理,在HLSL中使用Texture2DArray

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章