第十章 模板渲染

模板緩衝區是我們可以用來實現一些特殊效果的屏幕外緩衝區。模板緩衝區與後臺緩衝區和深度緩衝區具有相同的分辨率,使得模板緩衝區中的第ij個像素與後臺緩衝區和深度緩衝區中的第ij個像素相對應。從§4.1.5中回想一下,當一個模板緩衝區被指定時,它被附加到深度緩衝區。顧名思義,模板緩衝區作爲一個模板,並允許我們阻止某些像素片段渲染到後臺緩衝區。

例如實現鏡子時,我們需要在鏡子的平面上反射一個物體;但是,我們只想把鏡子區域反射回去。使用模板緩衝區,除非它在鏡子區域中,否則會被阻止反射的渲染,(見圖10.1)。

模板緩衝區(以及深度緩衝區)通過ID3D11DepthStencilState接口進行控制。像混合一樣,界面提供了一組靈活而強大的功能。通過學習現有的示例應用程序,學習如何有效地使用模板緩衝區。一旦瞭解了模板緩衝區的用法,您將更好地瞭解如何將其用於您自己的特定需求。

10-1
圖10.1 (左)反射的頭骨在鏡子中正確顯示。 反射沒有通過牆磚顯示,因爲它沒有通過這個區域的深度測試。 然而,看着牆後,我們能夠看到倒影,從而打破了錯覺(反射只能通過鏡子顯現出來)。 (右)通過使用模板緩衝區,我們可以阻止反射的頭骨被渲染,除非它被繪製在鏡子中。

學習目標:
1.瞭解如何使用ID3D11DepthStencilState接口控制深度和模板緩衝區設置。
2.要學習如何使用模板緩衝區來實現鏡像,以防止反射被繪製到非鏡面上。
3.識別雙重混合並理解模板緩衝區如何防止它。
4.解釋深度複雜性並描述兩種方式可以測量場景的深度複雜度。

10.1深度/標準格式和清除

可以將 深度/模板緩衝區理解爲一個紋理,它必須用特定的數據格式創建。用於深度/模板緩衝的格式如下所示:
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定32位浮點深度緩衝區,其中8位(無符號整數)保留用於映射到[0,255]範圍的模板緩衝區,24位不用於填充。
2.DXGI_FORMAT_D24_UNORM_S8_UINT:指定映射到[0,1]範圍的無符號24位深度緩衝區,爲映射到[0,255]範圍的模板緩衝區保留8位(無符號整數)。在我們的D3DApp框架中, 我們創建深度緩衝區,我們指定:
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

此外,模板緩衝區應該在每幀開始時重置爲某個值。 這是用下面的方法(它也清除深度緩衝區)完成的:

void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,
    UINT ClearFlags, FLOAT Depth, UINT8 Stencil);

1.pDepthStencilView:指向我們要清除的深度/模板緩衝區視圖的指針。
2.ClearFlags:指定D3D11_CLEAR_DEPTH只清除深度緩衝區; 指定D3D11_CLEAR_STENCIL只清除模板緩衝區; 指定D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL清除兩者。
3.Depth:將深度緩衝區中的每個像素設置爲的浮點值;它必須是一個浮點數x,使得0≤x≤1。
4.Stencil:設置模板緩衝區的每個像素的整數值;它必須是一個整數n,使得0≤n≤255。

我們已經在我們的演示中每幀都調用了這個方法。 例如:

void MirrorApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(
    mRenderTargetView,
    reinterpret_cast<const float*>(&Colors::Black));

md3dImmediateContext->ClearDepthStencilView(
    mDepthStencilView,
    D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
}

10.2模板測試

如前所述,我們可以使用模板緩衝區來阻止渲染到後臺緩衝區的某些區域。阻止特定像素被寫入的決定由模板測試決定,其由以下給出:

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

