DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用
前言
盡管在上一章的動態天空盒中用到了Render-To-Texture技術,但那是針對紋理立方體的特化實現。考慮到該技術的應用層面非常廣,在這里抽出獨立的一章專門來講有關它的通用實現以及各種應用。
| 深入理解與使用2D紋理資源(重點閱讀ScreenGrab庫) |
| 23 立方體映射:動態天空盒的實現 |
DirectX11 With Windows SDK完整目錄
Github項目源碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
再述Render-To-Texture技術
在前面的章節中,我們默認的渲染目標是來自DXGI后備緩沖區,它是一個2D紋理。而Render-To-Texture技術,實際上就是使用一張2D紋理作為渲染目標,但一般是自己新建的2D紋理。與此同時,這個紋理還能夠綁定到著色器資源視圖(SRV)供著色器所使用,即原本用作輸出的紋理現在用作輸入。
它可以用于:
在這一章,我們將展示下面這三種應用:
TextureRender類
該類借鑒了上一章DynamicSkyEffect的實現,因此也繼承了它簡單易用的特性:
class TextureRender { public:template<class T>using ComPtr = Microsoft::WRL::ComPtr<T>;TextureRender(ID3D11Device * device,int texWidth, int texHeight,bool generateMips = false);~TextureRender();// 開始對當前紋理進行渲染void Begin(ID3D11DeviceContext * deviceContext);// 結束對當前紋理的渲染,還原狀態void End(ID3D11DeviceContext * deviceContext);// 獲取渲染好的紋理ID3D11ShaderResourceView * GetOutputTexture();// 設置調試對象名void SetDebugObjectName(const std::string& name);private:ComPtr<ID3D11ShaderResourceView> m_pOutputTextureSRV; // 輸出的紋理對應的著色器資源視圖ComPtr<ID3D11RenderTargetView> m_pOutputTextureRTV; // 輸出的紋理對應的渲染目標視圖ComPtr<ID3D11DepthStencilView> m_pOutputTextureDSV; // 輸出紋理所用的深度/模板視圖D3D11_VIEWPORT m_OutputViewPort; // 輸出所用的視口ComPtr<ID3D11RenderTargetView> m_pCacheRTV; // 臨時緩存的后備緩沖區ComPtr<ID3D11DepthStencilView> m_pCacheDSV; // 臨時緩存的深度/模板緩沖區D3D11_VIEWPORT m_CacheViewPort; // 臨時緩存的視口bool m_GenerateMips; // 是否生成mipmap鏈 };它具有如下特點:
TextureRender初始化
現在我們需要完成下面5個步驟:
具體代碼如下:
TextureRender::TextureRender(ID3D11Device * device, int texWidth, int texHeight, bool generateMips): m_GenerateMips(generateMips), m_CacheViewPort() {// ******************// 1. 創建紋理//ComPtr<ID3D11Texture2D> texture;D3D11_TEXTURE2D_DESC texDesc;texDesc.Width = texWidth;texDesc.Height = texHeight;texDesc.MipLevels = (m_GenerateMips ? 0 : 1); // 0為完整mipmap鏈texDesc.ArraySize = 1;texDesc.SampleDesc.Count = 1;texDesc.SampleDesc.Quality = 0;texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;texDesc.Usage = D3D11_USAGE_DEFAULT;texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;texDesc.CPUAccessFlags = 0;texDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;// 現在texture用于新建紋理HR(device->CreateTexture2D(&texDesc, nullptr, texture.ReleaseAndGetAddressOf()));// ******************// 2. 創建紋理對應的渲染目標視圖//D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;rtvDesc.Format = texDesc.Format;rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;rtvDesc.Texture2D.MipSlice = 0;HR(device->CreateRenderTargetView(texture.Get(),&rtvDesc,m_pOutputTextureRTV.GetAddressOf()));// ******************// 3. 創建紋理對應的著色器資源視圖//D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;srvDesc.Format = texDesc.Format;srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;srvDesc.Texture2D.MostDetailedMip = 0;srvDesc.TextureCube.MipLevels = -1; // 使用所有的mip等級HR(device->CreateShaderResourceView(texture.Get(),&srvDesc,m_pOutputTextureSRV.GetAddressOf()));// ******************// 4. 創建與紋理等寬高的深度/模板緩沖區和對應的視圖//texDesc.Width = texWidth;texDesc.Height = texHeight;texDesc.MipLevels = 0;texDesc.ArraySize = 1;texDesc.SampleDesc.Count = 1;texDesc.SampleDesc.Quality = 0;texDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;texDesc.Usage = D3D11_USAGE_DEFAULT;texDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;texDesc.CPUAccessFlags = 0;texDesc.MiscFlags = 0;ComPtr<ID3D11Texture2D> depthTex;device->CreateTexture2D(&texDesc, nullptr, depthTex.GetAddressOf());D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;dsvDesc.Format = texDesc.Format;dsvDesc.Flags = 0;dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;dsvDesc.Texture2D.MipSlice = 0;HR(device->CreateDepthStencilView(depthTex.Get(),&dsvDesc,m_pOutputTextureDSV.GetAddressOf()));// ******************// 5. 初始化視口//m_OutputViewPort.TopLeftX = 0.0f;m_OutputViewPort.TopLeftY = 0.0f;m_OutputViewPort.Width = static_cast<float>(texWidth);m_OutputViewPort.Height = static_cast<float>(texHeight);m_OutputViewPort.MinDepth = 0.0f;m_OutputViewPort.MaxDepth = 1.0f; }TextureRender::Begin方法--開始對當前紋理進行渲染
該方法緩存當前渲染管線綁定的渲染目標視圖、深度/模板視圖以及視口,并替換初始化好的這些資源。注意還需要清空一遍緩沖區:
void TextureRender::Begin(ID3D11DeviceContext * deviceContext) {// 緩存渲染目標和深度模板視圖deviceContext->OMGetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.GetAddressOf());// 緩存視口UINT num_Viewports = 1;deviceContext->RSGetViewports(&num_Viewports, &m_CacheViewPort);// 清空緩沖區float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };deviceContext->ClearRenderTargetView(m_pOutputTextureRTV.Get(), black);deviceContext->ClearDepthStencilView(m_pOutputTextureDSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);// 設置渲染目標和深度模板視圖deviceContext->OMSetRenderTargets(1, m_pOutputTextureRTV.GetAddressOf(), m_pOutputTextureDSV.Get());// 設置視口deviceContext->RSSetViewports(1, &m_OutputViewPort); }TextureRender::End方法--結束對當前紋理的渲染,還原狀態
在對當前紋理的所有繪制方法調用完畢后,就需要調用該方法以恢復到原來的渲染目標視圖、深度/模板視圖以及視口。若在初始化時還指定了generateMips為true,還會給該紋理生成mipmap鏈:
void TextureRender::End(ComPtr<ID3D11DeviceContext> deviceContext) {// 恢復默認設定deviceContext->RSSetViewports(1, &m_CacheViewPort);deviceContext->OMSetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.Get());// 若之前有指定需要mipmap鏈,則生成if (m_GenerateMips){deviceContext->GenerateMips(m_pOutputTextureSRV.Get());}// 清空臨時緩存的渲染目標視圖和深度模板視圖m_pCacheDSV.Reset();m_pCacheRTV.Reset(); }最后就可以通過TextureRender::GetOutputTexture方法獲取渲染好的紋理了。
注意:不要將紋理既作為渲染目標,又作為著色器資源,雖然不會報錯,但這樣會導致程序運行速度被拖累。在VS的輸出窗口你可以看到它會將該資源強制從著色器中撤離,置其為NULL,以保證不會同時綁定在輸入和輸出端。
屏幕淡入/淡出效果的實現
該效果對應的特效文件為ScreenFadeEffect.cpp,著色器文件為ScreenFade_VS.hlsl和ScreenFade_PS.hlsl。
ScreenFadeEffect類在這不做講解,有興趣的可以查看第13章的自定義Effects管理類實現教程,或者去翻看ScreenFadeEffect類的源碼實現。
首先是ScreenFade.hlsli
// ScreenFade.hlsli Texture2D gTex : register(t0); SamplerState gSam : register(s0);cbuffer CBChangesEveryFrame : register(b0) {float g_FadeAmount; // 顏色程度控制(0.0f-1.0f)float3 g_Pad; }cbuffer CBChangesRarely : register(b1) {matrix g_WorldViewProj; }struct VertexPosTex {float3 PosL : POSITION;float2 Tex : TEXCOORD; };struct VertexPosHTex {float4 PosH : SV_POSITION;float2 Tex : TEXCOORD; };然后分別是對于的頂點著色器和像素著色器實現:
// ScreenFade_VS.hlsl #include "ScreenFade.hlsli"// 頂點著色器 VertexPosHTex VS(VertexPosTex vIn) {VertexPosHTex vOut;vOut.PosH = mul(float4(vIn.PosL, 1.0f), g_WorldViewProj);vOut.Tex = vIn.Tex;return vOut; } // ScreenFade_PS.hlsl #include "ScreenFade.hlsli"// 像素著色器 float4 PS(VertexPosHTex pIn) : SV_Target {return g_Tex.Sample(g_Sam, pIn.Tex) * float4(g_FadeAmount, g_FadeAmount, g_FadeAmount, 1.0f); }該套著色器通過gFadeAmount來控制最終輸出的顏色,我們可以通過對其進行動態調整來實現一些效果。當gFadeAmount從0到1時,屏幕從黑到正常顯示,即淡入效果;而當gFadeAmount從1到0時,平面從正常顯示到變暗,即淡出效果。
一開始像素著色器的返回值采用的是和Rastertek一樣的tex.Sample(sam, pIn.Tex) * gFadeAmount,但是在截屏出來的.dds文件觀看的時候顏色變得很奇怪
原本以為是輸出的文件格式亂了,但當我把Alpha通道關閉后,圖片卻一切正常了
故這里應該讓Alpha通道的值乘上1.0f以保持Alpha通道的一致性
為了實現屏幕的淡入淡出效果,我們需要一張渲染好的場景紋理,即通過TextureRender來實現。
首先我們看GameApp::UpdateScene方法中用于控制屏幕淡入淡出的部分:
// 更新淡入淡出值 if (m_FadeUsed) {m_FadeAmount += m_FadeSign * dt / 2.0f; // 2s時間淡入/淡出if (m_FadeSign > 0.0f && m_FadeAmount > 1.0f){m_FadeAmount = 1.0f;m_FadeUsed = false; // 結束淡入}else if (m_FadeSign < 0.0f && m_FadeAmount < 0.0f){m_FadeAmount = 0.0f;SendMessage(MainWnd(), WM_DESTROY, 0, 0); // 關閉程序// 這里不結束淡出是因為發送關閉窗口的消息還要過一會才真正關閉} }// ...// 退出程序,開始淡出 if (m_KeyboardTracker.IsKeyPressed(Keyboard::Escape)) {m_FadeSign = -1.0f;m_FadeUsed = true; }啟動程序的時候,mFadeSign的初始值是1.0f,這樣就使得打開程序的時候就在進行屏幕淡入。
而用戶按下Esc鍵退出的話,則先觸發屏幕淡出效果,等屏幕變黑后再發送關閉程序的消息給窗口。注意發送消息到真正關閉還相隔一段時間,在這段時間內也不要關閉淡出效果的繪制,否則最后那一瞬間又突然看到場景了。
然后在GameApp::DrawScene方法中,我們可以將繪制過程簡化成這樣:
// ****************** // 繪制Direct3D部分 //// 預先清空后備緩沖區 m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black)); m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);if (mFadeUsed) {// 開始淡入/淡出m_pScreenFadeRender->Begin(m_pd3dImmediateContext.Get()); }// 繪制主場景...if (mFadeUsed) {// 結束淡入/淡出,此時繪制的場景在屏幕淡入淡出渲染的紋理m_pScreenFadeRender->End(m_pd3dImmediateContext.Get());// 屏幕淡入淡出特效應用m_ScreenFadeEffect.SetRenderDefault(m_pd3dImmediateContext.Get());m_ScreenFadeEffect.SetFadeAmount(m_FadeAmount);m_ScreenFadeEffect.SetTexture(m_pScreenFadeRender->GetOutputTexture());m_ScreenFadeEffect.SetWorldViewProjMatrix(XMMatrixIdentity());m_ScreenFadeEffect.Apply(m_pd3dImmediateContext.Get());// 將保存的紋理輸出到屏幕m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_FullScreenShow.modelParts[0].vertexBuffer.GetAddressOf(), strides, offsets);m_pd3dImmediateContext->IASetIndexBuffer(m_FullScreenShow.modelParts[0].indexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);m_pd3dImmediateContext->DrawIndexed(6, 0, 0);// 務必解除綁定在著色器上的資源,因為下一幀開始它會作為渲染目標m_ScreenFadeEffect.SetTexture(nullptr);m_ScreenFadeEffect.Apply(m_pd3dImmediateContext.Get()); }對了,如果窗口被拉伸,那我們之前創建的紋理寬高就不適用了,需要重新創建一個。在GameApp::OnResize方法可以看到:
void GameApp::OnResize() {// ...// 攝像機變更顯示if (mCamera != nullptr){// ...// 屏幕淡入淡出紋理大小重設m_pScreenFadeRender = std::make_unique<TextureRender>(m_pd3dDevice.Get(), m_ClientWidth, m_ClientHeight, false);} }由于屏幕淡入淡出效果需要先繪制主場景到紋理,然后再用該紋理完整地繪制到屏幕上,就不說前面還進行了大量的深度測試了,兩次繪制下來使得在渲染淡入淡出效果的時候幀數下降比較明顯。因此不建議經常這么做。
小地圖的實現
關于小地圖的實現,有許多種方式。常見的如下:
可以看出,性能的消耗越往后要求越高。
因為本項目的場景是在夜間森林,并且樹是隨機生成的,因此采用第二種方式,但是地圖可視范圍為攝像機可視區域,并且不考慮額外繪制任何2D物件。
小地圖對應的特效文件為MinimapEffect.cpp,著色器文件為Minimap_VS.hlsl和Minimap_PS.hlsl。同樣這里只關注HLSL實現。
首先是Minimap.hlsli:
// Minimap.hlsliTexture2D g_Tex : register(t0); SamplerState g_Sam : register(s0);cbuffer CBChangesEveryFrame : register(b0) {float3 g_EyePosW; // 攝像機位置float g_Pad; }cbuffer CBDrawingStates : register(b1) {int g_FogEnabled; // 是否范圍可視float g_VisibleRange; // 3D世界可視范圍float2 g_Pad2;float4 g_RectW; // 小地圖xOz平面對應3D世界矩形區域(Left, Front, Right, Back)float4 g_InvisibleColor; // 不可視情況下的顏色 }struct VertexPosTex {float3 PosL : POSITION;float2 Tex : TEXCOORD; };struct VertexPosHTex {float4 PosH : SV_POSITION;float2 Tex : TEXCOORD; };為了能在小地圖中繪制出局部區域可視的效果,還需要依賴3D世界中的一些參數。其中gRectW對應的是3D世界中矩形區域(即x最小值, z最大值, x最大值, z最小值)。
然后是頂點著色器和像素著色器的實現:
// Minimap_VS.hlsl #include "Minimap.hlsli"// 頂點著色器 VertexPosHTex VS(VertexPosTex vIn) {VertexPosHTex vOut;vOut.PosH = float4(vIn.PosL, 1.0f);vOut.Tex = vIn.Tex;return vOut; } // Minimap_PS.hlsl #include "Minimap.hlsli"// 像素著色器 float4 PS(VertexPosHTex pIn) : SV_Target {// 要求Tex的取值范圍都在[0.0f, 1.0f], y值對應世界坐標z軸float2 PosW = pIn.Tex * float2(g_RectW.zw - g_RectW.xy) + g_RectW.xy;float4 color = g_Tex.Sample(g_Sam, pIn.Tex);[flatten]if (g_FogEnabled && length(PosW - g_EyePosW.xz) / g_VisibleRange > 1.0f){return g_InvisibleColor;}return color; }接下來我們需要通過Render-To-Texture技術,捕獲整個場景的俯視圖。關于小地圖的繪制放在了GameApp::InitResource中:
bool GameApp::InitResource() {// ...m_pMinimapRender = std::make_unique<TextureRender>(m_pd3dDevice.Get(), 400, 400, true);// 初始化網格,放置在右下角200x200m_Minimap.SetMesh(m_pd3dDevice, Geometry::Create2DShow(0.75f, -0.66666666f, 0.25f, 0.33333333f));// ...// 小地圖攝像機m_MinimapCamera = std::unique_ptr<FirstPersonCamera>(new FirstPersonCamera);m_MinimapCamera->SetViewPort(0.0f, 0.0f, 200.0f, 200.0f); // 200x200小地圖m_MinimapCamera->LookTo(XMVectorSet(0.0f, 10.0f, 0.0f, 1.0f),XMVectorSet(0.0f, -1.0f, 0.0f, 1.0f),XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f));m_MinimapCamera->UpdateViewMatrix();// ...// 小地圖范圍可視m_MinimapEffect.SetFogState(true);m_MinimapEffect.SetInvisibleColor(XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f));m_MinimapEffect.SetMinimapRect(XMVectorSet(-95.0f, 95.0f, 95.0f, -95.0f));m_MinimapEffect.SetVisibleRange(25.0f);// 方向光(默認)DirectionalLight dirLight[4];dirLight[0].Ambient = XMFLOAT4(0.15f, 0.15f, 0.15f, 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]);// ******************// 渲染小地圖紋理// m_BasicEffect.SetViewMatrix(m_MinimapCamera->GetViewXM());m_BasicEffect.SetProjMatrix(XMMatrixOrthographicLH(190.0f, 190.0f, 1.0f, 20.0f)); // 使用正交投影矩陣(中心在攝像機位置)// 關閉霧效m_BasicEffect.SetFogState(false);m_pMinimapRender->Begin(m_pd3dImmediateContext.Get());DrawScene(true);m_pMinimapRender->End(m_pd3dImmediateContext.Get());m_MinimapEffect.SetTexture(m_pMinimapRender->GetOutputTexture());// ... }通常小地圖的制作,建議是使用正交投影矩陣,XMMatrixOrthographicLH函數的中心在攝像機位置,不以攝像機為中心的話可以用XMMatrixOrthographicOffCenterLH函數。
然后如果窗口大小調整,為了保證小地圖在屏幕的顯示是在右下角,并且保持200x200,需要在GameApp::OnResize重新調整網格模型:
void GameApp::OnResize() {// ...// 攝像機變更顯示if (mCamera != nullptr){// ...// 小地圖網格模型重設m_Minimap.SetMesh(m_pd3dDevice.Get(), Geometry::Create2DShow(1.0f - 100.0f / m_ClientWidth * 2, -1.0f + 100.0f / m_ClientHeight * 2, 100.0f / m_ClientWidth * 2, 100.0f / m_ClientHeight * 2));} }最后是GameApp::DrawScene方法將小地圖紋理繪制到屏幕的部分:
// 此處用于小地圖和屏幕繪制 UINT strides[1] = { sizeof(VertexPosTex) }; UINT offsets[1] = { 0 };// 小地圖特效應用 m_MinimapEffect.SetRenderDefault(m_pd3dImmediateContext.Get()); m_MinimapEffect.Apply(m_pd3dImmediateContext.Get()); // 最后繪制小地圖 m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_Minimap.modelParts[0].vertexBuffer.GetAddressOf(), strides, offsets); m_pd3dImmediateContext->IASetIndexBuffer(m_Minimap.modelParts[0].indexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); m_pd3dImmediateContext->DrawIndexed(6, 0, 0);項目演示
本項目的場景沿用了第20章的森林場景,并搭配了夜晚霧效,在打開程序后可以看到屏幕淡入的效果,按下Esc后則屏幕淡出后退出。
然后人物在移動的時候,小地圖的可視范圍也會跟著移動。
DirectX11 With Windows SDK完整目錄
Github項目源碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
posted on 2019-05-05 10:01 NET未來之路 閱讀(...) 評論(...) 編輯 收藏轉載于:https://www.cnblogs.com/lonelyxmas/p/10811422.html
總結
以上是生活随笔為你收集整理的DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第十周学习总结
- 下一篇: Always keep in mind