上一章的静态天空盒已经能够知足绝大部分平常使用了。但对于自带反射/折射属性的物体来讲,它须要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了。所以咱们须要在运行期间构建动态天空盒,将周边物体绘制入当前的动态天空盒。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,以及有什么问题也能够在这里汇报。