模板測試是在像素被光柵化(即在輸出合併階段)的情況下進行的,假設模板被啓用,並且需要兩個操作數:
1.通過將應用程序定義的模板參考值(StencilRef)與應用程序定義的掩蔽值(StencilReadMask)進行“與”操作來確定左側(LHS)操作數。
2.通過將已測試的特定像素(值)的模板緩衝區中的條目與應用程序定義的遮罩值(StencilReadMask)進行AND運算,確定右側(RHS)操作數。

請注意,StencilReadMask對於LHS和RHS是相同的。然後,模板測試將LHS與RHS進行比較,如應用程序選擇的比較函數⊴所指定的,該函數返回true或false值。如果測試評估爲true,則將像素寫入後臺緩衝區(假設深度測試也通過) 。如果測試結果爲false,那麼我們阻止像素被寫入後臺緩衝區。當然,如果一個像素由於模板測試失敗而被拒絕,它也不會被寫入深度緩衝區。

⊴運算符是D3D11_COMPARISON_FUNC枚舉類型中定義的任何一個函數:

typedef enum D3D11_COMPARISON_FUNC
{
    D3D11_COMPARISON_NEVER = 1,
    D3D11_COMPARISON_LESS = 2,
    D3D11_COMPARISON_EQUAL = 3,
    D3D11_COMPARISON_LESS_EQUAL = 4,
    D3D11_COMPARISON_GREATER = 5,
    D3D11_COMPARISON_NOT_EQUAL = 6,
    D3D11_COMPARISON_GREATER_EQUAL = 7,
    D3D11_COMPARISON_ALWAYS = 8,
} D3D11_COMPARISON_FUNC;

1.D3D11_COMPARISON_NEVER:函數總是返回false。
2.D3D11_COMPARISON_LESS:用<運算符替換⊴。
3.D3D11_COMPARISON_EQUAL:用==運算符替換⊴。
4.D3D11_COMPARISON_LESS_EQUAL:用operator運算符替換⊴。
5.D3D11_COMPARISON_GREATER:用>運算符替換⊴。
6.D3D11_COMPARISON_NOT_EQUAL:用!替換⊴! =運算符。
7.D3D11_COMPARISON_GREATER_EQUAL:用≥操作符替換⊴。
8.D3D11_COMPARISON_ALWAYS:函數總是返回true。

10.3深度/標準狀態塊

創建一個ID3D11DepthStencilState接口的第一步是填寫一個D3D11_DEPTH_STENCIL_DESC實例:

typedef struct D3D11_DEPTH_STENCIL_DESC {
    BOOL DepthEnable; // Default True

    // Default: D3D11_DEPTH_WRITE_MASK_ALL
    D3D11_DEPTH_WRITE_MASK DepthWriteMask;

    // Default: D3D11_COMPARISON_LESS
    D3D11_COMPARISON_FUNC DepthFunc;

    BOOL StencilEnable; // Default: False
    UINT8 StencilReadMask; // Default: 0xff
    UINT8 StencilWriteMask; // Default: 0xff
    D3D11_DEPTH_STENCILOP_DESC FrontFace;
    D3D11_DEPTH_STENCILOP_DESC BackFace;
} D3D11_DEPTH_STENCIL_DESC;

10.3.1深度設置

1.DepthEnable:指定true以啓用深度緩衝;指定false來禁用它。
當深度測試被禁用時,繪製順序很重要,即使在一個遮擋物體後面也會繪製一個像素片段(見§4.1.5)。如果禁用深度緩衝,深度緩衝區中的元素也不會更新,無論DepthWriteMask如何設置。
2.DepthWriteMask:可以是D3D11_DEPTH_WRITE_MASK_ZEROD3D11_DEPTH_WRITE_MASK_ALL,但不能同時使用。假設DepthEnable設置爲true,D3D11_DEPTH_WRITE_MASK_ZERO將禁止寫入深度緩衝區,但深度測試仍將發生。D3D11_DEPTH_WRITE_MASK_ALL啓用寫入深度緩衝區;如果深度和模板測試都通過,將會寫入新的深度。控制深度讀取和寫入的能力對於實現某些特殊效果是必要的。
3.DepthFunc:指定D3D11_COMPARISON_FUNC枚舉類型的成員之一來定義深度測試比較函數。通常這總是D3D11_COMPARISON_LESS,以便進行通常的深度測試,如§4.1.5中所述。也就是說,像素片段只要其深度值小於寫入後緩衝區的前一個像素的深度,就可以接受。但是,如您所見,Direct3D允許您在必要時自定義深度測試。

