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

前言

這一章咱們主要學習由6個紋理所構成的立方體映射,以及用它來實現一個靜態天空盒。html

可是在此以前先要消除兩個誤區:git

  1. 認爲這一章的天空盒就是簡單的在一個超大立方體的六個面內部貼上天空盒紋理;
  2. 認爲天空盒的頂點都是固定的,距離起始點的位置特別遠。

我提出這兩個誤區,是由於看到有些人的做品直接貼了六個立方體,就說本身用到了天空盒技術,可是當你真正學這一章的話會發現此天空盒非彼天空盒,並且該篇教程除了天空盒的技術實現外,還有其他的一些乾貨值得學習,建議認真研讀。github

在此以前還須要回顧一下里面有關紋理子資源的部分:數組

章節回顧
深刻理解與使用2D紋理資源(重點了解紋理子資源、紋理數組和紋理天空盒)

DirectX11 With Windows SDK完整目錄網絡

Github項目源碼app

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

立方體映射(Cube Mapping)

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

注意:函數

  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填寫正方形紋理的像素寬度和高度

若是你須要自動生成mipmaps,則指定mipmap Level爲1.若是你須要手工填充mipmaps,因爲1024x1024的紋理mipmap最大數目爲11,你能夠指定mipmap Level爲2-11的值。

對於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來選擇對應的貼圖以加載進來便可。每完成當前的面就要切換到下一個面繼續操做,直到六個面都填充完畢。此時填充的是Mipmap Level爲0的子資源:

若是你須要自動生成紋理,則能夠點擊下面的選項生成,要求建立時MipMap Level爲1:

最後就能夠點擊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

能夠看到這兩種類型的天空盒資源在處理上有不少類似的地方。

有關天空盒讀取的代碼實現若是你想了解,須要回到開頭瞭解"深刻理解與使用2D紋理資源"這章

繪製天空盒

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

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

實際繪製的天空球體

繪製天空盒須要如下準備工做:

  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 g_TexCube : register(t0);
SamplerState g_Sam : register(s0);

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

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), g_WorldViewProj);
    vOut.PosH = posH.xyww;
    vOut.PosL = vIn.PosL;
    return vOut;
}
// Sky_PS.hlsl
#include "Sky.hlsli"

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

注意: 在過去,應用程序首先繪製天空盒以取代渲染目標和深度/模板緩衝區的清空。然而「ATI Radeon HD 2000 Programming Gudie"(如今已經404了)建議咱們不要這麼作。首先,爲了得到內部硬件深度優化的良好表現,深度/模板緩衝區須要被顯式清空。這對渲染目標一樣有效。其次,一般絕大多數的天空會被其它物體給遮擋。所以,若是咱們先繪製天空,再繪製物體的話會致使二次繪製,還不如先繪製物體,而後讓被遮擋的天空部分不經過深度測試。所以如今推薦的作法爲:老是先清空渲染目標和深度/模板緩衝區,天空盒的繪製留到最後。

模型的反射

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

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

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

// Basic.hlsli
Texture2D g_DiffuseMap : register(t0);
TextureCube g_TexCube : register(t1);
SamplerState g_Sam : register(s0);

// ...

而後只須要在Basic_PS.hlsl添加以下內容:

float4 litColor = texColor * (ambient + diffuse) + spec;

if (g_ReflectionEnabled)
{
    float3 incident = -toEyeW;
    float3 reflectionVector = reflect(incident, pIn.NormalW);
    float4 reflectionColor = g_TexCube.Sample(g_Sam, reflectionVector);

    litColor += g_Material.Reflect * reflectionColor;
}
    
litColor.a = texColor.a * g_Material.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(ID3D11Device * device, 
        ID3D11DeviceContext * deviceContext, 
        const std::wstring& cubemapFilename, 
        float skySphereRadius,      // 天空球半徑
        bool generateMips = false); // 默認不爲靜態天空盒生成mipmaps


    // 須要提供天空盒的六張正方形貼圖
    SkyRender(ID3D11Device * device, 
        ID3D11DeviceContext * deviceContext, 
        const std::vector<std::wstring>& cubemapFilenames, 
        float skySphereRadius,      // 天空球半徑
        bool generateMips = false); // 默認不爲靜態天空盒生成mipmaps


    ID3D11ShaderResourceView * GetTextureCube();

    void Draw(ID3D11DeviceContext * deviceContext, SkyEffect& skyEffect, const Camera& camera);

    // 設置調試對象名
    void SetDebugObjectName(const std::string& name);

private:
    void InitResource(ID3D11Device * device, float skySphereRadius);

private:
    ComPtr<ID3D11Buffer> m_pVertexBuffer;
    ComPtr<ID3D11Buffer> m_pIndexBuffer;

    UINT m_IndexCount;

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

    InitResource(device, skySphereRadius);
}

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

    HR(CreateWICTexture2DCubeFromFile(
        device,
        deviceContext,
        cubemapFilenames,
        nullptr,
        m_pTextureCubeSRV.GetAddressOf(),
        generateMips
    ));

    InitResource(device, skySphereRadius);
}

ID3D11ShaderResourceView * SkyRender::GetTextureCube()
{
    return m_pTextureCubeSRV.Get();
}

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

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

void SkyRender::SetDebugObjectName(const std::string& name)
{
#if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME)
    std::string texCubeName = name + ".CubeMapSRV";
    std::string vbName = name + ".VertexBuffer";
    std::string ibName = name + ".IndexBuffer";
    m_pTextureCubeSRV->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(texCubeName.length()), texCubeName.c_str());
    m_pVertexBuffer->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(vbName.length()), vbName.c_str());
    m_pIndexBuffer->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(ibName.length()), ibName.c_str());
#else
    UNREFERENCED_PARAMETER(name);
#endif
}

void SkyRender::InitResource(ID3D11Device * device, float skySphereRadius)
{
    auto sphere = Geometry::CreateSphere<VertexPos>(skySphereRadius);

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

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = sphere.vertexVec.data();

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

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

    D3D11_BUFFER_DESC ibd;
    ibd.Usage = D3D11_USAGE_IMMUTABLE;
    ibd.ByteWidth = sizeof(WORD) * m_IndexCount;
    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, &m_pIndexBuffer));

}

與其配套的SkyEffect能夠在源碼中觀察到。

項目演示

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

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

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

相關文章
相關標籤/搜索