1. 程式人生 > >DirectX11 With Windows SDK--22 靜態天空盒的讀取與實現、模型反射

DirectX11 With Windows SDK--22 靜態天空盒的讀取與實現、模型反射

前言

從現在開始可以說算是要進入到高階主題部分了。這一章我們主要學習由6個紋理所構成的立方體對映,以及用它來實現一個靜態天空盒。

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

立方體對映(Cube Mapping)

一個立方體(通常是正方體)包含六個面,對於立方體對映來說,它的六個面對應的是六張紋理貼圖,然後以該立方體建系,中心為原點,且三個座標軸是軸對齊的。我們可以使用方向向量(±X,±Y,±Z),從原點開始,發射一條射線(取方向向量的方向)來與某個面產生交點,取得該紋理交點對應的顏色。

注意:

  1. 方向向量的大小並不重要,只要方向一致,那麼不管長度是多少,最終選擇的紋理和取樣的畫素都是一致的。
  2. 使用方向向量時要確保所處的座標系和立方體對映所處的座標系一致,如方向向量和立方體對映同時處在世界座標系中。

Direct3D提供了列舉型別D3D11_TEXTURECUBE_FACE來標識立方體某一表面:

typedef enum D3D11_TEXTURECUBE_FACE {
    D3D11_TEXTURECUBE_FACE_POSITIVE_X = 0,
    D3D11_TEXTURECUBE_FACE_NEGATIVE_X = 1,
    D3D11_TEXTURECUBE_FACE_POSITIVE_Y = 2,
    D3D11_TEXTURECUBE_FACE_NEGATIVE_Y = 3,
    D3D11_TEXTURECUBE_FACE_POSITIVE_Z = 4,
    D3D11_TEXTURECUBE_FACE_NEGATIVE_Z = 5
} D3D11_TEXTURECUBE_FACE;

可以看出:

  1. 索引0指向+X表面;
  2. 索引1指向-X表面;
  3. 索引2指向+Y表面;
  4. 索引3指向-Y表面;
  5. 索引4指向+Z表面;
  6. 索引5指向-Z表面;

使用立方體對映意味著我們需要使用3D紋理座標進行定址。

在HLSL中,立方體紋理用TextureCube來表示。

環境對映(Environment Maps)

關於立方體對映,應用最廣泛的就是環境對映了。為了獲取一份環境對映,我們可以將攝像機繫結到一個物體的中心(或者攝像機本身視為一個物體),然後使用90°的垂直FOV和水平FOV(即寬高比1:1),再讓攝像機朝著±X軸、±Y軸、±Z軸共6個軸的方向各拍攝一張不包括物體本身的場景照片。因為FOV的角度為90°,這六張圖片已經包含了以物體中心進行的透視投影,所記錄的完整的周遭環境。接下來就是將這六張圖片儲存在立方體紋理中,以構成環境對映。綜上所述,環境對映就是在立方體表面的紋理中儲存了周圍環境的影象。

由於環境對映僅捕獲了遠景的資訊,這樣附近的許多物體都可以共用同一個環境對映。這種做法稱之為靜態立方體對映,它的優點是僅需要六張紋理就可以輕鬆實現,但缺陷是該環境對映並不會記錄臨近物體資訊,在繪製反射時就看不到周圍的物體了。

注意到環境對映所使用的六張圖片不一定非得是從Direct3D程式中捕獲的。因為立方體對映僅儲存紋理資料,它們的內容通常可以是美術師預先生成的,或者是自己找到的。

一般來說,我們能找到的天空盒有如下三種:

  1. 已經建立好的.dds檔案,可以直接通過DDSTextureLoader讀取使用
  2. 6張天空盒的正方形貼圖,格式不限。(暫不考慮只有5張的)
  3. 1張天空盒貼圖,包含了6個面,格式不限,圖片寬高比為4:3

對於第三種天空盒,其平面分佈如下:

對於其餘兩種天空盒,這裡也提供了3種方法讀取。

使用DXTex構建天空盒

準備6張天空盒的正方形貼圖,如果是屬於上述第三種情況,可以用截圖工具來截取出6張貼圖,但是要注意按原圖的解析度來進行擷取。

開啟放在Github專案中Utility資料夾內的DxTex.exe,新建紋理:

Texture Type要選擇Cubemap Texture

