前言 上一章我們知道瞭如何使用幾何著色器將頂點通過流輸出階段輸出到綁定的頂點緩衝區。接下來我們繼續利用它來實現一些新的效果,在這一章,你將瞭解: 1. 實現公告板效果 2. Alpha To Coverage 3. 對GPU資源進行讀/寫操作 4. 紋理數組 5. 實現霧效 "DirectX11 W ...
前言
上一章我們知道瞭如何使用幾何著色器將頂點通過流輸出階段輸出到綁定的頂點緩衝區。接下來我們繼續利用它來實現一些新的效果,在這一章,你將瞭解:
- 實現公告板效果
- Alpha-To-Coverage
- 對GPU資源進行讀/寫操作
- 紋理數組
- 實現霧效
DirectX11 With Windows SDK完整目錄
實現霧效
雖然這部分與幾何著色器並沒有什麼關係,但是霧的效果在該Demo中會用到,並且前面也沒有講過這部分內容,故先在這裡提出來。
有時候我們需要在游戲中模擬一些特定的天氣條件,比如說大霧。它可以讓物體平滑出現而不是突然蹦出來那樣(物體的一部分留在視錐體內使得只能看到該部分,然後在逐漸靠近該物體的時候,該物體就像經過了一個無形的掃描門被逐漸構造出來那樣)。通過讓霧在某一範圍內具有一定的層次(讓不可見區域比視錐體裁剪區域還近),我們可以避免上面所說的情況。但即便是晴朗的天氣,你可能仍希望包含一個較廣範圍的霧效,即距離達到很遠的地方纔逐漸看不清物體。
我們可以使用這種方式來實現霧效:指定霧的顏色,以攝像機為原點的霧開始的最小距離,霧效範圍值(超過起始距離+霧效範圍值的範圍外的顏色皆被指定的霧色取代)。在需要繪製的三角形內,某一像素片元的顏色如下:
\(\begin{align} foggedColor &= litColor + s(fogColor - litColor)\\ &= (1-s) \cdot litColor + s \cdot fogColor\\ \end{align}\)
該函數對應HLSL中的lerp函數,s取0的時候最終顏色為litColor,然後逐漸增大並逼近1的時候,最終顏色就逐漸趨近於fogColor。然後參數s的值取決於下麵的函數:
\(s = saturate(\frac{dist(\mathbf{p},\mathbf{E}) - fogStart}{fogRange})\)
\(saturate(x) = \begin{cases} x, 0 \le x \le 1\\ 0, x < 0\\ 1, x > 1\\ \end{cases}\)
其中dist(p,E)指的是兩點之間的距離值。配合下麵的圖去理解:
還有註意一點,在每次清空重新繪製的時候,要用霧的顏色進行清空。
HLSL代碼
與霧效相關的值存儲在下麵的常量緩衝區中,並且繪製3D物體的頂點沒有發生變化:
// Basic.fx
// ...
cbuffer CBDrawingStates : register(b2)
{
float4 gFogColor;
int gFogEnabled;
float gFogStart;
float gFogRange;
}
// ...
struct Vertex3DIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct Vertex3DOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float2 Tex : TEXCOORD;
};
Basic_VS_3D.hlsl
也與之前一樣,沒有什麼變動:
// Basic_VS_3D.hlsl
#include "Basic.fx"
// 頂點著色器(3D)
Vertex3DOut VS_3D(Vertex3DIn pIn)
{
Vertex3DOut pOut;
row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);
pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);
pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy;
return pOut;
}
而Basic_PS_3D.hlsl
現在使用了4盞方向光以保證4種不同方向的光能夠均勻照射,並添加了霧效部分的處理:
// Basic_PS_3D.hlsl
#include "Basic.fx"
// 像素著色器(3D)
float4 PS_3D(Vertex3DOut pIn) : SV_Target
{
// 提前進行裁剪,對不符合要求的像素可以避免後續運算
float4 texColor = tex.Sample(sam, pIn.Tex);
clip(texColor.a - 0.05f);
// 標準化法向量
pIn.NormalW = normalize(pIn.NormalW);
// 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
float3 toEyeW = normalize(gEyePosW - pIn.PosW);
float distToEye = distance(gEyePosW, 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);
[unroll]
for (int i = 0; i < 4; ++i)
{
ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
float4 litColor = texColor * (ambient + diffuse) + spec;
// 霧效部分
[flatten]
if (gFogEnabled)
{
// 限定在0.0f到1.0f範圍
float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
// 根據霧色和光照顏色進行線性插值
litColor = lerp(litColor, gFogColor, fogLerp);
}
litColor.a = texColor.a * gMaterial.Diffuse.a;
return litColor;
}
對於白天來說,我們可以使用RGBA=(0.75f, 0.75f, 0.75f, 1.0f)
來作為霧的顏色。
而對於黑夜來說,這個霧效更像是戰爭迷霧的效果,我們使用RGBA=(0.0f, 0.0f, 0.0f, 1.0f)
來作為霧的顏色,這樣遠處的物體我們就讓它看不見,而在可視範圍內,距離越遠的物體能見度越低。
具體的演示效果在最後可以看到。
樹的公告板效果
當一棵樹離攝像機太遠的話,我們可以使用公告板技術,用一張樹的貼圖來進行繪製,取代原來繪製3D樹模型的方式。首先我們給出樹的紋理貼圖組成:
關註Alpha通道部分,白色區域指代Alpha值為1.0(完全不透明),而黑色區域指代Alpha值0.0(完全透明)。所以在渲染樹紋理的時候,我們只需要對Alpha值為0.0的像素區域進行裁剪即可。
實現公告板的關鍵點在於:公告板要永遠正向攝像機(即視線要與公告板錶面垂直),使得用戶的視線在x0z面上的投影一直與貼圖錶面垂直。這樣做省去了大量頂點的輸入和處理,顯得更加高效,並且這個小技巧還能夠欺騙玩家讓人誤以為還是原來的3D模型(眼尖的玩家還是有可能認得出來),只要你別一開始就告訴人家這棵樹的繪製用了公告板原理就行了(→_→)。
現在不考慮坐標系的Y軸部分(即從上方俯視),從下麵的圖可以看到,公告板投影的中心部分的法向量是直接指向攝像機的。
因此我們可以得到公告板的u軸, v軸和w軸單位向量以及根據公告板構建的局部坐標系:
\(\mathbf{w}=\frac{(E_x-C_x,0,E_z-C_z)}{E_x-C_x,0,E_z-C_z}\)
\(\mathbf{v}=(0,1,0)\)
\(\mathbf{u}=\mathbf{v}\times\mathbf{w}\)
然後已知中心頂點位置、樹寬度和高度,就可以求得2D樹矩形的四個頂點了:
// 計算出公告板矩形的四個頂點
// up
// v1___|___v3
// | | |
// right__|___| |
// |__/____|
// v0 / v2
// look
v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f);
v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f);
v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f);
v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f);
註意上面的加減運算是針對float3
進行的,然後用1.0f填充成4D向量。並且由於每個公告板所處的局部坐標系不一樣,我們需要對它們分別計算出對應的坐標軸向量。
若現在我們需要繪製公告板,則在輸入的時候僅提供對應的中心頂點,然後圖元類型選擇D3D11_PRIMITIVE_TOPOLOGY_POINTLIST
,在幾何著色階段我們直接將頂點直傳到幾何著色階段,這些頂點傳遞給幾何著色器後就會解釋成一個個矩形(兩個三角形),產生公告板。
HLSL代碼
下麵是Basic.fx
的完整代碼:
// Basic.fx
#include "LightHelper.hlsli"
Texture2D tex : register(t0);
Texture2DArray texArray : register(t1);
SamplerState sam : register(s0);
cbuffer CBChangesEveryDrawing : register(b0)
{
row_major matrix gWorld;
row_major matrix gWorldInvTranspose;
row_major matrix gTexTransform;
Material gMaterial;
}
cbuffer CBChangesEveryFrame : register(b1)
{
row_major matrix gView;
float3 gEyePosW;
}
cbuffer CBDrawingStates : register(b2)
{
float4 gFogColor;
int gFogEnabled;
float gFogStart;
float gFogRange;
}
cbuffer CBChangesOnResize : register(b3)
{
row_major matrix gProj;
}
cbuffer CBNeverChange : register(b4)
{
DirectionalLight gDirLight[4];
}
struct Vertex3DIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct Vertex3DOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float2 Tex : TEXCOORD;
};
struct PointSprite
{
float3 PosW : POSITION;
float2 SizeW : SIZE;
};
struct BillboardVertex
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
uint PrimID : SV_PrimitiveID;
};
對於頂點著色器,僅負責頂點的直傳:
// Billboard_VS.hlsl
#include "Basic.fx"
PointSprite VS(PointSprite pIn)
{
return pIn;
}
而幾何著色器的代碼如下:
// Billboard_GS.hlsl
#include "Basic.fx"
// 節省記憶體資源,先用float4向量聲明。
static const float4 gVec[2] = { float4(0.0f, 1.0f, 0.0f, 0.0f), float4(1.0f, 1.0f, 1.0f, 0.0f) };
static const float2 gTex[4] = (float2[4])gVec;
[maxvertexcount(4)]
void GS(point PointSprite input[1], uint primID : SV_PrimitiveID,
inout TriangleStream<BillboardVertex> output)
{
// 計算公告板所處的局部坐標系,其中公告板相當於
// 被投影在了局部坐標系的xy平面,z軸
float3 up = float3(0.0f, 1.0f, 0.0f);
float3 look = gEyePosW - input[0].PosW;
look.y = 0.0f; // look向量只取投影到xz平面的向量
look = normalize(look);
float3 right = cross(up, look);
// 計算出公告板矩形的四個頂點
// up
// v1 ___|___ v3
// | | |
// right__|___| |
// | / |
// |_/_____|
// v0 / v2
// look
float4 v[4];
float3 center = input[0].PosW;
float halfWidth = 0.5f * input[0].SizeW.x;
float halfHeight = 0.5f * input[0].SizeW.y;
v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f);
v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f);
v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f);
v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f);
// 對頂點位置進行矩陣變換,並以TriangleStrip形式輸出
BillboardVertex gOut;
row_major matrix viewProj = mul(gView, gProj);
[unroll]
for (int i = 0; i < 4; ++i)
{
gOut.PosW = v[i].xyz;
gOut.PosH = mul(v[i], viewProj);
gOut.NormalW = look;
gOut.Tex = gTex[i];
gOut.PrimID = primID;
output.Append(gOut);
}
}
首先一開始不用float2數組是因為每個float2元素會單獨打包,浪費了一半的空間,因此這裡採取一種特殊的語法形式使得記憶體可以得到充分利用。
然後要註意maxvertexcount
的值要設為4,儘管Append的次數為4,但實際上輸出的三角形頂點數為6。
圖元ID
現在講述系統值SV_PrimitiveID
,我們可以將它作為函數的額外形參進行提供。它告訴我們在輸入裝配階段下自動分配的圖元ID值。當我們調用了一個draw方法,需要繪製n個圖元,那麼第一個圖元對應的ID值為0,第二個為1,直到最後一個為n-1.當前的所有圖元ID僅在當前的單次調用繪製是唯一的。其中該系統值的寫入操作允許在幾何著色器和像素著色器進行,而讀取操作則允許在幾何/像素/外殼/域著色器中進行。
在上面的例子中,我們將一個頂點產生的矩形四個頂點都標記為同一個圖元ID,是因為到後續的像素著色器中,我們用該圖元ID映射到紋理數組的索引值,來對應到要繪製的樹的紋理。
註意: 如果幾何著色器沒有提供圖元ID,在像素著色器中也可以將它加進參數列表中以使用:
float4 PS(Vertex3DOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
// Pixel shader body…
}
但如果像素著色器提供了圖元ID,渲染管線又綁定了幾何著色器,則幾何著色器必須提供該參數。在幾何著色器中你可以使用或修改圖元ID值。
頂點ID
緊接著是系統值SV_VertexID
,在輸入裝配階段的時候渲染管線就會為這些輸入的頂點分配頂點ID值。若使用的是Draw
方法,則這些頂點將會按順序從0到n-1被標記(n為頂點數目);若使用的是DrawIndexed
方法,則頂點ID對應到的是該頂點所處的索引值。該參數僅能在頂點著色器的參數列表中提供:
VertexOut VS(VertexIn vin, uint vertID : SV_VertexID)
{
// vertex shader body…
}
最後給出像素著色器的代碼:
// Billboard_PS.hlsl
#include "Basic.fx"
float4 PS(BillboardVertex pIn) : SV_Target
{
// 每4棵樹一個迴圈,儘量保證出現不同的樹
float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4));
// 提前進行裁剪,對不符合要求的像素可以避免後續運算
clip(texColor.a - 0.05f);
// 標準化法向量
pIn.NormalW = normalize(pIn.NormalW);
// 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
float3 toEyeW = normalize(gEyePosW - pIn.PosW);
float distToEye = distance(gEyePosW, 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);
[unroll]
for (int i = 0; i < 4; ++i)
{
ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
float4 litColor = texColor * (ambient + diffuse) + spec;
// 霧效部分
[flatten]
if (gFogEnabled)
{
// 限定在0.0f到1.0f範圍
float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
// 根據霧色和光照顏色進行線性插值
litColor = lerp(litColor, gFogColor, fogLerp);
}
litColor.a = texColor.a * gMaterial.Diffuse.a;
return litColor;
}
這裡加上了剛纔的霧效,並使用了紋理數組。
紋理數組
之前在C++代碼層中,我們的每一張紋理使用ID3D11Texture2D
的介面對象去單獨存儲。但實際上在我們創建ID3D11Texture2D
對象的時候,我們可以設置它的ArraySize
來指定該對象可以存放的紋理數目。
但是我們創建紋理並不是使用D3DX
系列的函數,因為我們根本就不使用DirectX SDK。在之前我們創建紋理使用的是DDSTextureLoader.h
和WICTextureLoader.h
中的函數。這裡再提及一下,這兩個頭文件對應的庫可以在下麵兩個途徑找到:
回到HLSL代碼,我們之所以不使用下麵的這種形式創建紋理數組:
Texture2D TexArray[4];
float4 PS(GeoOut pin) : SV_Target
{
float4 c = TexArray[pin.PrimID%4].Sample(samLinear, pin.Tex);
是因為這樣做的話HLSL編譯器會報錯:sampler array index must be a literal experssion,即pin.PrimID的值也必須是個字面值,而不是變數。但我們還是想要能夠根據變數取對應紋理的能力。
正確的做法應當是聲明一個Texture2DArray
的數組:
Texture2DArray texArray : register(t1);
這裡使用的是索引為1的紋理寄存器是因為前面還有一個紋理已經綁定了t0.
紋理數組的採樣
Texture2DArray
同樣也具有Sample
方法:
// 每4棵樹一個迴圈,儘量保證出現不同的樹
float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4));
第一個參數依然是採樣器
而第二個參數則是一個3D向量,其中x與y的值對應的還是紋理坐標,而z分量即便是個float
,主要是用於作為索引值選取紋理數組中的某一個具體紋理。同理索引值0對應紋理數組的第一張紋理,1對應的是第二張紋理等等...
在我們的這個demo中,紋理數組存放了4張不同樣式的樹的紋理貼圖,然後用SV_Primitive
模4的值來決定哪張樹紋理貼圖將被繪製。
使用紋理數組的優勢是,我們可以一次性預先創建好所有需要用到的紋理,並綁定到HLSL的紋理數組中,而不需要每次都重新綁定一個紋理。然後我們再使用索引值來訪問紋理數組中的某一紋理。
紋理數組的載入
現在我們手頭上僅有的就是DDSTextureLoader.h
和WICTextureLoader.h
中的函數,但這裡面的函數每次都只能載入一張紋理。我們還需要修改龍書樣例中讀取紋理的函數,具體的操作順序如下:
- 一個個讀取存有紋理的文件,創建出一系列
ID3D11Texture2D
對象,這裡的每個對象單獨包含一張紋理; - 創建一個
ID3D11Texture2D
對象,它同時也是一個紋理數組; - 將之前讀取的所有紋理有條理地複製到剛創建的紋理數組對象中;
- 為該紋理數組對象創建創建一個紋理資源視圖(Shader Resource View)。
首先我們需要瞭解增強版的紋理創建函數。
CreateDDSTextureFromFileEx函數--使用更多的參數,從文件中讀取DDS紋理
HRESULT CreateDDSTextureFromFileEx(
ID3D11Device* d3dDevice, // [In]D3D設備
ID3D11DeviceContext* d3dContext, // [In]D3D設備上下文(可選)
const wchar_t* szFileName, // [In].dds文件名
size_t maxsize, // [In]最大允許mipmap等級,預設0
D3D11_USAGE usage, // [In]D3D11_USAGE枚舉值類型,指定CPU/GPU讀寫許可權
unsigned int bindFlags, // [In]綁定標簽,指定它可以被綁定到什麼對象上
unsigned int cpuAccessFlags, // [In]CPU訪問許可權標簽
unsigned int miscFlags, // [In]雜項標簽,忽略
bool forceSRGB, // [In]強制使用SRGB,預設false
ID3D11Resource** texture, // [Out]獲取創建好的紋理(可選)
ID3D11ShaderResourceView** textureView, // [Out]獲取創建好的紋理資源視圖(可選)
DDS_ALPHA_MODE* alphaMode = nullptr); // [Out]忽略(可選)
也就是說,圖片的數據格式、寬度、高度等信息都是隨文件讀取的時候獲得的,我們無法在這裡指定。所以我們要求提供的所有DDS紋理寬度、高度、數據格式都應當一致。對於數據格式不一致的,我們可以使用DirectX Texture Tool
來修改,但是該程式包含在DirectX SDK中,這裡我在Github上嘗試提供單獨的DxTex.exe
程式看能不能直接使用。現在我預先確保Demo中的4張樹紋理都設置為同樣的數據格式。
第一步,讀取一系列紋理的代碼如下:
//
// 1. 讀取所有紋理
//
size_t size = filenames.size();
std::vector<ComPtr<ID3D11Texture2D>> srcTex(size);
UINT mipLevel = maxMipMapSize;
UINT width, height;
DXGI_FORMAT format;
for (size_t i = 0; i < size; ++i)
{
// 由於這些紋理並不會被GPU使用,我們使用D3D11_USAGE_STAGING枚舉值
// 使得CPU可以讀取資源
HR(CreateDDSTextureFromFileEx(device.Get(),
deviceContext.Get(),
filenames[i].c_str(),
maxMipMapSize,
D3D11_USAGE_STAGING, // Usage
0, // BindFlags
D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ, // CpuAccessFlags
0, // MiscFlags
false,
(ID3D11Resource**)srcTex[i].GetAddressOf(),
nullptr));
// 讀取創建好的紋理Mipmap等級, 寬度和高度
D3D11_TEXTURE2D_DESC texDesc;
srcTex[i]->GetDesc(&texDesc);
if (i == 0)
{
mipLevel = texDesc.MipLevels;
width = texDesc.Width;
height = texDesc.Height;
format = texDesc.Format;
}
// 這裡斷言所有紋理的MipMap等級,寬度和高度應當一致
assert(mipLevel == texDesc.MipLevels);
assert(texDesc.Width == width && texDesc.Height == height);
// 這裡要求所有提供的圖片數據格式應當是一致的,若存在不一致的情況,請
// 使用dxtex.exe(DirectX Texture Tool)將所有的圖片轉成一致的數據格式
assert(texDesc.Format == format);
}
接下來的第二步就是創建紋理數組,我們使用第一個紋理的描述去填充紋理數組的一部分描述:
//
// 2.創建紋理數組
//
D3D11_TEXTURE2D_DESC texDesc, texArrayDesc;
srcTex[0]->GetDesc(&texDesc);
texArrayDesc.Width = texDesc.Width;
texArrayDesc.Height = texDesc.Height;
texArrayDesc.MipLevels = texDesc.MipLevels;
texArrayDesc.ArraySize = size;
texArrayDesc.Format = texDesc.Format;
texArrayDesc.SampleDesc.Count = 1;
texArrayDesc.SampleDesc.Quality = 0;
texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texArrayDesc.CPUAccessFlags = 0;
texArrayDesc.MiscFlags = 0;
ComPtr<ID3D11Texture2D> texArray;
HR(device->CreateTexture2D(&texArrayDesc, nullptr, texArray.GetAddressOf()));
在第三步進行複製之前,我們還需要瞭解紋理的子資源
紋理子資源(Texture Subresources)
從WICTextureLoader
或者DDSTextureLoader
讀取出來的紋理數據實際上並不是由單純的一個二維數組構成,而是多個不同大小的二維數組,不同的mipmap等級對應不同的二維數組,這些二維數組都是該紋理的子資源。比如512x512的紋理載入進來包含的mipmap等級數(Mipmap Levels)為10,包含了從512x512, 256x256, 128x128...到1x1的10個二維數組顏色數據,而Direct3D API使用Mip切片(Mip slice)來指定某一mipmap等級的紋理,也有點像索引。比如mip slice值為0時,對應的是512x512的紋理,而mip slice值1對應的是256x256,以此類推。
對於紋理數組,每個元素就上面說的單個紋理對應的mipmap鏈,Direct3D API使用數組切片(array slice)來訪問不同紋理,也是相當於索引.這樣我們就可以把所有的紋理資源用下麵的圖來表示,假定下圖有4個紋理,每個紋理包含3個子資源,則當前指定的是Array Slice為2,Mip Slice為1的子資源。
D3D11CalcSubresource函數--計運算元資源的索引值
對於紋理數組的每一個子資源都可以用一個一維的索引值訪問,索引值的增減是以Mip切片值為主遞增的。
然後給定當前紋理數組的mipmap等級數(Mipmap Levels),數組切片(Array Slice)和Mip切片(Mip Slice),我們就可以用下麵的函數來求得指定子資源的索引值:
inline UINT D3D11CalcSubresource(UINT MipSlice, UINT ArraySlice, UINT MipLevels )
{ return MipSlice + ArraySlice * MipLevels; }
然後是映射相關的兩個函數
ID3D11DeviceContext::Map函數--獲取指向子資源中數據的指針並拒絕GPU對該子資源的訪問
HRESULT ID3D11DeviceContext::Map(
ID3D11Resource *pResource, // [In]包含ID3D11Resource介面的資源對象
UINT Subresource, // [In]子資源索引
D3D11_MAP MapType, // [In]D3D11_MAP枚舉值,指定讀寫相關操作
UINT MapFlags, // [In]填0,忽略
D3D11_MAPPED_SUBRESOURCE *pMappedResource // [Out]獲取到的已經映射到記憶體的子資源
);
D3D11_MAP枚舉值類型的成員如下:
D3D11_MAP成員 | 含義 |
---|---|
D3D11_MAP_READ | 映射到記憶體的資源用於讀取。該資源在創建的時候必須綁定了D3D11_CPU_ACCESS_READ標簽 |
D3D11_MAP_WRITE | 映射到記憶體的資源用於寫入。該資源在創建的時候必須綁定了D3D11_CPU_ACCESS_WRITE標簽 |
D3D11_MAP_READ_WRITE | 映射到記憶體的資源用於讀寫。該資源在創建的時候必須綁定了D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE標簽 |
D3D11_MAP_WRITE_DISCARD | 映射到記憶體的資源用於寫入,之前的資源數據將會被拋棄。該資源在創建的時候必須綁定了D3D11_CPU_ACCESS_WRITE和D3D11_USAGE_DYNAMIC標簽 |
D3D11_MAP_WRITE_NO_OVERWRITE | 映射到記憶體的資源用於寫入,但不能覆寫已經存在的資源。該枚舉值只能用於頂點/索引緩衝區。該資源在創建的時候需要有D3D11_CPU_ACCESS_WRITE標簽,在Direct3D 11不能用於設置了D3D11_BIND_CONSTANT_BUFFER標簽的資源,但在11.1後可以。具體可以查閱MSDN文檔 |
獲取到的結構體D3D11_MAPPED_SUBRESOURCE
成員如下:
typedef struct D3D11_MAPPED_SUBRESOURCE {
void *pData;
UINT RowPitch;
UINT DepthPitch;
};
首先pData
指向的是映射到記憶體上的子資源首元素地址,即對應Row和Depth Slice值都為0的位置
其次RowPitch
通常是一行元素占用的位元組數,對於512x512的紋理來說,若它使用DXGI_R8G8B8A8_UNORM
數據類型,則一行占用了512像素*4位元組/像素=2048位元組。它的每個子資源的RowPitch
都是一致的,這樣方便其進行下一行跳轉,即便在記憶體上會有些許的浪費,在mipmap等級越高時也有所體現,但一個完整的mipmap鏈消耗的記憶體也就逼近原來單張紋理所占位元組數的2倍而已。
DepthPitch
在2D紋理的含義則是當前子資源占用的位元組數。像剛纔說的那樣,你在調試器上觀察每個mipmap等級對應的DepthPitch
值,可以發現mip slice增加1,子資源占用的位元組數是原來的1/2,而不是原來的1/4。這可以說明每行占用的位元組數是不會改變的。
ID3D11DeviceContext::UpdateSubresource函數[2]--將記憶體數據拷貝到不可進行映射的子資源中
這個函數在之前我們主要是用來將記憶體數據拷貝到常量緩衝區中,現在我們也可以用它將記憶體數據拷貝到紋理的子資源當中:
void ID3D11DeviceContext::UpdateSubresource(
ID3D11Resource *pDstResource, // [In]目標資源對象
UINT DstSubresource, // [In]對於2D紋理來說,該參數為指定Mipmap等級的子資源
const D3D11_BOX *pDstBox, // [In]這裡填nullptr
const void *pSrcData, // [In]用於拷貝的記憶體數據
UINT SrcRowPitch, // [In]該2D紋理的 寬度*數據格式的位數
UINT SrcDepthPitch // [In]對於2D紋理來說並不需要用到該參數,因此可以任意設置
);
ID3D11DeviceContext::UnMap函數--讓指向資源的指針無效並重新啟用GPU對該資源的訪問許可權
void ID3D11DeviceContext::Unmap(
ID3D11Resource *pResource, // [In]包含ID3D11Resource介面的資源對象
UINT Subresource // [In]需要取消的子資源索引
);
第三步的具體代碼如下:
//
// 3.將所有的紋理子資源賦值到紋理數組中
//
// 每個紋理元素
for (size_t i = 0; i < size; ++i)
{
// 紋理中的每個mipmap等級
for (UINT j = 0; j < mipLevel; ++j)
{
D3D11_MAPPED_SUBRESOURCE mappedTex2D;
// 允許映射索引i紋理中,索引j的mipmap等級的2D紋理
HR(deviceContext->Map(srcTex[i].Get(),
j, D3D11_MAP_READ, 0, &mappedTex2D));
deviceContext->UpdateSubresource(
texArray.Get(),
D3D11CalcSubresource(j, i, mipLevel), // i * mipLevel + j
nullptr,
mappedTex2D.pData,
mappedTex2D.RowPitch,
mappedTex2D.DepthPitch);
// 停止映射
deviceContext->Unmap(srcTex[i].Get(), j);
}
}
最後一步就是要創建著色器資源視圖。
ID3D11Device::CreateShaderResourceView--創建著色器資源視圖
HRESULT ID3D11Device::CreateShaderResourceView(
ID3D11Resource *pResource, // [In]待綁定資源
const D3D11_SHADER_RESOURCE_VIEW_DESC *pDesc, // [In]著色器資源視圖描述
ID3D11ShaderResourceView **ppSRView // [Out]獲取創建的著色器資源視圖
);
所以還需要填充D3D11_SHADER_RESOURCE_VIEW_DESC結構體:
typedef struct D3D11_SHADER_RESOURCE_VIEW_DESC
{
DXGI_FORMAT Format; // 數據格式
D3D11_SRV_DIMENSION ViewDimension; // 視圖維度,決定下麵需要填充哪個共用體成員
union
{
D3D11_BUFFER_SRV Buffer;
D3D11_TEX1D_SRV Texture1D;
D3D11_TEX1D_ARRAY_SRV Texture1DArray;
D3D11_TEX2D_SRV Texture2D;
D3D11_TEX2D_ARRAY_SRV Texture2DArray;
D3D11_TEX2DMS_SRV Texture2DMS;
D3D11_TEX2DMS_ARRAY_SRV Texture2DMSArray;
D3D11_TEX3D_SRV Texture3D;
D3D11_TEXCUBE_SRV TextureCube;
D3D11_TEXCUBE_ARRAY_SRV TextureCubeArray;
D3D11_BUFFEREX_SRV BufferEx;
};
} D3D11_SHADER_RESOURCE_VIEW_DESC;
最後一步的代碼如下:
//
// 4.創建紋理數組的SRV
//
D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
viewDesc.Format = texArrayDesc.Format;
viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
viewDesc.Texture2DArray.MostDetailedMip = 0;
viewDesc.Texture2DArray.MipLevels = texArrayDesc.MipLevels;
viewDesc.Texture2DArray.FirstArraySlice = 0;
viewDesc.Texture2DArray.ArraySize = size;
ComPtr<ID3D11ShaderResourceView> texArraySRV;
HR(device->CreateShaderResourceView(texArray.Get(), &viewDesc, texArraySRV.GetAddressOf()));
// 已經確保所有資源由ComPtr管理,無需手動釋放
return texArraySRV;
CreateDDSTexture2DArrayShaderResourceView函數--創建用於DDS紋理的數組著色器資源視圖
該函數放到了GameApp類中,你也可以單獨抽離出來。
ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView(
ComPtr<ID3D11Device> device, // [In]D3D設備
ComPtr<ID3D11DeviceContext> deviceContext, // [In]D3D設備上下文
const std::vector<std::wstring>& filenames, // [In]文件名數組
int maxMipMapSize); // [In]最大允許mipmap等級,若為0,則使用預設紋理mipmap等級
具體的函數實現就是上面四步的所有代碼。
Alpha-To-Coverage
在Demo運行的時候,仔細觀察可以發現樹公告板的某些邊緣部分有一些比較突出的黑邊。
這是因為當前預設使用的是Alpha Test,即HLSL中使用clip函數將Alpha值為0的像素點給剔除掉,這些像素也不是樹的一部分。該函數決定某一像素是留下還是拋棄,這會導致不平滑的過渡現象,在攝像機逐漸靠近該紋理時,圖片本身也在不斷放大,硬邊部分也會被放大,就像下麵那張圖:
當然,你也可以使用透明混合的方式,但是透明混合對繪製的順序是有要求的,要求透明物體按從後到前的順序進行繪製,即需要在繪製透明物體前先對物體按到攝像機的距離排個序。當然如果需要繪製大量的草叢的話,這種方法所需要的開銷會變得非常大,操作起來也十分麻煩。
當然,我們可以考慮下使用MSAA(多重採樣抗鋸齒),並配合Alpha Test進行。MSAA可以用於將多邊形的鋸齒邊緣平滑處理,然後讓Direct3D開啟alpha-to-coverage技術,標記邊緣部分。
首先在創建後備緩衝區、深度/模板緩衝區的時候需要打開4倍多重採樣的支持,我們只需要在GameApp的構造函數中這樣寫即可:
GameApp::GameApp(HINSTANCE hInstance)
: D3DApp(hInstance)
{
// 開啟4倍多重採樣
mEnable4xMsaa = true;
}
然後在之前的例子里,我們已經在RenderStates
類中預先創建好了混合狀態:
D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
auto& rtDesc = blendDesc.RenderTarget[0];
// Alpha-To-Coverage模式
blendDesc.AlphaToCoverageEnable = true;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = false;
rtDesc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
HR(device->CreateBlendState(&blendDesc, BSAlphaToCoverage.ReleaseAndGetAddressOf()));
然後只需要在需要的時候綁定該狀態即可。
BasicFX.h的變化
常量緩衝區對應的結構體和BasicFX類的變化如下:
#ifndef BASICFX_H
#define BASICFX_H
#include <wrl/client.h>
#include <d3d11_1.h>
#include <d3dcompiler.h>
#include <directxmath.h>
#include <vector>
#include "LightHelper.h"
#include "RenderStates.h"
#include "Vertex.h"
// 由於常量緩衝區的創建需要是16位元組的倍數,該函數可以返回合適的位元組大小
inline UINT Align16Bytes(UINT size)
{
return (size + 15) & (UINT)(-16);
}
struct CBChangesEveryDrawing
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX worldInvTranspose;
DirectX::XMMATRIX texTransform;
Material material;
};
struct CBChangesEveryFrame
{
DirectX::XMMATRIX view;
DirectX::XMFLOAT4 eyePos;
};
struct CBDrawingStates
{
DirectX::XMFLOAT4 fogColor;
int fogEnabled;
float fogStart;
float fogRange;
float pad;
};
struct CBChangesOnResize
{
DirectX::XMMATRIX proj;
};
struct CBNeverChange
{
DirectionalLight dirLight[4];
};
class BasicFX
{
public:
// 使用模板別名(C++11)簡化類型名
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
// 初始化Basix.fx所需資源並初始化光柵化狀態
bool InitAll(ComPtr<ID3D11Device> device);
// 是否已經初始化
bool IsInit() const;
template <class T>
void UpdateConstantBuffer(const T& cbuffer);
// 預設狀態繪製
void SetRenderDefault();
// 公告板繪製
void SetRenderBillboard(bool enableAlphaToCoverage);
private:
// objFileNameInOut為編譯好的著色器二進位文件(.*so),若有指定則優先尋找該文件並讀取
// hlslFileName為著色器代碼,若未找到著色器二進位文件則編譯著色器代碼
// 編譯成功後,若指定了objFileNameInOut,則保存編譯好的著色器二進位信息到該文件
// ppBlobOut輸出著色器二進位信息
HRESULT CreateShaderFromFile(const WCHAR* objFileNameInOut, const WCHAR* hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut);
private:
ComPtr<ID3D11VertexShader> mBasicVS;
ComPtr<ID3D11PixelShader> mBasicPS;
ComPtr<ID3D11VertexShader> mBillboardVS;
ComPtr<ID3D11GeometryShader> mBillboardGS;
ComPtr<ID3D11PixelShader> mBillboardPS;
ComPtr<ID3D11InputLayout> mVertexPosSizeLayout; // 點精靈輸入佈局
ComPtr<ID3D11InputLayout> mVertexPosNormalTexLayout; // 3D頂點輸入佈局
ComPtr<ID3D11DeviceContext> md3dImmediateContext; // 設備上下文
std::vector<ComPtr<ID3D11Buffer>> mConstantBuffers; // 常量緩衝區
};
#endif
初始化函數和SetRenderDeafult
方法這裡就不贅述了。
BasicFX::SetRenderBillboard方法--公告板繪製
實現如下:
void BasicFX::SetRenderBillboard(bool enableAlphaToCoverage)
{
md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
md3dImmediateContext->IASetInputLayout(mVertexPosSizeLayout.Get());
md3dImmediateContext->VSSetShader(mBillboardVS.Get(), nullptr, 0);
md3dImmediateContext->GSSetShader(mBillboardGS.Get(), nullptr, 0);
md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
md3dImmediateContext->PSSetShader(mBillboardPS.Get(), nullptr, 0);
md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
md3dImmediateContext->OMSetBlendState(
(enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr),
nullptr, 0xFFFFFFFF);
}
參數enableAlphaToCoverage
決定是否要綁定渲染狀態對象RenderStates::BSAlphaToCoverage
。
GameApp類的變化
類成員相關聲明如下:
class GameApp : public D3DApp
{
public:
// 攝像機模式
enum class CameraMode { FirstPerson, ThirdPerson, Free };
public:
GameApp(HINSTANCE hInstance);
~GameApp();
bool Init();
void OnResize();
void UpdateScene(float dt);
void DrawScene();
private:
bool InitResource();
void InitPointSpritesBuffer();
// 根據給定的DDS紋理文件集合,創建2D紋理數組
// 要求所有紋理的寬度和高度都一致
// 若maxMipMapSize為0,使用預設mipmap等級
// 否則,mipmap等級將不會超過maxMipMapSize
ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView(
ComPtr<ID3D11Device> device,
ComPtr<ID3D11DeviceContext> deviceContext,
const std::vector<std::wstring>& filenames,
int maxMipMapSize = 0);
private:
ComPtr<ID2D1SolidColorBrush> mColorBrush; // 單色筆刷
ComPtr<IDWriteFont> mFont; // 字體
ComPtr<IDWriteTextFormat> mTextFormat; // 文本格式
ComPtr<ID3D11Buffer> mPointSpritesBuffer; // 點精靈頂點緩衝區
ComPtr<ID3D11ShaderResourceView> mTreeTexArray; // 樹的紋理數組
Material mTreeMat; // 樹的材質
GameObject mGround; // 地面
BasicFX mBasicFX; // Basic特效管理類
CameraMode mCameraMode; // 攝像機模式
std::shared_ptr<Camera> mCamera; // 攝像機
bool mIsNight; // 是否黑夜
bool mEnableAlphaToCoverage; // 是否開啟Alpha-To-Coverage
CBChangesEveryDrawing mCBChangesEveryDrawing; // 該緩衝區存放每次繪製更新的變數
CBChangesEveryFrame mCBChangesEveryFrame; // 該緩衝區存放每幀更新的變數
CBDrawingStates mCBDrawingStates; // 該緩衝區存放繪製狀態
CBChangesOnResize mCBChangesOnReSize; // 該緩衝區存放僅在視窗大小變化時更新的變數
CBNeverChange mCBNeverChange; // 該緩衝區存放不會再進行修改的變數
};
GameApp::InitPointSpritesBuffer方法--初始化存放點精靈的緩衝區
該方法會生成20個頂點,均勻並略帶隨機性地環繞在原點周圍。這些頂點一經創建就不可以被修改了,它們將會被用於公告板的創建:
void GameApp::InitPointSpritesBuffer()
{
srand((unsigned)time(nullptr));
VertexPosSize vertexes[16];
float theta = 0.0f;
for (int i = 0; i < 16; ++i)
{
// 取20-50的半徑放置隨機的樹
float radius = (float)(rand() % 31 + 20);
float randomRad = rand() % 256 / 256.0f * XM_2PI / 16;
vertexes[i].pos = XMFLOAT3(radius * cosf(theta + randomRad), 8.0f, radius * sinf(theta + randomRad));
vertexes[i].size = XMFLOAT2(30.0f, 30.0f);
theta += XM_2PI / 16;
}
// 設置頂點緩衝區描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_IMMUTABLE; // 數據不可修改
vbd.ByteWidth = sizeof (vertexes);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
// 新建頂點緩衝區
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = vertexes;
HR(md3dDevice->CreateBuffer(&vbd, &InitData, mPointSpritesBuffer.GetAddressOf()));
}
GameApp::InitResource方法--初始化資源
該方法集成了所有資源的初始化,註意樹的紋理數組要提供到輸入槽1,對應紋理寄存器t1的Texture2DArray
:
bool GameApp::InitResource()
{
// 預設白天,開啟AlphaToCoverage
mIsNight = false;
mEnableAlphaToCoverage = true;
// ******************
// 初始化各種物體
// 初始化樹紋理資源
mTreeTexArray = CreateDDSTexture2DArrayShaderResourceView(
md3dDevice,
md3dImmediateContext,
std::vector<std::wstring>{
L"Texture\\tree0.dds",
L"Texture\\tree1.dds",
L"Texture\\tree2.dds",
L"Texture\\tree3.dds"});
// 初始化點精靈緩衝區
InitPointSpritesBuffer();
// 初始化樹的材質
mTreeMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mTreeMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
mTreeMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
ComPtr<ID3D11ShaderResourceView> texture;
// 初始化地板
mGround.SetBuffer(md3dDevice, Geometry::CreatePlane(XMFLOAT3(0.0f, -5.0f, 0.0f), XMFLOAT2(100.0f, 100.0f), XMFLOAT2(10.0f, 10.0f)));
HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\Grass.dds", nullptr, texture.GetAddressOf()));
mGround.SetTexture(texture);
Material material;
material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
material.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
mGround.SetMaterial(material);
mGround.SetWorldMatrix(XMMatrixIdentity());
mGround.SetTexTransformMatrix(XMMatrixIdentity());
// ******************
// 初始化常量緩衝區的值
mCBChangesEveryDrawing.material = mTreeMat;
mCBChangesEveryDrawing.world = mCBChangesEveryDrawing.worldInvTranspose = XMMatrixIdentity();
mCBChangesEveryDrawing.texTransform = XMMatrixIdentity();
// 方向光
mCBNeverChange.dirLight[0].Ambient = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
mCBNeverChange.dirLight[0].Diffuse = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);
mCBNeverChange.dirLight[0].Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
mCBNeverChange.dirLight[0].Direction = XMFLOAT3(-0.577f, -0.577f, 0.577f);
mCBNeverChange.dirLight[1] = mCBNeverChange.dirLight[0];
mCBNeverChange.dirLight[1].Direction = XMFLOAT3(0.577f, -0.577f, 0.577f);
mCBNeverChange.dirLight[2] = mCBNeverChange.dirLight[0];
mCBNeverChange.dirLight[2].Direction = XMFLOAT3(0.577f, -0.577f, -0.577f);
mCBNeverChange.dirLight[3] = mCBNeverChange.dirLight[0];
mCBNeverChange.dirLight[3].Direction = XMFLOAT3(-0.577f, -0.577f, -0.577f);
// 攝像機相關
mCameraMode = CameraMode::Free;
auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
mCamera = camera;
camera->SetPosition(XMFLOAT3());
camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f);
camera->LookTo(
XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f),
XMVectorSet(0.0f, 0.0f, 1.0f, 1.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f));
camera->UpdateViewMatrix();
mCBChangesEveryFrame.view = camera->GetView();
XMStoreFloat4(&mCBChangesEveryFrame.eyePos, camera->GetPositionXM());
mCBChangesOnReSize.proj = camera->GetProj();
// 霧狀態預設開啟
mCBDrawingStates.fogEnabled = 1;
mCBDrawingStates.fogColor = XMFLOAT4(0.75f, 0.75f, 0.75f, 1.0f); // 銀色
mCBDrawingStates.fogRange = 75.0f;
mCBDrawingStates.fogStart = 15.0f;
// 更新常量緩衝區資源
mBasicFX.UpdateConstantBuffer(mCBChangesEveryDrawing);
mBasicFX.UpdateConstantBuffer(mCBChangesEveryFrame);
mBasicFX.UpdateConstantBuffer(mCBChangesOnReSize);
mBasicFX.UpdateConstantBuffer(mCBDrawingStates);
mBasicFX.UpdateConstantBuffer(mCBNeverChange);
// 直接綁定樹的紋理
md3dImmediateContext->PSSetShaderResources(1, 1, mTreeTexArray.GetAddressOf());
return true;
}
其餘方法限於篇幅就不放在這裡了,讀者可以查看源碼觀察剩餘部分的代碼實現。現在來看實現效果吧。
實現效果
可以觀察到,在與公告版近距離接觸時可以很明顯地看到公告板在跟著攝像機旋轉。如果距離很遠的話轉動的幅度就會很小,用戶才會比較難以分辨出遠處物體是否為公告板或3D模型了。
下麵演示了白天和黑夜的霧效
最後則是Alpha-To-Coverage的開啟/關閉效果對比
DirectX11 With Windows SDK完整目錄