10.3.2模板設置

1.StencilEnable:指定true以啓用模板測試; 指定false來禁用它。
2.StencilReadMask:模板測試中使用的StencilReadMask

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

我們指定不區分大小寫,所以,例如,INCR等同於Incr。

10.4 實施平面鏡

自然界中的許多表面都是鏡面,讓我們看到物體的反射。本節介紹如何爲3D應用程序模擬鏡面。請注意,爲了簡便,我們只在平面鏡上進行講解。雖然一輛閃亮的汽車可以顯示反射; 但汽車的車身是光滑的,圓的,而不是平面的。因此,我們會在類似閃亮的大理石地板上,或者掛在牆上的鏡子上渲染反射,換句話說,就是在平面上進行。

以編程方式實現鏡像需要我們解決兩個問題。 首先,我們必須學會如何反映一個任意的平面的對象,以便我們能夠正確地繪製反射。 其次,我們只能在鏡子中展示反射,也就是說,我們必須以某種方式將一個表面“標記”爲一面鏡子,然後在渲染時只繪製反射的物體。 請參考圖10.1,它首先介紹了這個概念。

第一個問題很容易用一些解析幾何來解決,並在附錄C中討論。第二個問題可以用模板緩衝來解決。

10.4.1鏡像概述

當我們繪製反射時,我們也需要反射鏡面上的光源。 否則,反射中的照明將不準確。

圖10.2顯示了繪製物體的反射,我們只需要在鏡面上反射它。但是,這引入了圖10.1所示的問題。也就是說,物體(這種情況下的頭骨)的反射只是我們場景中的另一個物體,如果沒有物體遮擋它,那麼眼睛就會看到它。但是,反射只能通過鏡子來看。我們可以使用模板緩衝區來解決這個問題,因爲模板緩衝區允許我們阻止渲染到後臺緩衝區的某些區域。因此,如果模板緩衝區沒有被渲染到鏡像中,我們可以使用模板緩衝區來阻止渲染反射的頭骨。 以下概述了完成此步驟的步驟:

10-2
圖10.2 眼睛通過鏡子看到盒子的反射。爲了模擬這一點,我們在鏡像平面上反射盒子並像往常一樣渲染反射盒子。

10-3
圖10.3 將後臺緩衝區和模板緩衝區的地板,牆壁和頭骨清除爲0(用淺灰色表示)。在模板緩衝區上繪製的黑色輪廓線說明了後端緩衝區像素和模板緩衝區像素之間的關係 - 它們不表示模板緩衝區上繪製的任何數據。

1.像平常一樣將地板,牆壁和頭骨渲染到後臺緩衝區(但不是鏡像)。 請注意,此步驟不會修改模板緩衝區。
2.將模板緩衝區清除爲0.圖10.3顯示了此時的後臺緩衝區和模板緩衝區(我們用一個方框替代了頭骨以使繪圖更簡單)。
3.僅將鏡像渲染到模板緩衝區。 我們可以通過創建設置的混合狀態來禁用對後臺緩衝區的顏色寫入

D3D11_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;

我們可以通過設置禁止寫入深度緩衝區

D3D11_DEPTH_STENCIL_DESC::DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;