Dimensions填寫正方形紋理的畫素寬度和高度,因為1024x1024的紋理最多可以生成11級mipmap鏈,這裡設定成11.但如果你不需要mipmap鏈,則直接指定為1.

對於Surface/Volume Format,通常情況下使用Unsigned 32-bit: A8R8G8B8格式,如果想要節省記憶體(但是會犧牲質量),可以選用Four CC 4-bit: DXT1格式,可以獲得6:1甚至8:1的壓縮比。

建立好後會變成這樣:

可以看到當前預設的是+X紋理。

接下來就是將這六張圖片塞進該立方體紋理中了,選擇View-Cube map Face,並選擇需要修改的紋理:

在當前專案的Texture資料夾內已經準備好了有6張貼圖。

選擇File-Open To This Cubemap Face來選擇對應的貼圖以載入進來即可。每完成當前的面就要切換到下一個面繼續操作,直到六個面都填充完畢。

最後就可以點選File-Save As來儲存dds檔案了。

這種做法需要比較長的前期準備時間,它不適合批量處理。但是在讀取上是最方便的。

使用程式碼讀取天空盒

對於建立好的DDS立方體紋理,我們只需要使用DDSTextureLoader就可以很方便地讀取進來:

HR(CreateDDSTextureFromFile(
    device.Get(), 
    cubemapFilename.c_str(), 
    nullptr, 
    textureCubeSRV.GetAddressOf()));

然而從網路上能夠下到的天空盒資源經常要麼是一張天空盒貼圖,要麼是六張天空盒的正方形貼圖,用DXTex匯入還是比較麻煩的一件事情。我們也可以自己編寫程式碼來構造立方體紋理。

將一張天空盒貼圖轉化成立方體紋理需要經歷以下4個步驟:

  1. 讀取天空盒的貼圖
  2. 建立包含6個紋理的陣列
  3. 選取原天空盒紋理的6個子正方形區域,拷貝到該陣列中
  4. 建立立方體紋理的SRV

而將六張天空盒的正方形貼圖轉換成立方體需要經歷這4個步驟:

  1. 讀取這六張正方形貼圖
  2. 建立包含6個紋理的陣列
  3. 將這六張貼圖完整地拷貝到該陣列中
  4. 建立立方體紋理的SRV

可以看到這兩種型別的天空盒資源在處理上有很多相似的地方。

在d3dUtil.h中,提供了CreateWICTextureCubeFromFile的過載函式,原型如下:

//
// 紋理立方體相關函式
//

// 根據給定的一張包含立方體六個面的紋理,建立紋理立方體
// 要求紋理寬高比為4:3,且按下面形式佈局:
// .  +Y .  .
// -X +Z +X -Z 
// .  -Y .  .
// 該函式預設不生成mipmap(即等級僅為1),若需要則設定generateMips為true
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> CreateWICTextureCubeFromFile(
    Microsoft::WRL::ComPtr<ID3D11Device> device,
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> deviceContext,
    std::wstring cubemapFileName,
    bool generateMips = false);

// 根據按D3D11_TEXTURECUBE_FACE索引順序給定的六張紋理,建立紋理立方體
// 要求紋理是同樣大小的正方形
// 該函式預設不生成mipmap(即等級僅為1),若需要則設定generateMips為true
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> CreateWICTextureCubeFromFile(
    Microsoft::WRL::ComPtr<ID3D11Device> device,
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> deviceContext,
    std::vector<std::wstring> cubemapFileNames,
    bool generateMips = false);

1.讀取天空盒紋理

CreateWICTextureFromFileEx函式--使用更多的引數,從檔案中讀取WIC紋理

HRESULT __cdecl CreateWICTextureFromFileEx(
    ID3D11Device* d3dDevice,                // [In]D3D裝置
    ID3D11DeviceContext* d3dContext,        // [In]D3D裝置上下文(可選)
    const wchar_t* szFileName,              // [In].bmp/.jpg/.png檔名
    size_t maxsize,                         // [In]預設填0,否則圖片會根據該畫素大小進行縮放
    D3D11_USAGE usage,                      // [In]D3D11_USAGE列舉值型別,指定CPU/GPU讀寫許可權
    unsigned int bindFlags,                 // [In]繫結標籤,指定它可以被繫結到什麼物件上
    unsigned int cpuAccessFlags,            // [In]CPU訪問許可權標籤
    unsigned int miscFlags,                 // [In]雜項標籤
    unsigned int loadFlags,                 // [In]WIC_LOADER_FLAGS列舉值型別,用於指定SRGB
    ID3D11Resource** texture,               // [Out]獲取建立好的紋理(可選)
    ID3D11ShaderResourceView** textureView);// [Out]獲取建立好的紋理資源檢視(可選)
}

