DirectX11 With Windows SDK--22 立方體映射:靜態天空盒的讀取與實現

前言

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

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入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可以在源碼中觀察到。

項目演示

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

該項目加載了三種類型的天空盒,可以隨時切換。

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

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