將鏡像渲染到模板緩衝區時,我們將模板測試設置爲始終成功(D3D11_COMPARISON_ALWAYS),並指定如果測試通過,則應使用1(StencilRef)替換模板緩衝區條目(D3D11_STENCIL_OP_REPLACE)。 如果深度測試失敗,我們指定D3D11_STENCIL_OP_KEEP,這樣如果深度測試失敗(例如,如果頭骨遮住鏡像的一部分),模板緩衝區不會發生更改。 因爲我們只將鏡像渲染到模板緩衝區,所以模板緩衝區中的所有像素將爲0,除了與鏡像的可見部分對應的像素 - 它們將具有1.圖10.4顯示了更新 模板緩衝區。 本質上,我們在模板緩衝區中標記鏡像的可見像素。

繪製完頭骨後,將鏡子繪製到模板緩衝區非常重要,以便鏡頭的鏡子像素不能進行深度測試,因此不會修改模板緩衝區。 我們不想打開被遮擋的部分模板緩衝區; 否則反射將通過頭骨顯示。

10-4
圖10.4 將鏡像渲染到模板緩衝區,基本上標記模板緩衝區中與鏡像的可見部分相對應的像素。模板緩衝區上的純黑色區域表示模板條目設置爲1.請注意,由模塊緩衝的模板緩衝區上的區域未被設置爲1,因爲它未能進行深度測試(該框位於該部分的前面 鏡子)。

4.現在我們將反射的頭骨渲染到後臺緩衝區和模板緩衝區。但回想一下,如果模板測試通過,我們只會渲染到後臺緩衝區。這一次,我們將模板測試設置爲僅在模板緩衝區中的值等於1時才成功;這是通過使用1的StencilRef和模板操作符D3D11_COMPARISON_EQUAL完成的。通過這種方式,反射的頭骨只會渲染到相應的模板緩衝區條目中具有1的區域。由於模板緩衝區中對應於鏡像可見部分的區域是唯一具有1的條目,因此反射的頭骨只會渲染到鏡像的可見部分。
5.最後,我們像往常一樣將鏡像渲染到後臺緩衝區。但是,爲了讓頭骨反射透過(位於鏡子後面),我們需要使用透明度混合渲染鏡子。如果我們沒有渲染具有透明度的鏡子,鏡子會簡單地遮擋反射,因爲它的深度小於反射的深度。爲了實現這一點,我們只需要爲鏡像定義一個新的材質實例;我們將漫反射分量的alpha通道設置爲0.5,以使反射鏡50%不透明,並且如上一章(§9.5.4)中所述,我們使用透明度混合狀態渲染反射鏡。

mMirrorMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mMirrorMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.5f);
mMirrorMat.Specular = XMFLOAT4(0.4f, 0.4f, 0.4f, 16.0f);

這些設置提供以下混合等式:

C=0.5·Csrc+0.5·Cdst

假設我們已將反射的頭骨像素放置到後臺緩衝區,我們看到50%的顏色來自鏡像(源),50%的顏色來自頭骨(目標)。

10.4.2 定義鏡像深度/模板狀態

爲了實現前面描述的算法,我們需要兩個深度/模板狀態。 第一個用於繪製鏡像以標記模板緩衝區上的鏡像像素。 第二個用於繪製反射的頭骨,以便它僅被繪製到鏡子的可見部分。

//
// MarkMirrorDSS
//
D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = true;
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
mirrorDesc.DepthFunc = D3D11_COMPARISON_LESS;
mirrorDesc.StencilEnable = true;
mirrorDesc.StencilReadMask = 0xff;
mirrorDesc.StencilWriteMask = 0xff;
mirrorDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
// We are not rendering backfacing polygons, so these settings do not matter.
mirrorDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
ID3D11DepthStencilState* MarkMirrorDSS;
HR(device->CreateDepthStencilState(&mirrorDesc, &MarkMirrorDSS));
//
// DrawReflectionDSS
//
D3D11_DEPTH_STENCIL_DESC drawReflectionDesc;
drawReflectionDesc.DepthEnable = true;
drawReflectionDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
drawReflectionDesc.DepthFunc = D3D11_COMPARISON_LESS;
drawReflectionDesc.StencilEnable = true;
drawReflectionDesc.StencilReadMask = 0xff;
drawReflectionDesc.StencilWriteMask = 0xff;
drawReflectionDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
drawReflectionDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
ID3D11DepthStencilState* DrawReflectionDSS;
HR(device->CreateDepthStencilState(&drawReflectionDesc, &DrawReflectionDSS));