關於紋理的拷貝操作可以不需要從GPU讀到CPU再進行,而是直接在GPU之間進行拷貝,因此可以將usage設為D3D11_USAGE_DEFAULTcpuAccessFlags設為0.

現在先不演示使用方法。由於通過該函式讀取進來的紋理mipmap等級只有1,如果還需要建立mipmap鏈的話,還需要用到下面的方法。

ID3D11DeviceContext::GenerateMips--為紋理資源檢視建立完整的mipmap鏈

void ID3D11DeviceContext::GenerateMips(
  ID3D11ShaderResourceView *pShaderResourceView // [In]需要建立mipamp鏈的SRV
);

比如一張1024x1024的紋理,經過該方法呼叫後,就會生成剩餘的512x512, 256x256 ... 1x1的子紋理資源,加起來一共是11級mipmap。

但是在呼叫該方法之前,需要確保所使用的紋理bindFlags需要同時設定D3D11_BIND_RENDER_TARGETD3D11_BIND_SHADER_RESOURCE標籤,然後在miscFlags中設定為D3D11_RESOURCE_MISC_GENERATE_MIPS標籤,否則呼叫無效。

無論是否需要生成mipmap鏈,D3D11_BIND_SHADER_RESOURCE標籤是必須的,因為它很大可能會用在著色器資源的繫結。我們可以在CreateWICTextureFromFile函式的實現中看到:

HRESULT DirectX::CreateWICTextureFromFile(ID3D11Device* d3dDevice,
    ID3D11DeviceContext* d3dContext,
    const wchar_t* fileName,
    ID3D11Resource** texture,
    ID3D11ShaderResourceView** textureView,
    size_t maxsize)
{
    return CreateWICTextureFromFileEx(d3dDevice, d3dContext, fileName, maxsize,
        D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE, 0, 0, WIC_LOADER_DEFAULT,
        texture, textureView);
}

在瞭解上面這些內容後,我們就可以開始載入天空盒紋理了,然後在使用者指定了需要建立mipmap鏈時再呼叫ID3D11DeviceContext::GenerateMips方法。現在演示的是單張天空盒紋理的載入:

ComPtr<ID3D11Texture2D> srcTex;
ComPtr<ID3D11ShaderResourceView> srcTexSRV;

// 該資源用於GPU複製
HR(CreateWICTextureFromFileEx(device.Get(),
    deviceContext.Get(),
    cubemapFileName.c_str(),
    0,
    D3D11_USAGE_DEFAULT,
    D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0),
    0,
    (generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0),
    WIC_LOADER_DEFAULT,
    (ID3D11Resource**)srcTex.GetAddressOf(),
    (generateMips ? srcTexSRV.GetAddressOf() : nullptr)));
// (可選)生成mipmap鏈
if (generateMips)
{
    deviceContext->GenerateMips(srcTexSRV.Get());
}

注意srcTexsrcTexSRV都指向同一份資源。

至於讀取六張正方形貼圖的操作也是一樣的,這裡就不贅述了。

2.建立包含6個紋理的陣列

接下來需要建立一個新的紋理陣列。首先需要填充D3D11_TEXTURE2D_DESC結構體內容,這裡的大部分引數可以從天空盒紋理取得。

這裡以單張天空盒貼圖的為例:


D3D11_TEXTURE2D_DESC texDesc, texCubeDesc;
srcTex->GetDesc(&texDesc);
    
// 確保寬高比4:3
assert(texDesc.Width * 3 == texDesc.Height * 4);

UINT squareLength = texDesc.Width / 4;

texCubeDesc.Width = squareLength;
texCubeDesc.Height = squareLength;

