DirectX11 With Windows SDK--17 利用幾何着色器實現公告板效果


前言

上一章我們知道了如何使用幾何着色器將頂點通過流輸出階段輸出到綁定的頂點緩沖區。接下來我們繼續利用它來實現一些新的效果,在這一章,你將了解:

  1. 實現公告板效果
  2. Alpha-To-Coverage
  3. 對GPU資源進行讀/寫操作
  4. 紋理數組
  5. 實現霧效

在此之前需要額外了解的章節如下:

章節回顧
深入理解與使用2D紋理資源(重點閱讀紋理數組)
15 幾何着色器初探

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

實現霧效

雖然這部分與幾何着色器並沒有什么關系,但是霧的效果在該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 g_FogColor;
    int g_FogEnabled;
    float g_FogStart;
    float g_FogRange;
    float g_Pad2;
}

// ...
struct VertexPosNormalTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct VertexPosHWNormalTex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION; // 在世界中的位置
    float3 NormalW : NORMAL; // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

Basic_VS.hlsl也與之前一樣,沒有什么變動:

// Basic_VS.hlsl
#include "Basic.hlsli"

// 頂點着色器
VertexPosHWNormalTex VS(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    
    matrix viewProj = mul(g_View, g_Proj);
    vector posW = mul(float4(vIn.PosL, 1.0f), g_World);

    vOut.PosW = posW.xyz;
    vOut.PosH = mul(posW, viewProj);
    vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
    vOut.Tex = vIn.Tex;
    return vOut;
}

Basic_PS.hlsl現在使用了4盞方向光以保證4種不同方向的光能夠均勻照射,並添加了霧效部分的處理:

// Basic_PS.hlsl
#include "Basic.hlsli"

// 像素着色器
float4 PS(VertexPosHWNormalTex pIn) : SV_Target
{
	// 提前進行裁剪,對不符合要求的像素可以避免后續運算
    float4 texColor = g_Tex.Sample(g_Sam, pIn.Tex);
    clip(texColor.a - 0.05f);

    // 標准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
    float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
    float distToEye = distance(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);

    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    float4 litColor = texColor * (ambient + diffuse) + spec;

    // 霧效部分
    [flatten]
    if (g_FogEnabled)
    {
        // 限定在0.0f到1.0f范圍
        float fogLerp = saturate((distToEye - g_FogStart) / g_FogRange);
        // 根據霧色和光照顏色進行線性插值
        litColor = lerp(litColor, g_FogColor, fogLerp);
    }

    litColor.a = texColor.a * g_Material.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軸單位向量以及根據公告板構建的局部坐標系:

\[\begin{align} \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} \end{align} \]

然后已知中心頂點位置、樹寬度和高度,就可以求得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.hlsli的完整代碼:

// Basic.hlsli

#include "LightHelper.hlsli"

Texture2D g_Tex : register(t0);
Texture2DArray g_TexArray : register(t1);
SamplerState g_Sam : register(s0);


cbuffer CBChangesEveryDrawing : register(b0)
{
    matrix g_World;
    matrix g_WorldInvTranspose;
    Material g_Material;
}

cbuffer CBChangesEveryFrame : register(b1)
{
    matrix g_View;
    float3 g_EyePosW;
    float g_Pad;
}

cbuffer CBDrawingStates : register(b2)
{
    float4 g_FogColor;
    int g_FogEnabled;
    float g_FogStart;
    float g_FogRange;
    float g_Pad2;
}

cbuffer CBChangesOnResize : register(b3)
{
    matrix g_Proj;
}

cbuffer CBChangesRarely : register(b4)
{
    DirectionalLight g_DirLight[5];
    PointLight g_PointLight[5];
    SpotLight g_SpotLight[5];
}