10.4.3 繪製場景

以下代碼概述了我們的繪製方法。爲了簡潔和清晰,我們省略了不相關的細節,例如設置常量緩衝區值(請參閱示例代碼瞭解完整細節)。

//
// Draw the floor, walls and skull to the back buffer as normal.
//…
//
// Draw the mirror to the stencil buffer.
// Here we set the stencil value of visible pixels of the mirror
// to 1, thereby marking “mirror pixels.”
//
// Note that we have to draw the mirror last because we need to
// render the skull into the depth buffer first so that when we
// render the mirror, portions of the mirror that are occluded
// by the skull fail the depth test and do not get rendered
// into the stencil buffer. We do not want to set pixels on
// the stencil buffer that are occluded. Otherwise the reflection
// will show through the skull, too.
//
// Do not write to render target.
md3dImmediateContext->OMSetBlendState(
RenderStates::NoRenderTargetWritesBS, blendFactor, 0xffffffff);
// Render visible mirror pixels to stencil buffer.
// Do not write mirror depth to depth buffer at this point, otherwise
// it will occlude the reflection.
md3dImmediateContext->OMSetDepthStencilState(
RenderStates::MarkMirrorDSS, 1);
pass->Apply(0, md3dImmediateContext);
// Draw mirror.
md3dImmediateContext->Draw(6, 24);
//
// Draw the reflected skull.
//
// Build reflection matrix to reflect the skull.
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
XMMATRIX world = XMLoadFloat4x4(&mSkullWorld) * R;
…/
/ Reflect the light source as well.
// Cache the old light directions, and reflect the light directions.
XMFLOAT3 oldLightDirections[3];
for(int i = 0; i < 3; ++i)
{
oldLightDirections[i] = mDirLights[i].Direction;
XMVECTOR lightDir = XMLoadFloat3(&mDirLights[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mDirLights[i].Direction, reflectedLightDir);
}
Effects::BasicFX->SetDirLights(mDirLights);
// Reflection changes winding order, so cull clockwise
// triangles instead (see §10.4.4).
md3dImmediateContext->RSSetState(RenderStates::CullClockwiseRS);
// Only render reflection to pixels with a stencil value equal to 1. Only
// visible mirror pixels have a stencil value of 1, hence the skull
// will only be rendered into the mirror.
md3dImmediateContext->OMSetDepthStencilState(
RenderStates::DrawReflectionDSS, 1);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSkullIndexCount, 0, 0);
// Restore default states.
md3dImmediateContext->RSSetState(0);
md3dImmediateContext->OMSetDepthStencilState(0, 0);
// Restore light directions.
for(int i = 0; i < 3; ++i)
{
mDirLights[i].Direction = oldLightDirections[i];
}
Effects::BasicFX->SetDirLights(mDirLights);
//
// Draw the mirror to the back buffer as usual but with transparency
// blending so the reflection shows through.
//
// Mirror
md3dImmediateContext->OMSetBlendState(
RenderStates::TransparentBS, blendFactor, 0xffffffff);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->Draw(6, 24);

10.4.4 扭曲順序和反射

當三角形在平面上反射時,其繞線順序不會反轉,因此其面部法線不會反轉。因此,面向外部的法線在反射之後變成面向法線的內部法線(見圖10.5)。爲了解決這個問題,我們告訴Direct3D將逆時針順序的三角形解釋爲正面,將三角形順時針纏繞順序解釋爲背面(這與我們通常的慣例相反 - §5.10.2)。這有效地反映了正常的方向,使它們在反射後面向外。 我們通過設置以下光柵器狀態來顛倒卷繞順序約定:

10-5
圖10.5 多邊形法線不會與反射相反,這使得它們在反射後面向內。

// Note: Define such that we still cull backfaces by making front faces CCW.
// If we did not cull backfaces, then we have to worry about the BackFace
// property in the D3D11_DEPTH_STENCIL_DESC.
D3D11_RASTERIZER_DESC cullClockwiseDesc;
ZeroMemory(&cullClockwiseDesc, sizeof(D3D11_RASTERIZER_DESC));
cullClockwiseDesc.FillMode = D3D11_FILL_SOLID;
cullClockwiseDesc.CullMode = D3D11_CULL_BACK;
cullClockwiseDesc.FrontCounterClockwise = true;
cullClockwiseDesc.DepthClipEnable = true;
ID3D11RasterizerState* CullClockwiseRS;
HR(device->CreateRasterizerState(&cullClockwiseDesc, &CullClockwiseRS));

10.5 實施平面陰影

陰影有助於我們理解場景中的光線在哪裏發出,並最終使場景更真實。 在本節中,我們將展示如何實現平面陰影; 即位於平面上的陰影(見圖10.6)。

10-6
圖10.6 主光源在“鏡像”演示中投射平面陰影。

爲了實現平面陰影,我們必須首先找到物體投射到平面上的陰影並對其進行幾何建模,以便我們可以對其進行渲染。這可以通過一些3D數學來輕鬆完成。然後,我們用50%透明度的黑色材質渲染描述陰影的三角形。像這樣渲染陰影可以引入一些稱爲“雙重混合”的渲染構件,我們在幾節中解釋它; 我們利用模板緩衝區來防止發生雙重混合。

10.5.1 平行光陰影

圖10.7顯示了一個物體相對於平行光源投射的陰影。 給定方向爲L的平行光源,穿過頂點P的光線由rt=p+tL 給出。光線r(t)與陰影平面(n,d)的交點給出s(讀者可以在附錄C中閱讀更多關於光線和平面的信息)。通過拍攝每一條光線 物體與平面的頂點定義了投影的陰影幾何。 對於頂點p,其陰影投影由下式給出

s=r(ts)=pn·p+dn·LL

射線/平面相交測試的細節在附錄C中給出。
10-7
圖10.7 相對於平行光源投射的陰影。

公式10.1可以寫成矩陣
s=[pxpypz1][n·LLxnxLynyLznz0Lxnyn·LLynyLzny0LxnzLynzn·LLznz0LxdLydLzdn·L]

我們稱前面的4×4矩陣爲方向陰影矩陣,並由Sdir表示。 要看這個矩陣如何等於公式10.1,我們只需要執行乘法。 然而,首先觀察到這個方程式修改w分量,使得sw = n·L。因此,當透視分割(§5.6.3.4)發生時,s的每個座標將被除以n·L; 這就是我們如何使用矩陣在方程10.1中得到n·L的除法。 現在進行矩陣乘法以獲得i∈{1,2,3}的第i個座標si’,然後獲得透視分割:
si=(n·L)piLinxpxLinypyLinzpzLidn·L(n·L)pi(n·p+d)Lin·Lpin·p+dn·LLi

這正是方程10.1中s的第i個座標,所以s = s’。

10-8
圖10.8 n·L <0的情況。

要使用陰影矩陣,我們將它與我們的世界矩陣結合起來。 然而,在世界變換之後,幾何圖形還沒有真正投影到陰影平面上,因爲視角鴻溝尚未出現。 如果sw = n·L <0,則會出現問題,因爲這會使w座標爲負數。 通常在透視投影過程中,我們將z座標複製到w座標中,並且負w座標表示該點不在視圖體積中,因此被剪切掉(剪切在分割之前的均勻空間中完成)。 這對平面陰影是一個問題,因爲除了透視分割之外,我們現在使用w座標來實現陰影。 圖10.8顯示了一個有效的情況,其中n·L <0,但陰影不會出現。