// 例如64x48的天空盒,可以產生7級mipmap鏈,但天空盒的每個面是16x16,對應5級mipmap鏈,因此需要減2
texCubeDesc.MipLevels = (generateMips ? texDesc.MipLevels - 2 : 1);
texCubeDesc.ArraySize = 6;
texCubeDesc.Format = texDesc.Format;    
texCubeDesc.SampleDesc.Count = 1;
texCubeDesc.SampleDesc.Quality = 0;
texCubeDesc.Usage = D3D11_USAGE_DEFAULT;
texCubeDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; 
texCubeDesc.CPUAccessFlags = 0;
texCubeDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;    // 標記為TextureCube

ComPtr<ID3D11Texture2D> texCube;
HR(device->CreateTexture2D(&texCubeDesc, nullptr, texCube.GetAddressOf()));

D3D11_BIND_SHADER_RESOURCED3D11_RESOURCE_MISC_TEXTURECUBE的標籤記得不要遺漏。

3.選取原天空盒紋理的6個子正方形區域,拷貝到該陣列中

D3D11_BOX結構體

在進行節選之前,首先我們需要了解定義3D盒的結構體D3D11_BOX

typedef struct D3D11_BOX {
    UINT left;  
    UINT top;
    UINT front;
    UINT right;
    UINT bottom;
    UINT back;
} D3D11_BOX;

3D box使用的是下面的座標系,和紋理座標系很像:

由於選取畫素採用的是半開半閉區間,如[left, right),在指定left, top, front的值時會選到該畫素,而不對想到right, bottom, back對應的畫素。

對於1D紋理來說,是沒有Y軸和Z軸的,因此需要令back=0, front=1, top=0, bottom=1才能表示當前的1D紋理,如果出現像back和front相等的情況,則不會選到任何的紋理畫素區間。

而2D紋理沒有Z軸,在選取畫素區域前需要置back=0, front=1

3D紋理(體積紋理)可以看做一系列紋理的堆疊,因此frontback可以用來選定哪些紋理需要節選。

ID3D11DeviceContext::CopySubresourceRegion方法--從指定資源選取區域複製到目標資源特定區域

void ID3D11DeviceContext::CopySubresourceRegion(
    ID3D11Resource  *pDstResource,  // [In/Out]目標資源
    UINT            DstSubresource, // [In]目標子資源索引
    UINT            DstX,           // [In]目標起始X值
    UINT            DstY,           // [In]目標起始Y值
    UINT            DstZ,           // [In]目標起始Z值
    ID3D11Resource  *pSrcResource,  // [In]源資源
    UINT            SrcSubresource, // [In]源子資源索引
    const D3D11_BOX *pSrcBox        // [In]指定複製區域
);

例如現在我們要將該天空盒的+X面對應的mipmap鏈拷貝到ArraySlice為0(即D3D11_TEXTURECUBE_FACE_POSITIVE_X)的目標資源中,則可以像下面這樣寫:

D3D11_BOX box;
box.front = 0;
box.back = 1;

for (UINT i = 0; i < texCubeDesc.MipLevels; ++i)
{
    // +X面拷貝
    box.left = squareLength * 2;
    box.top = squareLength;
    box.right = squareLength * 3;
    box.bottom = squareLength * 2;
    deviceContext->CopySubresourceRegion(
        texCube.Get(),
        D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_POSITIVE_X, texCubeDesc.MipLevels),
        0, 0, 0,
        srcTex.Get(),
        i,
        &box);
    
    // 此處省略其餘面的拷貝...
    
    // 下一個mipLevel的紋理寬高都是原來的1/2
    squareLength /= 2;
}

至於天空盒的六張正方形貼圖的話,我們不需要對原貼圖進行裁剪,但還是需要將子資源逐個轉移到紋理陣列中。為了拷貝整個紋理子資源,需要指定pSrcBoxnullptr:

for (int i = 0; i < 6; ++i)
{
    for (UINT j = 0; j < texCubeDesc.MipLevels; ++j)
    {
        deviceContext->CopySubresourceRegion(
            texCube.Get(),
            D3D11CalcSubresource(j, i, texCubeDesc.MipLevels),
            0, 0, 0,
            srcTex[i].Get(),
            j,
            nullptr);
    }
}

4.建立紋理立方體的著色器資源檢視

到這一步就簡單的多了:

D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
viewDesc.Format = texCubeDesc.Format;
viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
viewDesc.TextureCube.MostDetailedMip = 0;
viewDesc.TextureCube.MipLevels = texCubeDesc.MipLevels;