struct VertexPosNormalTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct VertexPosHWNormalTex
{
    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_GS.hlsl

#include "Basic.hlsli"

// 節省內存資源,先用float4向量聲明。
static const float4 g_Vec[2] = { float4(0.0f, 1.0f, 0.0f, 0.0f), float4(1.0f, 1.0f, 1.0f, 0.0f) };
static const float2 g_TexCoord[4] = (float2[4])g_Vec;

[maxvertexcount(4)]
void GS(point PointSprite input[1], uint primID : SV_PrimitiveID, 
    inout TriangleStream<BillboardVertex> output)
{
    // 計算公告板所處的局部坐標系,其中公告板相當於
    // 被投影在了局部坐標系的xy平面,z=0

    float3 up = float3(0.0f, 1.0f, 0.0f);
    float3 look = g_EyePosW - 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;
    matrix viewProj = mul(g_View, g_Proj);
    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        gOut.PosW = v[i].xyz;
        gOut.PosH = mul(v[i], viewProj);
        gOut.NormalW = look;
        gOut.Tex = g_TexCoord[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.hlsli"

float4 PS(BillboardVertex pIn) : SV_Target
{
	// 每4棵樹一個循環,盡量保證出現不同的樹
    float4 texColor = g_TexArray.Sample(g_Sam, float3(pIn.Tex, pIn.PrimID % 4));
    // 提前進行裁剪,對不符合要求的像素可以避免后續運算
    clip(texColor.a - 0.05f);

    // 標准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
    float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
    float distToEye = distance(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);

    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }

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

    // 霧效部分
    [flatten]
    if (g_FogEnabled)
    {
        // 限定在0.0f到1.0f范圍
        float fogLerp = saturate((distToEye - g_FogStart) / g_FogRange);
        // 根據霧色和光照顏色進行線性插值
        litColor = lerp(litColor, g_FogColor, fogLerp);
    }

    litColor.a = texColor.a * g_Material.Diffuse.a;
    return litColor;
}

這里加上了剛才的霧效,並使用了紋理數組。如果你對紋理數組這部分內容不熟悉的話,請回到開頭閱讀"深入理解與使用2D紋理資源"中的紋理數組部分

Alpha-To-Coverage

在Demo運行的時候,仔細觀察可以發現樹公告板的某些邊緣部分有一些比較突出的黑邊。

這是因為當前默認使用的是Alpha Test,即HLSL中使用clip函數將Alpha值為0的像素點給剔除掉,這些像素也不是樹的一部分。該函數決定某一像素是留下還是拋棄,這會導致不平滑的過渡現象,在攝像機逐漸靠近該紋理時,圖片本身也在不斷放大,硬邊部分也會被放大,就像下面那張圖:

當然,你也可以使用透明混合的方式,但是透明混合對繪制的順序是有要求的,要求透明物體按從后到前的順序進行繪制,即需要在繪制透明物體前先對物體按到攝像機的距離排個序。當然如果需要繪制大量的草叢的話,這種方法所需要的開銷會變得非常大,操作起來也十分麻煩。

當然,我們可以考慮下使用MSAA(多重采樣抗鋸齒),並配合Alpha Test進行。MSAA可以用於將多邊形的鋸齒邊緣平滑處理,然后讓Direct3D開啟alpha-to-coverage技術,標記邊緣部分。

在創建后備緩沖區、深度/模板緩沖區前需要打開4倍多重采樣的支持,目前的項目在d3dApp中已經默認開啟好了。

然后在之前的例子里,我們已經在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()));

然后只需要在需要的時候綁定該狀態即可。

BasicEffect的變化

BasicEffect::SetRenderBillboard方法--公告板繪制

該方法要考慮輸入的是一系列頂點圖元:

void BasicEffect::SetRenderBillboard(ID3D11DeviceContext * deviceContext, bool enableAlphaToCoverage)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexPosSizeLayout.Get());
	deviceContext->VSSetShader(pImpl->m_pBillboardVS.Get(), nullptr, 0);
	deviceContext->GSSetShader(pImpl->m_pBillboardGS.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSNoCull.Get());
	deviceContext->PSSetShader(pImpl->m_pBillboardPS.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(nullptr, 0);
	deviceContext->OMSetBlendState(
		(enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr),
		nullptr, 0xFFFFFFFF);

}

參數enableAlphaToCoverage決定是否要綁定渲染狀態對象RenderStates::BSAlphaToCoverage