爲了解決這個問題,我們應該使用矢量朝向無限遠的光源LT = -L,而不是使用光線方向L. 觀察r(t)= p + tL和r(t)= p + tLT定義相同的3D線,線和平面之間的交點將相同(交點參數ts將不同以補償 LT和L之間的符號差異)。 所以使用LT = -L給出了相同的答案,但是n·L> 0,這避免了負的w座標。

10.5.2 點光陰影

圖10.9顯示了一個物體相對於一個點光源投射的陰影,該點光源的位置由點L描述。點光線通過任何頂點p的光線由下式給出:r(t)=p+t(pL) 。射線r(t)與陰影平面(n,d)的交點給出s。通過用平面拍攝對象頂點中的每個頂點所發現的交點集合定義了投影的陰影幾何圖形。對於頂點p,其陰影投影由下式給出

s=r(ts)=pn·p+dn·(pL)(pL)

10-9
圖10.9 相對於點光源投射的陰影。

公式10.2也可以寫成矩陣方程:
Spoint=[n·L+dLxnxLynyLznznxLxnyn·L+dLynyLznynyLxnzLynzn·L+dLznznzLxdLydLzdn·L]

爲了瞭解這個矩陣如何等於公式10.2,我們只需要按照前一節中的方法進行乘法運算。 請注意,最後一列沒有零,並給出:
sw=pxnxpynypznzn·Lp·n+n·Ln·(pL)

這是公式10.2中分母的負數,但如果我們也否定分子,則可以否定分母。

請注意,L爲點光源和平行光源提供了不同的用途。 對於點光源,我們使用L來定義點光源的位置。 對於平行光,我們使用L來定義朝向無限遠的光源的方向(即,平行光線行進的相反方向)。

10.5.3 一般陰影矩陣

使用齊次座標,可以創建一個適用於點光源和定向光源的通用陰影矩陣。
1.如果LW=0 ,則L描述朝向無限遠的光源的方向(即,平行光線行進的相反方向)。
2.如果LW=1 ,那麼L描述點光源的位置。
然後我們用下面的陰影矩陣表示從頂點p到其投影s的轉換:

S=[n·L+dLwLxnxLynyLznzLxnxLxnyn·L+dLwLynyLznynyLxnzLynzn·L+dLwLznznzLxdLydLzdn·L]

很容易看出,如果LW=0 ,則S減小到Sdir ,並且當LW=1 時S減少到Spoint

XNA數學庫提供了以下函數來建立陰影矩陣,給出我們希望投影陰影的平面和描述如果w = 0時的平行光或w = 1時的點光的矢量:

XMFINLINE XMMATRIX XMMatrixShadow(
    FXMVECTOR ShadowPlane,
    FXMVECTOR LightPosition);

爲了進一步閱讀,[Blinn96]和[Möller02]討論平面陰影。

10.5.4 使用模板緩衝區防止雙重混合

當我們將一個物體的幾何圖形展平到平面上來描述它的陰影時,可能(並且事實上很可能)兩個或更多的扁平三角形將重疊。當我們渲染具有透明度的陰影(使用混合)時,這些具有重疊三角形的區域將被混合多次,因此顯得較暗。圖10.10顯示了這一點。

我們可以使用模板緩衝來解決這個問題。
1.假設陰影將被渲染的模板緩衝區像素已被清除爲0.這在我們的鏡像演示中是真實的,因爲我們只將陰影投射到地平面上,而我們只修改鏡像模板緩衝區像素。
2.如果模板緩衝區的條目爲0,則將模板測試設置爲僅接受像素。如果模板測試通過,則我們將模板緩衝區值增加到1。