ComPtr<ID3D11ShaderResourceView> texCubeSRV;
HR(device->CreateShaderResourceView(texCube.Get(), &viewDesc, texCubeSRV.GetAddressOf()));

繪製天空盒

儘管天空盒是一個立方體,但是實際上渲染的是一個很大的"球體"(由大量的三角形逼近)表面。使用方向向量來對映到立方體紋理對應的畫素顏色,同時它也指向當前繪製的"球"面上對應點。另外,為了保證繪製的天空盒永遠處在攝像機能看到的最遠處,通常會將該球體的中心設定在攝像機所處的位置。這樣無論攝像機如何移動,天空盒也跟隨攝像機移動,使用者將永遠到不了天空盒的一端。可以說這和公告板一樣,都是一種欺騙人眼的小技巧。如果不讓天空盒跟隨攝像機移動,這種假象立馬就會被打破。

天空球體和紋理立方體的中心一致,不需要管它們的大小關係。

實際繪製的天空球體

繪製天空盒需要以下準備工作:

  1. 將天空盒載入HLSL的TextureCube中
  2. 在光柵化階段關閉背面消隱
  3. 在輸出合併階段的深度/模板狀態,設定深度比較函式為小於等於,以允許深度值為1的畫素繪製

新的深度/模板狀態

RenderStates.h引進了一個新的ID3D11DepthStencilState型別的成員DSSLessEqual,定義如下:

D3D11_DEPTH_STENCIL_DESC dsDesc;

// 允許使用深度值一致的畫素進行替換的深度/模板狀態
// 該狀態用於繪製天空盒,因為深度值為1.0時預設無法通過深度測試
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;

dsDesc.StencilEnable = false;

HR(device->CreateDepthStencilState(&dsDesc, DSSLessEqual.GetAddressOf()));

在繪製天空盒前就需要設定該深度/模板狀態:

deviceContext->OMSetDepthStencilState(RenderStates::DSSLessEqual.Get(), 0);

HLSL程式碼

現在我們需要一組新的特效來繪製天空盒,其中與之相關的是Sky.hlsli, Sky_VS.hlslSky_PS.hlsl,當然在C++那邊還有新的SkyEffect類來管理,需要了解自定義Effect的可以回看第13章。

// Sky.hlsli
TextureCube texCube : register(t0);
SamplerState sam : register(s0);

cbuffer CBChangesEveryFrame : register(b0)
{
    matrix gWorldViewProj;
}

struct VertexPos
{
    float3 PosL : POSITION;
};

struct VertexPosHL
{
    float4 PosH : SV_POSITION;
    float3 PosL : POSITION;
};

// Sky_VS.hlsl
#include "Sky.hlsli"

VertexPosHL VS(VertexPos vIn)
{
    VertexPosHL vOut;
    
    // 設定z = w使得z/w = 1(天空盒保持在遠平面)
    float4 posH = mul(float4(vIn.PosL, 1.0f), gWorldViewProj);
    vOut.PosH = posH.xyww;
    vOut.PosL = vIn.PosL;
    return vOut;
}
// Sky_PS.hlsl
#include "Sky.hlsli"

float4 PS(VertexPosHL pIn) : SV_Target
{
    return texCube.Sample(sam, pIn.PosL);
}

注意: 在過去,應用程式首先繪製天空盒以取代渲染目標和深度/模板緩衝區的清空。然而“ATI Radeon HD 2000 Programming Gudie"(現在已經404了)建議我們不要這麼做。首先,為了獲得內部硬體深度優化的良好表現,深度/模板緩衝區需要被顯式清空。這對渲染目標同樣有效。其次,通常絕大多數的天空會被其它物體給遮擋。因此,如果我們先繪製天空,再繪製物體的話會導致二次繪製,還不如先繪製物體,然後讓被遮擋的天空部分不通過深度測試。因此現在推薦的做法為:總是先清空渲染目標和深度/模板緩衝區,天空盒的繪製留到最後。

模型的反射

關於環境對映,另一個主要應用就是模型表面的反射(只有當天空盒記錄了除當前反射物體外的其它物體時,才能在該物體看到其餘物體的反射)。對於靜態天空盒來說,通過模型看到的反射只能看到天空盒本身,因此還是顯得不夠真實。至於動態天空盒就還是留到下一章再講。

下圖說明了反射是如何通過環境對映運作的。法向量n對應的表面就像是一個鏡面,攝像機在位置e,觀察點p時可以看到經過反射得到的向量v所指向的天空盒紋理的取樣畫素點:

首先在之前的Basic.hlsli中加入TextureCube:

// Basic.hlsli
Texture2D texA : register(t0);
Texture2D texD : register(t1);
TextureCube texCube : register(t2);
SamplerState sam : register(s0);

// ...

然後只需要在Basic_PS.hlsl新增如下內容:

float4 litColor = texColorA * ambient + texColorD * diffuse + spec;

if (gReflectionEnabled)
{
    float3 incident = -toEyeW;
    float3 reflectionVector = reflect(incident, pIn.NormalW);
    float4 reflectionColor = texCube.Sample(sam, reflectionVector);

    litColor += gMaterial.Reflect * reflectionColor;
}
    
litColor.a = texColorD.a * gMaterial.Diffuse.a;
return litColor;

然後在C++端,將取樣器設定為各向異性過濾:

// 在RenderStates.h/.cpp可以看到
ComPtr<ID3D11SamplerState> RenderStates::SSAnistropicWrap;

D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));

// 各向異性過濾模式
sampDesc.Filter = D3D11_FILTER_ANISOTROPIC;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MaxAnisotropy = 4;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(device->CreateSamplerState(&sampDesc, SSAnistropicWrap.GetAddressOf()));


// 在BasicEffect.cpp可以看到
deviceContext->PSSetSamplers(0, 1, RenderStates::SSAnistropicWrap.GetAddressOf());

通常一個畫素的顏色不完全是反射後的顏色(只有鏡面才是100%反射)。因此,我們將原來的光照等式加上了材質反射的分量。當初MaterialReflect成員現在就派上了用場:

// 物體表面材質
struct Material
{
    Material() { memset(this, 0, sizeof(Material)); }

    DirectX::XMFLOAT4 Ambient;
    DirectX::XMFLOAT4 Diffuse;
    DirectX::XMFLOAT4 Specular; // w = 鏡面反射強度
    DirectX::XMFLOAT4 Reflect;
};

我們可以指定該材質的反射顏色,如果該材質只反射完整的紅光部分,則在C++指定Reflect = XMFLOAT4(1.0f, 0.0f, 0.0f, 0.0f)

使用帶加法的反射容易引發一個問題:過度飽和。兩個顏色的相加可能會存在RGB值超過1而變白,這會導致某些畫素的顏色過於明亮。通常如果我們新增反射分量的顏色,就必須減小材質本身的環境分量和漫反射分量來實現平衡。另一種方式就是對反射分量和畫素顏色s進行插值處理:

\[\mathbf{f} = t\mathbf{c}_{R} + (1 - t)\mathbf{s} (0 <= t <= 1) \]

這樣我們就可以通過調整係數t來控制反射程度,以達到自己想要的效果。

還有一個問題就是,在平面上進行環境對映並不會取得理想的效果。這是因為上面的HLSL程式碼關於反射的部分只使用了方向向量來進行取樣,這會導致以相同的的傾斜角度看平面時,不同的位置看到的反射效果卻是一模一樣的。正確的效果應該是:攝像機在跟隨平面鏡做平移運動時,平面鏡的映象應該保持不動。下面用兩張圖來說明這個問題:

這裡給出龍書所提供相關論文,用以糾正環境映射出現的問題: Brennan02

本專案現在不考慮解決這個問題。

SkyRender類

SkyRender類支援之前所述的3種天空盒的載入,由於在構造的同時還會建立球體,建議使用unique_ptr來管理物件。

下面是SkyRender的完整實現:

class SkyRender
{
public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;


    // 需要提供完整的天空盒貼圖 或者 已經建立好的天空盒紋理.dds檔案
    SkyRender(ComPtr<ID3D11Device> device, 
        ComPtr<ID3D11DeviceContext> deviceContext, 
        const std::wstring& cubemapFilename, 
        float skySphereRadius,
        bool generateMips = false);


    // 需要提供天空盒的六張正方形貼圖
    SkyRender(ComPtr<ID3D11Device> device, 
        ComPtr<ID3D11DeviceContext> deviceContext, 
        const std::vector<std::wstring>& cubemapFilenames, 
        float skySphereRadius,
        bool generateMips = false);


    ComPtr<ID3D11ShaderResourceView> GetTextureCube();