GameApp類的變化

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(m_pd3dDevice->CreateBuffer(&vbd, &InitData, mPointSpritesBuffer.GetAddressOf()));
}

GameApp::InitResource方法--初始化資源

該方法集成了所有資源的初始化,注意樹的紋理數組要提供到輸入槽1,對應紋理寄存器t1的Texture2DArray

bool GameApp::InitResource()
{
	// ******************
	// 初始化各種物體
	//

	// 初始化樹紋理資源
	HR(CreateDDSTexture2DArrayFromFile(
		m_pd3dDevice.Get(),
		m_pd3dImmediateContext.Get(),
		std::vector<std::wstring>{
			L"Texture\\tree0.dds",
			L"Texture\\tree1.dds",
			L"Texture\\tree2.dds",
			L"Texture\\tree3.dds"},
		nullptr,
		mTreeTexArray.GetAddressOf()));
	m_BasicEffect.SetTextureArray(mTreeTexArray.Get());

	// 初始化點精靈緩沖區
	InitPointSpritesBuffer();

	// 初始化樹的材質
	m_TreeMat.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
	m_TreeMat.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
	m_TreeMat.specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);

	ComPtr<ID3D11ShaderResourceView> texture;
	// 初始化地板
	m_Ground.SetBuffer(m_pd3dDevice.Get(), Geometry::CreatePlane(XMFLOAT2(100.0f, 100.0f), XMFLOAT2(10.0f, 10.0f)));
	m_Ground.GetTransform().SetPosition(0.0f, -5.0f, 0.0f);
	HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\Grass.dds", nullptr, texture.GetAddressOf()));
	m_Ground.SetTexture(texture.Get());
	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);
	m_Ground.SetMaterial(material);

	// ******************
	// 初始化不會變化的值
	//

	// 方向光
	DirectionalLight dirLight[4];
	dirLight[0].ambient = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
	dirLight[0].diffuse = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);
	dirLight[0].specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
	dirLight[0].direction = XMFLOAT3(-0.577f, -0.577f, 0.577f);
	dirLight[1] = dirLight[0];
	dirLight[1].direction = XMFLOAT3(0.577f, -0.577f, 0.577f);
	dirLight[2] = dirLight[0];
	dirLight[2].direction = XMFLOAT3(0.577f, -0.577f, -0.577f);
	dirLight[3] = dirLight[0];
	dirLight[3].direction = XMFLOAT3(-0.577f, -0.577f, -0.577f);
	for (int i = 0; i < 4; ++i)
		m_BasicEffect.SetDirLight(i, dirLight[i]);

	// ******************
	// 初始化攝像機
	//
	auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
	m_pCamera = camera;
	camera->SetViewPort(0.0f, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
	camera->SetPosition(XMFLOAT3());
	camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f);
	camera->LookTo(XMFLOAT3(), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f));

	m_BasicEffect.SetWorldMatrix(XMMatrixIdentity());
	m_BasicEffect.SetViewMatrix(camera->GetViewXM());
	m_BasicEffect.SetProjMatrix(camera->GetProjXM());
	m_BasicEffect.SetEyePos(camera->GetPosition());

	// ******************
	// 初始化霧效和天氣等
	//

	m_BasicEffect.SetFogState(m_FogEnabled);
	m_BasicEffect.SetFogColor(XMVectorSet(0.75f, 0.75f, 0.75f, 1.0f));
	m_BasicEffect.SetFogStart(15.0f);
	m_BasicEffect.SetFogRange(75.0f);
	
	return true;
}

其余方法限於篇幅就不放在這里了,讀者可以查看源碼觀察剩余部分的代碼實現。現在來看實現效果吧。

實現效果

可以觀察到,在與公告版近距離接觸時可以很明顯地看到公告板在跟着攝像機旋轉。如果距離很遠的話轉動的幅度就會很小,用戶才會比較難以分辨出遠處物體是否為公告板或3D模型了。

下面演示了白天和黑夜的霧效

最后則是Alpha-To-Coverage的開啟/關閉效果對比

DirectX11 With Windows SDK完整目錄

Github項目源碼

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM