上一章的靜態天空盒已經能夠知足絕大部分平常使用了。但對於自帶反射/折射屬性的物體來講,它須要依賴天空盒進行繪製,但靜態天空盒並不會記錄周邊的物體,更不用說正在其周圍運動的物體了。所以咱們須要在運行期間構建動態天空盒,將周邊物體繪製入當前的動態天空盒。html
沒了解過靜態天空盒的讀者請先移步到下面的連接:git
章節回顧 |
---|
22 立方體映射:靜態天空盒的讀取與實現 |
DirectX11 With Windows SDK完整目錄github
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。數組
如今若是咱們要讓擁有反射/折射屬性的物體映射其周圍的物體和天空盒的話,就須要在每一幀重建動態天空盒,具體作法爲:在每一幀將攝像機放置在待反射/折射物體中心,而後沿着各個座標軸渲染除了本身之外的全部物體及靜態天空盒共六次,一次對應紋理立方體的一個面。這樣繪製好的動態天空盒就會記錄下當前幀各物體所在的位置了。緩存
可是這樣作會帶來很是大的性能開銷,加上動態天空盒後,如今一個場景就要渲染七次,對應七個不一樣的渲染目標!若是要使用的話,儘量減小所須要用到的動態天空盒數目。對於多個物體來講,你能夠只對比較重要,關注度較高的反射/折射物體使用動態天空盒,其他的仍使用靜態天空盒,甚至不用。畢竟動態天空盒也不是用在場景繪製,而是在物體上,能夠不須要跟靜態天空盒那樣大的分辨率,一般狀況下設置到256x256便可.app
因爲動態天空盒的實現同時要用到渲染目標視圖(Render Target View)、深度模板視圖(Depth Stencil View)和着色器資源視圖(Shader Resource View),這裏再進行一次回顧。ide
因爲資源(ID3D11Resource
)自己的類型十分複雜,好比一個ID3D11Texture2D
自己既能夠是一個紋理,也能夠是一個紋理數組,但紋理數組在元素個數爲6時有可能會被用做立方體紋理,就這樣直接綁定到渲染管線上是沒法肯定它自己究竟要被用做什麼樣的類型的。好比說做爲着色器資源,它能夠是Texture2D
, Texture2DArray
, TextureCube
的任意一種。函數
所以,咱們須要用到一種叫資源視圖(Resource Views)的類型,它主要有下面4種功能:性能
渲染目標視圖用於將渲染管線的運行結果輸出給其綁定的資源,即僅能設置給輸出合併階段。這意味着該資源主要用於寫入,可是在進行混合操做時還須要讀取該資源。一般渲染目標是一個二維的紋理,但它依舊可能會綁定其他類型的資源。這裏不作討論。
深度/模板視圖一樣用於設置給輸出合併階段,可是它用於深度測試和模板測試,決定了當前像素是經過仍是會被拋棄,並更新深度/模板值。它容許一個資源同時綁定到深度模板視圖和着色器資源視圖,可是兩個資源視圖此時都是隻讀的,深度/模板視圖也沒法對其進行修改,這樣該紋理就還能夠綁定到任意容許的可編程着色器階段上。若是要容許深度/模板緩衝區進行寫入,則應該取消綁定在着色器的資源視圖。
着色器資源視圖提供了資源的讀取權限,能夠用於渲染管線的全部可編程着色器階段中。一般該視圖多用於像素着色器階段,但要注意沒法經過着色器寫入該資源。
該類繼承自上一章的SkyRender類,用以支持動態天空盒的相關操做。
class DynamicSkyRender : public SkyRender { public: DynamicSkyRender(ID3D11Device* device, ID3D11DeviceContext* deviceContext, const std::wstring& cubemapFilename, float skySphereRadius, // 天空球半徑 int dynamicCubeSize, // 立方體棱長 bool generateMips = false); // 默認不爲靜態天空盒生成mipmaps // 動態天空盒必然生成mipmaps DynamicSkyRender(ID3D11Device* device, ID3D11DeviceContext* deviceContext, const std::vector<std::wstring>& cubemapFilenames, float skySphereRadius, // 天空球半徑 int dynamicCubeSize, // 立方體棱長 bool generateMips = false); // 默認不爲靜態天空盒生成mipmaps // 動態天空盒必然生成mipmaps // 緩存當前渲染目標視圖 void Cache(ID3D11DeviceContext* deviceContext, BasicEffect& effect); // 指定天空盒某一面開始繪製,須要先調用Cache方法 void BeginCapture(ID3D11DeviceContext* deviceContext, BasicEffect& effect, const DirectX::XMFLOAT3& pos, D3D11_TEXTURECUBE_FACE face, float nearZ = 1e-3f, float farZ = 1e3f); // 恢復渲染目標視圖及攝像機,並綁定當前動態天空盒 void Restore(ID3D11DeviceContext* deviceContext, BasicEffect& effect, const Camera& camera); // 獲取動態天空盒 // 注意:該方法只能在Restore後再調用 ID3D11ShaderResourceView* GetDynamicTextureCube(); // 獲取當前用於捕獲的天空盒 const Camera& GetCamera() const; // 設置調試對象名 void SetDebugObjectName(const std::string& name); private: void InitResource(ID3D11Device* device, int dynamicCubeSize); private: ComPtr<ID3D11RenderTargetView> m_pCacheRTV; // 臨時緩存的後備緩衝區 ComPtr<ID3D11DepthStencilView> m_pCacheDSV; // 臨時緩存的深度/模板緩衝區 FirstPersonCamera m_pCamera; // 捕獲當前天空盒其中一面的攝像機 ComPtr<ID3D11DepthStencilView> m_pDynamicCubeMapDSV; // 動態天空盒渲染對應的深度/模板視圖 ComPtr<ID3D11ShaderResourceView> m_pDynamicCubeMapSRV; // 動態天空盒對應的着色器資源視圖 ComPtr<ID3D11RenderTargetView> m_pDynamicCubeMapRTVs[6]; // 動態天空盒每一個面對應的渲染目標視圖 };
構造函數在完成靜態天空盒的初始化後,就會調用DynamicSkyRender::InitResource
方法來初始化動態天空盒。
由於以前的我的教程把計算着色器給跳過了,Render-To-Texture
恰好又在龍書裏的這章,只好把它帶到這裏來說了。
在咱們以前的程序中,咱們都是渲染到後備緩衝區裏。通過了這麼多的章節,應該能夠知道它的類型是ID3D11Texture2D
,僅僅是一個2D紋理罷了。在d3dApp
類裏能夠看到這部分的代碼:
// 重設交換鏈而且從新建立渲染目標視圖 ComPtr<ID3D11Texture2D> backBuffer; HR(m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_B8G8R8A8_UNORM, 0)); // 注意此處DXGI_FORMAT_B8G8R8A8_UNORM HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf()))); HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf())); backBuffer.Reset();
這裏渲染目標視圖綁定的是從新調整過大小的後備緩衝區。而後把該視圖交給輸出合併階段:
// 將渲染目標視圖和深度/模板緩衝區結合到管線 m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());
這樣通過一次繪製指令後就會將管線的運行結果輸出到該視圖綁定的後備緩衝區上,待全部繪製完成後,再調用IDXGISwapChain::Present
方法來交換前/後臺以達到畫面更新的效果。
若是渲染目標視圖綁定的是新建的2D紋理,而非後備緩衝區的話,那麼渲染結果將會輸出到該紋理上,而且不會直接在屏幕上顯示出來。而後咱們就可使用該紋理作一些別的事情,好比綁定到着色器資源視圖供可編程着色器使用,又或者將結果保存到文件等等。
雖然這個技術並不高深,但它的應用很是普遍:
在更新動態天空盒的時候,該紋理將會被用作渲染目標;而完成渲染後,它將用做着色器資源視圖用於球體反射/折射的渲染。所以它須要在BindFlag
設置D3D11_BIND_RENDER_TARGET
和D3D11_BIND_SHADER_RESOURCE
。
void DynamicSkyRender::InitResource(ID3D11Device * device, int dynamicCubeSize) { // ****************** // 1. 建立紋理數組 // ComPtr<ID3D11Texture2D> texCube; D3D11_TEXTURE2D_DESC texDesc; texDesc.Width = dynamicCubeSize; texDesc.Height = dynamicCubeSize; texDesc.MipLevels = 0; texDesc.ArraySize = 6; 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 | D3D11_RESOURCE_MISC_TEXTURECUBE; // 如今texCube用於新建紋理 HR(device->CreateTexture2D(&texDesc, nullptr, texCube.ReleaseAndGetAddressOf())); // ...
把MipLevels
設置爲0是要說明該紋理將會在後面生成完整的mipmap鏈,但不表明建立紋理後當即就會生成,須要在後續經過GenerateMips
方法纔會生成出來。爲此,還須要在MiscFlags
設置D3D11_RESOURCE_MISC_GENERATE_MIPS
。固然,把該紋理用做天空盒的D3D11_RESOURCE_MISC_TEXTURECUBE
標籤也不能漏掉。
接下來就是建立渲染目標視圖的部分,紋理數組中的每一個紋理都須要綁定一個渲染目標視圖:
// ****************** // 2. 建立渲染目標視圖 // D3D11_RENDER_TARGET_VIEW_DESC rtvDesc; rtvDesc.Format = texDesc.Format; rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; rtvDesc.Texture2DArray.MipSlice = 0; // 一個視圖只對應一個紋理數組元素 rtvDesc.Texture2DArray.ArraySize = 1; // 每一個元素建立一個渲染目標視圖 for (int i = 0; i < 6; ++i) { rtvDesc.Texture2DArray.FirstArraySlice = i; HR(device->CreateRenderTargetView( texCube.Get(), &rtvDesc, m_pDynamicCubeMapRTVs[i].GetAddressOf())); } // ...
最後就是爲整個紋理數組以天空盒的形式建立着色器資源視圖:
// ****************** // 3. 建立着色器目標視圖 // D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; srvDesc.Format = texDesc.Format; srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE; srvDesc.TextureCube.MostDetailedMip = 0; srvDesc.TextureCube.MipLevels = -1; // 使用全部的mip等級 HR(device->CreateShaderResourceView( texCube.Get(), &srvDesc, m_pDynamicCubeMapSRV.GetAddressOf()));
到這裏尚未結束。
一般天空盒的面分辨率和後備緩衝區的分辨率不一致,這意味着咱們還須要建立一個和天空盒表面分辨率一致的深度緩衝區(無模板測試):
// ****************** // 4. 建立深度/模板緩衝區與對應的視圖 // texDesc.Width = dynamicCubeSize; texDesc.Height = dynamicCubeSize; texDesc.MipLevels = 1; texDesc.ArraySize = 1; texDesc.SampleDesc.Count = 1; texDesc.SampleDesc.Quality = 0; texDesc.Format = DXGI_FORMAT_D32_FLOAT; 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_pDynamicCubeMapDSV.GetAddressOf()));
一樣,視口也須要通過適配。不過以前的攝像機類能夠幫咱們簡化一下:
// ****************** // 5. 初始化視口 // m_pCamera.SetViewPort(0.0f, 0.0f, static_cast<float>(dynamicCubeSize), static_cast<float>(dynamicCubeSize)); }
講完了初始化的事,就要開始留意幀與幀之間的動態天空盒渲染操做了。除了繪製部分之外的操做都交給了DynamicSkyRender
類來完成。總結以下(粗體部分爲該方法完成的任務):
ResizeBuffer
時由於引用的遺留出現問題)該方法對應上面所說的第1,2步:
void DynamicSkyRender::Cache(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect) { deviceContext->OMGetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.GetAddressOf()); // 清掉綁定在着色器的動態天空盒,須要當即生效 effect.SetTextureCube(nullptr); effect.Apply(deviceContext.Get()); }
該方法對應上面所說的第3,4步:
void DynamicSkyRender::BeginCapture(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, const XMFLOAT3& pos, D3D11_TEXTURECUBE_FACE face, float nearZ, float farZ) { static XMVECTORF32 ups[6] = { {{ 0.0f, 1.0f, 0.0f, 0.0f }}, // +X {{ 0.0f, 1.0f, 0.0f, 0.0f }}, // -X {{ 0.0f, 0.0f, -1.0f, 0.0f }}, // +Y {{ 0.0f, 0.0f, 1.0f, 0.0f }}, // -Y {{ 0.0f, 1.0f, 0.0f, 0.0f }}, // +Z {{ 0.0f, 1.0f, 0.0f, 0.0f }} // -Z }; static XMVECTORF32 looks[6] = { {{ 1.0f, 0.0f, 0.0f, 0.0f }}, // +X {{ -1.0f, 0.0f, 0.0f, 0.0f }}, // -X {{ 0.0f, 1.0f, 0.0f, 0.0f }}, // +Y {{ 0.0f, -1.0f, 0.0f, 0.0f }}, // -Y {{ 0.0f, 0.0f, 1.0f, 0.0f }}, // +Z {{ 0.0f, 0.0f, -1.0f, 0.0f }}, // -Z }; // 設置天空盒攝像機 m_pCamera.LookTo(XMLoadFloat3(&pos) , looks[face].v, ups[face].v); m_pCamera.UpdateViewMatrix(); // 這裏儘量捕獲近距離物體 m_pCamera.SetFrustum(XM_PIDIV2, 1.0f, nearZ, farZ); // 應用觀察矩陣、投影矩陣 effect.SetViewMatrix(m_pCamera.GetViewXM()); effect.SetProjMatrix(m_pCamera.GetProjXM()); // 清空緩衝區 deviceContext->ClearRenderTargetView(m_pDynamicCubeMapRTVs[face].Get(), reinterpret_cast<const float*>(&Colors::Black)); deviceContext->ClearDepthStencilView(m_pDynamicCubeMapDSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // 設置渲染目標和深度模板視圖 deviceContext->OMSetRenderTargets(1, m_pDynamicCubeMapRTVs[face].GetAddressOf(), m_pDynamicCubeMapDSV.Get()); // 設置視口 deviceContext->RSSetViewports(1, &m_pCamera.GetViewPort()); }
在調用該方法後,就能夠開始繪製到天空盒的指定面了,直到下一次DynamicSkyRender::BeginCapture
或DynamicSkyRender::Restore
被調用。
該方法對應上面所說的第7,8步:
void DynamicSkyRender::Restore(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, const Camera & camera) { // 恢復默認設定 deviceContext->RSSetViewports(1, &camera.GetViewPort()); deviceContext->OMSetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.Get()); // 生成動態天空盒後必需要生成mipmap鏈 deviceContext->GenerateMips(m_pDynamicCubeMapSRV.Get()); effect.SetViewMatrix(camera.GetViewXM()); effect.SetProjMatrix(camera.GetProjXM()); // 恢復綁定的動態天空盒 effect.SetTextureCube(m_pDynamicCubeMapSRV); // 清空臨時緩存的渲染目標視圖和深度模板視圖 m_pCacheDSV.Reset(); m_pCacheRTV.Reset(); }
在GameApp類多了這樣一個重載的成員函數:
void GameApp::DrawScene(bool drawCenterSphere);
該方法額外添加了一個參數,僅用於控制中心球是否要繪製,而其他的物體無論怎樣都是要繪製出來的。使用該重載方法有利於減小代碼重複,這裏面的大部分物體都須要繪製7次。
假如只考慮Daylight
天空盒的話,無形參的GameApp::DrawScene
方法關於3D場景的繪製能夠簡化成這樣:
void GameApp::DrawScene() { // ****************** // 生成動態天空盒 // // 保留當前繪製的渲染目標視圖和深度模板視圖 m_pDaylight->Cache(m_pd3dImmediateContext.Get(), m_BasicEffect); // 繪製動態天空盒的每一個面(以球體爲中心) for (int i = 0; i < 6; ++i) { m_pDaylight->BeginCapture(m_pd3dImmediateContext.Get(), m_BasicEffect, XMFLOAT3(0.0f, 0.0f, 0.0f), static_cast<D3D11_TEXTURECUBE_FACE>(i)); // 不繪製中心球 DrawScene(false); } // 恢復以前的繪製設定 m_pDaylight->Restore(m_pd3dImmediateContext.Get(), m_BasicEffect, *m_pCamera); // ****************** // 繪製場景 // // 預先清空 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); // 繪製中心球 DrawScene(true); // 省略文字繪製部分... }
至於有形參的GameApp::DrawScene
方法就不在這裏給出,能夠在項目源碼看到。
這部份內容並無融入到項目中,所以只是簡單地說起一下。
在上面的內容中,咱們對一個場景繪製了6次,從而生成動態天空盒。爲了減小繪製調用,這裏可使用幾何着色器來使得只須要進行1次繪製調用就能夠生成整個動態天空盒。
首先,建立一個渲染目標視圖綁定整個紋理數組:
D3D11_RENDER_TARGET_VIEW_DESC rtvDesc; rtvDesc.Format = texDesc.Format; rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; rtvDesc.Texture2DArray.FirstArraySlice = 0; rtvDesc.Texture2DArray.ArraySize = 6; rtvDesc.Texture2DArray.MipSlice = 0; HR(device->CreateRenderTargetView( texCube.Get(), &rtvDesc, m_pDynamicCubeMapRTV.GetAddressOf())); rtvDesc.
緊接着,就是要建立一個深度緩衝區數組(一個對應立方體面,元素個數爲6):
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc; dsvDesc.Format = DXGI_FORMAT_D32_FLOAT; dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2DARRAY; dsvDesc.Texture2DArray.FirstArraySlice = 0; dsvDesc.Texture2DArray.ArraySize = 6; dsvDesc.Texture2DArray.MipSlice = 0; HR(device->CreateDepthStencilView( depthTexArray.Get(), &dsvDesc, m_pDynamicCubeMapDSV.GetAddressOf()));
在輸出合併階段這樣綁定到渲染管線:
deviceContext->OMSetRenderTargets(1, m_pDynamicCubeMapRTV.Get(), m_pDynamicCubeMapDSV.Get());
這樣作會使得一次調用繪製能夠同時向該渲染目標視圖對應的六個紋理進行渲染。
在HLSL,如今須要同時在常量緩衝區提供6個觀察矩陣。頂點着色階段將頂點直接傳遞給幾何着色器,而後幾何着色器重複傳遞一個頂點六次,但區別在於每次將會傳遞給不一樣的渲染目標。這須要依賴系統值SV_RenderTargetArrayIndex
來實現,它是一個整型索引值,而且只能由幾何着色器寫入來指定當前須要往渲染目標視圖所綁定的紋理數組中的哪個紋理。該系統值只能用於綁定了紋理數組的視圖。
struct VertexPosTex { float3 PosL : POSITION; float2 Tex : TEXCOORD; }; struct VertexPosHTexRT { float3 PosH : SV_POSITION; float2 Tex : TEXCOORD; uint RTIndex : SV_RenderTargetArrayIndex; }; [maxvertexcount(18)] void GS(trangle VertexPosTex input[3], inout TriangleStream<VertexPosTexRT> output) { for (int i = 0; i < 6; ++i) { VertexPosTexRT vertex; // 指定該三角形到第i個渲染目標 vertex.RTIndex = i; for (int j = 0; j < 3; ++j) { vertex.PosH = mul(input[j].PosL, mul(g_Views[i], g_Proj)); vertex.Tex = input[j].Tex; output.Append(vertex); } output.RestartStrip(); } }
上面的代碼是通過魔改的,至於與它相關的示例項目CubeMapGS
只能在舊版的Microsoft DirectX SDK的Samples中看到了。
這種方法有兩點不那麼吸引人的緣由:
但還有一種狀況它的表現還算不俗。假如你如今有一個動態天空系統,這些雲層會移動,而且顏色隨着時間變化。由於天空正在實時變化,咱們不能使用預先烘焙的天空盒紋理來進行反射/折射。使用幾何着色器繪製天空盒的方法在性能上不會損失太大。
dielectric(絕緣體?)是指可以折射光線的透明材料,以下圖。當光束射到絕緣體表面時,一部分光會被反射,還有一部分光會基於斯涅爾定律進行折射。公式以下:
\[n_{1}sinθ_{1} = n_{2}sinθ_{2}\]
其中n1和n2分別是兩個介質的折射率,θ1和θ2則分別是入射光、折射光與界面法線的夾角,叫作入射角和折射角。
當n1 = n2
時,θ1 = θ2
(無折射)
當n2 > n1
時,θ2 < θ1
(光線向內彎折)
當n1 > n2
時,θ2 > θ1
(光線向外彎折)
在物理上,光線在從絕緣體出來後還會進行一次彎折。可是在實時渲染中,一般只考慮第一次折射的狀況。
HLSL提供了固有函數refract
來幫助咱們計算折射向量:
float3 refract(float3 incident, float3 normal, float eta);
incident
指的是入射光向量
normal
指的是交界面處的法向量(與入射光點乘的結果爲負值)
eta
指的是n1/n2
,即介質之間的折射比
一般,空氣的折射率爲1.0
,水的折射率爲1.33
,玻璃的折射率爲1.51
.
以前的項目中Material::Reflect
來調整反射顏色,如今你能夠拿它來調整折射顏色。
在HLSL裏,你只須要在像素着色器中加上這部分代碼,就能夠實現折射效果了(gEta
出如今常量緩衝區中):
// 折射 if (g_RefractionEnabled) { float3 incident = -toEyeW; float3 refractionVector = refract(incident, pIn.NormalW, g_Eta); float4 refractionColor = g_TexCube.Sample(g_Sam, refractionVector); litColor += g_Material.Reflect * refractionColor; }
該項目實現了反射和折射
DirectX11 With Windows SDK完整目錄
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。