假設牆上有一面鏡子,鏡子前面有個木箱。如果觀察角度合適,整個木箱鏡像都會在鏡子裏,計算起來還比較簡單;而變換個角度,木箱的鏡像可能只有一部分在鏡子裏,這時單純依靠計算來實現就很麻煩。DirectX提供了模板技術以方便地完成這個任務。我印象中用到模板就是噴漆的時候。將設計好的圖案在一塊板上刻出來,然後把這塊板扣在要噴塗的地方,不管三七二十一直接噴漆,最後把板拿下來,圖案就噴好了。DirectX中的模板也一樣,啓用模板後,後續的繪製就被限制在模板鏤空(即模板測試通過)的地方。不過DirectX中的模板更加靈活複雜。
使用模板時只有兩個結果,通過和不通過。測試方法如下:
StencilRef & StencilReadMask⊴ Value & StencilReadMask
其中StencilRef是模板參考值,StencilReadMask是模板掩碼,Value是當前像素點的模板值,&是按位與運算,而⊴代表比較函數。與混合一樣,模板也是通過組合StencilReadMask和⊴的不同枚舉值來實現各種效果。在DirectX中使用模板的細節內容見DirectX 10 遊戲編程入門和MSDN,這裏只關注鏡像效果的實現。
鏡像效果可以仿照噴漆來做,給牆壁蓋上一塊只能露出鏡子的模板,畫完反射模型之後再把模板拿掉。大致可以分爲四步:
1、創建牆壁、地板、木箱
2、創建鏡子模版
3、繪製木箱和地板的反射
4、混合鏡子,木箱和地板的紋理
基於第十篇的紋理木箱來實現鏡像效果。其中HLSL代碼只修改頂點着色器即可,方便紋理座標變換,即調整ConstantBuffer的結構。
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
matrix texTransform;
};
並在main方法中進行紋理座標變換。
// 紋理座標變換
output.tex =mul(float4(input.tex, 0.0f,1.0f), texTransform).xy;
然後可以直接進入C++代碼部分。首先依然是修改CubeRenderer.h中的ModelViewProjectionConstantBuffer定義,使其與頂點着色器中的一致。接着添加新的私有成員和方法。
Microsoft::WRL::ComPtr<ID3D11BlendState>m_alphaBlendState;
Microsoft::WRL::ComPtr<ID3D11DepthStencilState>m_markDepthStencilState;
Microsoft::WRL::ComPtr<ID3D11DepthStencilState>m_reflectDepthStencilState;
Microsoft::WRL::ComPtr<ID3D11RasterizerState>m_rasterizerState;
ModelViewProjectionConstantBufferm_constantBufferData;
ModelViewProjectionConstantBufferm_reflectConstantBufferData;
// 立方體模型
void CreateCubeModel();
void RenderCubeModel(ModelViewProjectionConstantBuffer*constantBufferData);
Microsoft::WRL::ComPtr<ID3D11Buffer>m_vertexCubeBuffer;
Microsoft::WRL::ComPtr<ID3D11Buffer>m_indexCubeBuffer;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_woodSRV;
uint32 m_indexCubeCount;
// 牆體模型
void CreateWallModel();
void RenderWallModel();
Microsoft::WRL::ComPtr<ID3D11Buffer>m_vertexWallBuffer;
Microsoft::WRL::ComPtr<ID3D11Buffer>m_indexWallBuffer;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_wallSRV;
uint32 m_indexWallCount;
// 地板模型
void CreateFloorModel();
void RenderFloorModel(ModelViewProjectionConstantBuffer*constantBufferData);
Microsoft::WRL::ComPtr<ID3D11Buffer>m_vertexFloorBuffer;
Microsoft::WRL::ComPtr<ID3D11Buffer>m_indexFloorBuffer;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_floorSRV;
uint32 m_indexFloorCount;
// 鏡子模型
voidCreateMirrorModel();
voidRenderMirrorModel();
Microsoft::WRL::ComPtr<ID3D11Buffer>m_vertexMirrorBuffer;
Microsoft::WRL::ComPtr<ID3D11Buffer>m_indexMirrorBuffer;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_mirrorSRV;
uint32 m_indexMirrorCount;
前面幾個成員的功能在後面用到時詳細介紹。而後面四個模型的成員和方法只是名字不同,功能一致,寫的很囉嗦,有待修改。定義好上面的成員和方法就轉到實現部分。首先在CreateDeviceResources方法中載入四種紋理並初始化各個狀態
DX::ThrowIfFailed(
CreateWICTextureFromFile(
m_d3dDevice.Get(),
m_d3dContext.Get(),
L"wood.jpg",
NULL,
m_woodSRV.GetAddressOf()
)
);
……
D3D11_SAMPLER_DESC samplerDesc;
samplerDesc.Filter= D3D11_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.MipLODBias= 0;
samplerDesc.MaxAnisotropy= 1;
samplerDesc.ComparisonFunc= D3D11_COMPARISON_NEVER;
samplerDesc.BorderColor[0 ] = 1.0f;
samplerDesc.BorderColor[1 ] = 1.0f;
samplerDesc.BorderColor[2 ] = 1.0f;
samplerDesc.BorderColor[3 ] = 1.0f;
samplerDesc.MinLOD= -3.402823466e+38F; // -FLT_MAX
samplerDesc.MaxLOD= 3.402823466e+38F; // FLT_MAX
DX::ThrowIfFailed(
m_d3dDevice->CreateSamplerState(
&samplerDesc,
m_samplerState.GetAddressOf()
)
);
// 初始化Alpha混合狀態
D3D11_BLEND_DESC alphaBlendDesc ={0};
alphaBlendDesc.RenderTarget[0].BlendEnable= TRUE;
alphaBlendDesc.RenderTarget[0].SrcBlend= D3D11_BLEND_SRC_ALPHA; // Color_Fsrc
alphaBlendDesc.RenderTarget[0].DestBlend= D3D11_BLEND_INV_SRC_ALPHA; // Color_Fdst
alphaBlendDesc.RenderTarget[0].BlendOp= D3D11_BLEND_OP_ADD; // Color_Operation
alphaBlendDesc.RenderTarget[0].SrcBlendAlpha= D3D11_BLEND_ONE; // Alpha_Fsrc
alphaBlendDesc.RenderTarget[0].DestBlendAlpha= D3D11_BLEND_ZERO; // Alpha_Fdst
alphaBlendDesc.RenderTarget[0].BlendOpAlpha= D3D11_BLEND_OP_ADD; // Alpha_Operation
alphaBlendDesc.RenderTarget[0].RenderTargetWriteMask= D3D11_COLOR_WRITE_ENABLE_ALL;
DX::ThrowIfFailed(
m_d3dDevice->CreateBlendState(
&alphaBlendDesc,
&m_alphaBlendState
)
);
// 初始化鏡子模版
D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = true;
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
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;
// 背面參數設置無影響
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;
DX::ThrowIfFailed(
m_d3dDevice->CreateDepthStencilState(
&mirrorDesc,
&m_markDepthStencilState
)
);
// 初始化繪製反射時的模板
D3D11_DEPTH_STENCIL_DESC drawReflectionDesc;
drawReflectionDesc.DepthEnable = true;
drawReflectionDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
drawReflectionDesc.DepthFunc = D3D11_COMPARISON_ALWAYS;
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;
// 背面參數設置無影響
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;
DX::ThrowIfFailed(
m_d3dDevice->CreateDepthStencilState(
&drawReflectionDesc,
&m_reflectDepthStencilState
)
);
samplerDesc和alphaBlendDesc在以前的文章中已經介紹過,主要看下mirrorDesc和drawReflectionDesc。結合模板公式和MSDN的介紹,對這兩個結構能有基本的瞭解。
http://msdn.microsoft.com/zh-cn/library/windows/desktop/ff476101.aspx
http://msdn.microsoft.com/zh-cn/library/windows/desktop/ff476219.aspx
D3D11_COMPARISON_LESS
If the source data is lessthan the destination data, the comparison passes.
D3D11_COMPARISON_EQUAL
If the source data is equalto the destination data, the comparison passes.
D3D11_COMPARISON_ALWAYS
Always pass the comparison.
D3D11_STENCIL_OP_KEEP
Keep the existing stencil data.
D3D11_STENCIL_OP_ZERO
Set the stencil data to 0.
mirrorDesc就是一直通過模板測試,因爲繪製鏡子模板之前還沒有任何模板,測試也無意義。但是深度測試是必須的,鏡子在牆前面,通過深度測試時就會將已有模板值替換爲當前的模板值,實現鏡子形狀的模板繪製。
drawReflectionDesc則是一直通過深度測試,只進行模板測試。因爲要繪製的木箱和地板實際是在鏡子後面,所以深度測試無意義。模板測試的通過條件是兩個模板值相等。因爲鏡子覆蓋區域的模板值已被修改,其他區域都是默認。所以這次測試時,只有鏡子區域的模板值能達成相等的條件,對應位置像素被更新,從而實現限制繪製區域的目的。
在這後面還要調用四個方法來初始化模型,即初始化模型的頂點緩衝區和索引緩衝區。代碼很簡單,大同小異,不進行介紹,細節可以參照源代碼。接着看下在Render方法中的詳細繪製流程。
//------------------------------------------------
// 第一步 繪製正常模型,牆壁,地板和原始木箱
//------------------------------------------------
RenderWallModel();
RenderFloorModel(&m_constantBufferData);
RenderCubeModel(&m_constantBufferData);
//------------------------------------------------
// 第二步 繪製鏡子模版
//------------------------------------------------
// 設置模板狀態
m_d3dContext->OMSetDepthStencilState(m_markDepthStencilState.Get(),1);
RenderMirrorModel();
// 清除設置
m_d3dContext->OMSetDepthStencilState(0,0);
//------------------------------------------------
// 第三步 繪製反射木箱和地板
//------------------------------------------------
// 設置光柵化狀態和模板狀態
m_d3dContext->RSSetState(m_rasterizerState.Get());
m_d3dContext->OMSetDepthStencilState(m_reflectDepthStencilState.Get(),1);
RenderFloorModel(&m_reflectConstantBufferData);
RenderCubeModel(&m_reflectConstantBufferData);
// 清除設置
m_d3dContext->RSSetState(0);
m_d3dContext->OMSetDepthStencilState(0,0);
//------------------------------------------------
// 第四步 繪製鏡子
//------------------------------------------------
m_d3dContext->OMSetBlendState(m_alphaBlendState.Get(),blendFactors, 0xffffffff);
RenderMirrorModel();
m_d3dContext->OMSetBlendState(0,blendFactors, 0xffffffff);
以上代碼與開始時介紹的流程一致。注意在繪製鏡像時還更改了光柵化狀態。因爲繪製鏡像時,模型索引的環繞順序和平面法線都不翻轉,造成鏡像的法線方向錯誤,所以需要更改光柵化狀態。代碼中的RenderXXXModel方法也很簡單,就是把Render方法中負責渲染的代碼抽出來,僅以繪製正方體的方法爲例:
void CubeRenderer::RenderCubeModel(ModelViewProjectionConstantBuffer* constantBufferData)
{
UINT stride = sizeof(VertexPositionColor);
UINT offset = 0;
XMMATRIX cubeScale =XMMatrixScaling(1.0f, 1.0f, 0.0f);
XMStoreFloat4x4(&constantBufferData->transform,XMMatrixTranspose(cubeScale));
m_d3dContext->UpdateSubresource(
m_constantBuffer.Get(),
0,
NULL,
constantBufferData,
0,
0
);
m_d3dContext->IASetVertexBuffers(
0,
1,
m_vertexCubeBuffer.GetAddressOf(),
&stride,
&offset
);
m_d3dContext->IASetIndexBuffer(
m_indexCubeBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0
);
// 設置材質
m_d3dContext->PSSetShaderResources(
0,
1,
m_woodSRV.GetAddressOf()
);
// 設置紋理採樣
m_d3dContext->PSSetSamplers(
0,
1,
m_samplerState.GetAddressOf()
);
m_d3dContext->DrawIndexed(
m_indexCubeCount,
0,
0
);
}
還要注意m_reflectConstantBufferData。它代表鏡子裏的空間,是現實空間的鏡像,在Update方法中更新。本例中鏡子是在XY平面上,所以用XMMatrixReflect方法計算XY平面的對稱變換矩陣,並增加平移變換,使模型能夠轉移到鏡子內的對應位置。
// 計算反射空間矩陣
XMVECTOR mirrorPlane =XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy 平面
XMMATRIX T =XMMatrixTranslation(0.0f, 0.0f, -4.0f);
XMMATRIX R =XMMatrixReflect(mirrorPlane) * T;
XMStoreFloat4x4(&m_reflectConstantBufferData.projection,XMLoadFloat4x4(&m_constantBufferData.projection) );
XMStoreFloat4x4(&m_reflectConstantBufferData.view,XMLoadFloat4x4(&m_constantBufferData.view));
XMStoreFloat4x4(&m_reflectConstantBufferData.model,XMMatrixTranspose(XMMatrixRotationY(0) * R));
程序運行效果如下圖:
本篇文章雖然實現了鏡像效果,但是代碼中有很多重複的地方,像渲染模型的功能就可以只用一個方法,通過輸入參數進行不同模型的渲染,時間緊沒有整理,會在以後更新。
本篇文章的源代碼:Direct3DApp_CubeMirror
原文地址:http://blog.csdn.net/raymondcode/article/details/8479342
-------------------------------------------------------------------------------------------------------------------
修改後的源代碼:http://download.csdn.net/detail/raymondcode/4975386