儘管在上一章的動態天空盒中用到了Render-To-Texture技術,但那是針對紋理立方體的特化實現。考慮到該技術的應用層面很是廣,在這裏抽出獨立的一章專門來說有關它的通用實現以及各類應用。html
章節回顧 |
---|
深刻理解與使用2D紋理資源(重點閱讀ScreenGrab庫) |
23 立方體映射:動態天空盒的實現 |
DirectX11 With Windows SDK完整目錄git
Github項目源碼github
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。緩存
在前面的章節中,咱們默認的渲染目標是來自DXGI後備緩衝區,它是一個2D紋理。而Render-To-Texture技術,實際上就是使用一張2D紋理做爲渲染目標,但通常是本身新建的2D紋理。與此同時,這個紋理還可以綁定到着色器資源視圖(SRV)供着色器所使用,即本來用做輸出的紋理如今用做輸入。app
它能夠用於:ide
在這一章,咱們將展現下面這三種應用:函數
該類借鑑了上一章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鏈 };
它具備以下特色:測試
Begin
和End
方法,確保在這兩個方法調用之間的全部繪製都將輸出到該紋理Begin
方法會臨時緩存後備緩衝區、深度/模板緩衝區和視口,並在End
方法恢復,所以無需本身去從新設置這些東西如今咱們須要完成下面5個步驟:3d
具體代碼以下:
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; }
該方法緩存當前渲染管線綁定的渲染目標視圖、深度/模板視圖以及視口,並替換初始化好的這些資源。注意還須要清空一遍緩衝區:
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); }
在對當前紋理的全部繪製方法調用完畢後,就須要調用該方法以恢復到原來的渲染目標視圖、深度/模板視圖以及視口。若在初始化時還指定了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.hlsli Texture2D 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); // 初始化網格,放置在右下角200x200 m_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完整目錄
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。