當我們第一次渲染一個陰影像素時,模板測試會通過,因爲模板緩衝區條目是0.但是,當我們渲染這個像素時,我們也將相應的模板緩衝區條目增加到1.因此,如果我們嘗試覆蓋到 已經渲染到的區域(在模板緩衝區中標記值爲1),模板測試將失敗。 這可以防止多次繪製相同的像素,從而防止雙重混合。

10-10
圖10.10 注意左圖像陰影中較暗的“粉刺”區域; 這些對應於部分扁平顱骨重疊的區域,從而導致“雙重混合”。右側的圖像顯示了正確的陰影,沒有雙重混合。

10.5.5 陰影代碼

我們定義了一個陰影材質,用於爲僅有50%透明黑色材質的陰影着色:

mShadowMat.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
mShadowMat.Diffuse = XMFLOAT4(0.0f, 0.0f, 0.0f, 0.5f);
mShadowMat.Specular = XMFLOAT4(0.0f, 0.0f, 0.0f, 16.0f);

爲了防止雙重混合,我們設置了以下深度/模板狀態對象:

D3D11_DEPTH_STENCIL_DESC noDoubleBlendDesc;
noDoubleBlendDesc.DepthEnable = true;
noDoubleBlendDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
noDoubleBlendDesc.DepthFunc = D3D11_COMPARISON_LESS;
noDoubleBlendDesc.StencilEnable = true;
noDoubleBlendDesc.StencilReadMask = 0xff;
noDoubleBlendDesc.StencilWriteMask = 0xff;
noDoubleBlendDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDoubleBlendDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
noDoubleBlendDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDoubleBlendDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
ID3D11DepthStencilState* NoDoubleBlendDSS;
HR(device->CreateDepthStencilState(&noDoubleBlendDesc, &NoDoubleBlendDSS));

然後我們繪製如下的頭骨陰影:構建陰影矩陣並使用StencilRef值爲0的“非雙混合”深度/模板狀態進行渲染:

XMVECTOR shadowPlane = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // xz plane
XMVECTOR toMainLight = -XMLoadFloat3(&mDirLights[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f);
// Set per object constants.
XMMATRIX world = XMLoadFloat4x4(&mSkullWorld)*S*shadowOffsetY;
XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
XMMATRIX worldViewProj = world*view*proj;
Effects::BasicFX->SetWorld(world);
Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
Effects::BasicFX->SetWorldViewProj(worldViewProj);
Effects::BasicFX->SetMaterial(mShadowMat);
md3dImmediateContext->OMSetDepthStencilState(RenderStates::NoDoubleBlendDSS, 0);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSkullIndexCount, 0, 0);

10.6 總結

1.模板緩衝區是一個屏幕外的緩衝區,我們可以使用它來阻止將某些像素片段渲染到後臺緩衝區。 模板緩衝區與深度緩衝區共享,因此具有與深度緩衝區相同的分辨率。 有效深度/模板緩衝區格式爲DXGI_FORMAT_D32_FLOAT_S8X24_UINTDXGI_FORMAT_D24_UNORM_S8_UINT

2.阻止特定像素被寫入的決定由模板測試決定,其由以下給出:

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

其中⊴運算符是D3D11_COMPARISON_FUNC枚舉類型中定義的任何一個函數。 StencilRef,StencilReadMask,StencilReadMask和比較運算符都是使用Direct3D深度/模板API設置的應用程序定義數量。 值數量是模板緩衝區中的當前值。

3.創建ID3D11DepthStencilState接口的第一步是填寫一個D3D11_DEPTH_STENCIL_DESC實例,該實例描述了我們要創建的深度/模板狀態。 在填充D3D11_DEPTH_STENCIL_DESC結構後,我們可以使用ID3D11Device :: CreateDepthStencilState方法獲取指向ID3D11DepthStencilState接口的指針。 最後,我們使用ID3D11Device :: OMSetDepthStencilState方法將深度/模板狀態塊綁定到管道的輸出合併階段。

發佈了7 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章