    void Draw(ComPtr<ID3D11DeviceContext> deviceContext, SkyEffect& skyEffect, const Camera& camera);

private:
    void InitResource(ComPtr<ID3D11Device> device, float skySphereRadius);

private:
    ComPtr<ID3D11Buffer> mVertexBuffer;
    ComPtr<ID3D11Buffer> mIndexBuffer;

    UINT mIndexCount;

    ComPtr<ID3D11ShaderResourceView> mTextureCubeSRV;
};
SkyRender::SkyRender(
    ComPtr<ID3D11Device> device, 
    ComPtr<ID3D11DeviceContext> deviceContext, 
    const std::wstring & cubemapFilename, 
    float skySphereRadius,
    bool generateMips)
{
    // 天空盒紋理載入
    if (cubemapFilename.substr(cubemapFilename.size() - 3) == L"dds")
    {
        HR(CreateDDSTextureFromFile(
            device.Get(),
            cubemapFilename.c_str(),
            nullptr,
            mTextureCubeSRV.GetAddressOf()
        ));
    }
    else
    {
        mTextureCubeSRV = CreateWICTextureCubeFromFile(
            device,
            deviceContext,
            cubemapFilename,
            generateMips
        );
    }

    InitResource(device, skySphereRadius);
}

SkyRender::SkyRender(ComPtr<ID3D11Device> device, 
    ComPtr<ID3D11DeviceContext> deviceContext, 
    const std::vector<std::wstring>& cubemapFilenames, 
    float skySphereRadius,
    bool generateMips)
{
    // 天空盒紋理載入

    mTextureCubeSRV = CreateWICTextureCubeFromFile(
        device,
        deviceContext,
        cubemapFilenames,
        generateMips
    );

    InitResource(device, skySphereRadius);
}

ComPtr<ID3D11ShaderResourceView> SkyRender::GetTextureCube()
{
    return mTextureCubeSRV;
}

void SkyRender::Draw(ComPtr<ID3D11DeviceContext> deviceContext, SkyEffect & skyEffect, const Camera & camera)
{
    UINT strides[1] = { sizeof(XMFLOAT3) };
    UINT offsets[1] = { 0 };
    deviceContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), strides, offsets);
    deviceContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    XMFLOAT3 pos = camera.GetPosition();
    skyEffect.SetWorldViewProjMatrix(XMMatrixTranslation(pos.x, pos.y, pos.z) * camera.GetViewProjXM());
    skyEffect.SetTextureCube(mTextureCubeSRV);
    skyEffect.Apply(deviceContext);
    deviceContext->DrawIndexed(mIndexCount, 0, 0);
}

void SkyRender::InitResource(ComPtr<ID3D11Device> device, float skySphereRadius)
{
    Geometry::MeshData sphere = Geometry::CreateSphere(skySphereRadius);
    size_t size = sphere.vertexVec.size();
    std::vector<XMFLOAT3> vertices(size);
    for (size_t i = 0; i < size; ++i)
    {
        vertices[i] = sphere.vertexVec[i].pos;
    }

    // 頂點緩衝區建立
    D3D11_BUFFER_DESC vbd;
    vbd.Usage = D3D11_USAGE_IMMUTABLE;
    vbd.ByteWidth = sizeof(XMFLOAT3) * (UINT)vertices.size();
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vbd.CPUAccessFlags = 0;
    vbd.MiscFlags = 0;
    vbd.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = vertices.data();

    HR(device->CreateBuffer(&vbd, &InitData, &mVertexBuffer));

    // 索引緩衝區建立
    mIndexCount = (UINT)sphere.indexVec.size();

    D3D11_BUFFER_DESC ibd;
    ibd.Usage = D3D11_USAGE_IMMUTABLE;
    ibd.ByteWidth = sizeof(WORD) * mIndexCount;
    ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
    ibd.CPUAccessFlags = 0;
    ibd.StructureByteStride = 0;
    ibd.MiscFlags = 0;

    InitData.pSysMem = sphere.indexVec.data();

    HR(device->CreateBuffer(&ibd, &InitData, &mIndexBuffer));

}

與其配套的SkyEffect可以在原始碼中觀察到。

專案演示

說了那麼多內容,是時候看一些動圖了吧。

該專案載入了三種類型的天空盒,可以隨時切換。

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