DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现
原文:
DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现
前言上一章的静态天空盒已经可以满足绝大部分日常使用了。但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了。因此我们需要在运行期间构建动态天空盒,将周边物体绘制入当前的动态天空盒。 没了解过静态天空盒的读者请先移步到下面的链接:
DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 动态天空盒现在如果我们要让拥有反射/折射属性的物体映射其周围的物体和天空盒的话,就需要在每一帧重建动态天空盒,具体做法为:在每一帧将摄像机放置在待反射/折射物体中心,然后沿着各个坐标轴渲染除了自己以外的所有物体及静态天空盒共六次,一次对应纹理立方体的一个面。这样绘制好的动态天空盒就会记录下当前帧各物体所在的位置了。 但是这样做会带来非常大的性能开销,加上动态天空盒后,现在一个场景就要渲染七次,对应七个不同的渲染目标!如果要使用的话,尽可能减少所需要用到的动态天空盒数目。对于多个物体来说,你可以只对比较重要,关注度较高的反射/折射物体使用动态天空盒,其余的仍使用静态天空盒,甚至不用。毕竟动态天空盒也不是用在场景绘制,而是在物体上,可以不需要跟静态天空盒那样大的分辨率,通常情况下设置到256x256即可. 资源视图(Resource Views)回顾由于动态天空盒的实现同时要用到渲染目标视图(Render Target View)、深度模板视图(Depth Stencil View)和着色器资源视图(Shader Resource View),这里再进行一次回顾。 由于资源( 因此,我们需要用到一种叫资源视图(Resource Views)的类型,它主要有下面4种功能:
渲染目标视图用于将渲染管线的运行结果输出给其绑定的资源,即仅能设置给输出合并阶段。这意味着该资源主要用于写入,但是在进行混合操作时还需要读取该资源。通常渲染目标是一个二维的纹理,但它依旧可能会绑定其余类型的资源。这里不做讨论。 深度/模板视图同样用于设置给输出合并阶段,但是它用于深度测试和模板测试,决定了当前像素是通过还是会被抛弃,并更新深度/模板值。它允许一个资源同时绑定到深度模板视图和着色器资源视图,但是两个资源视图此时都是只读的,深度/模板视图也无法对其进行修改,这样该纹理就还可以绑定到任意允许的可编程着色器阶段上。如果要允许深度/模板缓冲区进行写入,则应该取消绑定在着色器的资源视图。 着色器资源视图提供了资源的读取权限,可以用于渲染管线的所有可编程着色器阶段中。通常该视图多用于像素着色器阶段,但要注意无法通过着色器写入该资源。 DynamicSkyRender类该类继承自上一章的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,const std::vector<std::wstring>& cubemapFilenames,// 立方体棱长 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,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]; // 动态天空盒每个面对应的渲染目标视图 }; 构造函数在完成静态天空盒的初始化后,就会调用 Render-To-Texture 技术因为之前的个人教程把计算着色器给跳过了, 在我们之前的程序中,我们都是渲染到后备缓冲区里。经过了这么多的章节,应该可以知道它的类型是 // 重设交换链并且重新创建渲染目标视图 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()); 这样经过一次绘制指令后就会将管线的运行结果输出到该视图绑定的后备缓冲区上,待所有绘制完成后,再调用 如果渲染目标视图绑定的是新建的2D纹理,而非后备缓冲区的话,那么渲染结果将会输出到该纹理上,并且不会直接在屏幕上显示出来。然后我们就可以使用该纹理做一些别的事情,比如绑定到着色器资源视图供可编程着色器使用,又或者将结果保存到文件等等。 虽然这个技术并不高深,但它的应用非常广泛:
DynamicSkyRender::InitResource方法--初始化动态纹理立方体资源创建动态纹理立方体和对应渲染目标视图、着色器资源视图在更新动态天空盒的时候,该纹理将会被用做渲染目标;而完成渲染后,它将用作着色器资源视图用于球体反射/折射的渲染。因此它需要在 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,texCube.ReleaseAndGetAddressOf())); // ... 把 接下来就是创建渲染目标视图的部分,纹理数组中的每个纹理都需要绑定一个渲染目标视图: // ****************** // 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 = 0; 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,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::Cache方法--缓存渲染目标视图该方法对应上面所说的第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()); } DynamicSkyRender::BeginCapture方法--指定天空盒某一面开始绘制该方法对应上面所说的第3,4步: void DynamicSkyRender::BeginCapture(ComPtr<ID3D11DeviceContext> deviceContext,const XMFLOAT3& pos,float nearZ,float farZ) { static XMVECTORF32 ups[6] = { {{ 0.0f,1.0f,0.0f }},// +X {{ 0.0f,// -X {{ 0.0f,-1.0f,// +Y {{ 0.0f,// -Y {{ 0.0f,// +Z {{ 0.0f,0.0f }} // -Z }; static XMVECTORF32 looks[6] = { {{ 1.0f,// +X {{ -1.0f,// -Z }; // 设置天空盒摄像机 m_pCamera.LookTo(XMLoadFloat3(&pos),looks[face].v,ups[face].v); m_pCamera.UpdateViewMatrix(); // 这里尽可能捕获近距离物体 m_pCamera.SetFrustum(XM_PIDIV2,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,0); // 设置渲染目标和深度模板视图 deviceContext->OMSetRenderTargets(1,m_pDynamicCubeMapRTVs[face].GetAddressOf(),m_pDynamicCubeMapDSV.Get()); // 设置视口 deviceContext->RSSetViewports(1,&m_pCamera.GetViewPort()); } 在调用该方法后,就可以开始绘制到天空盒的指定面了,直到下一次 DynamicSkyRender::Restore方法--恢复之前绑定的资源并清空缓存该方法对应上面所说的第7,8步: void DynamicSkyRender::Restore(ComPtr<ID3D11DeviceContext> deviceContext,const Camera & camera) { // 恢复默认设定 deviceContext->RSSetViewports(1,&camera.GetViewPort()); deviceContext->OMSetRenderTargets(1,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::DrawScene方法在GameApp类多了这样一个重载的成员函数: void GameApp::DrawScene(bool drawCenterSphere); 该方法额外添加了一个参数,仅用于控制中心球是否要绘制,而其余的物体不管怎样都是要绘制出来的。使用该重载方法有利于减少代码重复,这里面的大部分物体都需要绘制7次。 假如只考虑 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),static_cast<D3D11_TEXTURECUBE_FACE>(i)); // 不绘制中心球 DrawScene(false); } // 恢复之前的绘制设定 m_pDaylight->Restore(m_pd3dImmediateContext.Get(),*m_pCamera); // ****************** // 绘制场景 // // 预先清空 m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(),reinterpret_cast<const float*>(&Colors::Black)); m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(),0); // 绘制中心球 DrawScene(true); // 省略文字绘制部分... } 至于有形参的 使用几何着色器的动态天空盒这部分内容并没有融入到项目中,因此只是简单地提及一下。 在上面的内容中,我们对一个场景绘制了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(),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(),m_pDynamicCubeMapDSV.GetAddressOf())); 在输出合并阶段这样绑定到渲染管线: deviceContext->OMSetRenderTargets(1,m_pDynamicCubeMapRTV.Get(),m_pDynamicCubeMapDSV.Get()); 这样做会使得一次调用绘制可以同时向该渲染目标视图对应的六个纹理进行渲染。 在HLSL,现在需要同时在常量缓冲区提供6个观察矩阵。顶点着色阶段将顶点直接传递给几何着色器,然后几何着色器重复传递一个顶点六次,但区别在于每次将会传递给不同的渲染目标。这需要依赖系统值 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(); } } 上面的代码是经过魔改的,至于与它相关的示例项目 这种方法有两点不那么吸引人的原因:
但还有一种情况它的表现还算不俗。假如你现在有一个动态天空系统,这些云层会移动,并且颜色随着时间变化。因为天空正在实时变化,我们不能使用预先烘焙的天空盒纹理来进行反射/折射。使用几何着色器绘制天空盒的方法在性能上不会损失太大。 模型的折射dielectric(绝缘体?)是指能够折射光线的透明材料,如下图。当光束射到绝缘体表面时,一部分光会被反射,还有一部分光会基于斯涅尔定律进行折射。公式如下: 其中n1和n2分半是两个介质的折射率,θ1和θ2分别是入射光、折射光与界面法线的夹角,叫做入射角和折射角。 当 在物理上,光线在从绝缘体出来后还会进行一次弯折。但是在实时渲染中,通常只考虑第一次折射的情况。 HLSL提供了固有函数 float3 refract(float3 incident,float3 normal,float eta);
通常,空气的折射率为 之前的项目中 在HLSL里,你只需要在像素着色器中加上这部分代码,就可以实现折射效果了( // 折射 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完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- 在Mono上用F#签署DLL的问题
- windows – 通过在同一服务器上运行的KMS激活MS Office 201
- .net – 获取“DialogResult只能在Window被创建之后被设置,
- wpf – 如何使用正确的Windows系统颜色?
- xaml – 如何将图钉添加到Windows Phone 8.1地图控件?
- windows – 有没有办法使用WScript.Shell启动使用VBScript最
- Windows Server 2016-客户端加域准备工作
- windows-server-2008-r2 – Windows防火墙拒绝连接,除非打开
- Win10设置文件夹权限报错-(提示:无法枚举容器中的对象 访问
- 如何为Windows上的一个线程保留核心?