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

前言

在以前的DirectX SDK中,紋理的讀取使用的是D3DX11CreateShaderResourceViewFromFile函數,如今在Windows SDK中已經沒有這些函數,咱們須要找到DDSTextureLoaderWICTextureLoader這兩個庫來讀取DDS位圖和WIC位圖html

DirectX11 With Windows SDK完整目錄git

Github項目源碼github

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。編程

紋理座標系

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

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

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

\[(x, y, z) = \mathbf{p_0} + s(\mathbf{p_1} - \mathbf{p_0}) + t(\mathbf{p_2} - \mathbf{p_0})\]佈局

\[(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 <= 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)):

\[\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的紋理,經過不斷的向下採樣,能夠得到256x25六、128x12八、64x64...一直到1x1的一系列位圖,這些位圖構建了一條mipmap鏈,而且不一樣的紋理標註有不一樣的mipmap等級

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

\[\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使用重複的紋理去覆蓋整個實數域,能夠當作fmod(X, 1.0f)

D3D11_TEXTURE_ADDRESS_MIRROR用重複的紋理覆蓋整個實數域,只不過相鄰的兩個紋理知足按交線對稱

D3D11_TEXTURE_ADDRESS_CLAMP對U軸和V軸,小於0的值都取做0,大於1的值都取做1

D3D11_TEXTURE_BORDER_COLOR對於紋理範圍外的區域都使用BorderColor進行填充

D3D11_TEXTURE_ADDRESS_MIRROR_ONCE至關於MIRROR和CLAMP的結合,僅[-1,1]的範圍內鏡像有效,其他範圍都會取到-1或者1

最後就是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,邊界色爲黑色。效果以下:

    着色器須要提供兩個紋理,並在一個頂點着色器、一個像素着色器完成任務。
  4. 若是你閱讀過"深刻理解與使用2D紋理資源"這篇,那麼嘗試用一個紋理數組存儲全部的火焰紋理,在HLSL中使用Texture2DArray

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

相關文章
相關標籤/搜索