DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现

前言

注意:这一章对应项目代码2.x.x版本,持有低版本项目代码的同学请到Github更新一份

上一章的静态天空盒已经可以满足绝大部分日常使用了。但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了。因此我们需要在运行期间构建动态天空盒,将周边物体绘制入当前的动态天空盒。

没了解过静态天空盒的读者请先移步到下面的链接

章节回顾
22 立方体映射:静态天空盒的读取与实现

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

动态天空盒

现在如果我们要让拥有反射/折射属性的物体映射其周围的物体和天空盒的话,就需要在每一帧重建动态天空盒,具体做法为:在每一帧将摄像机放置在待反射/折射物体中心,然后沿着各个坐标轴渲染除了自己以外的所有物体及静态天空盒共六次,一次对应纹理立方体的一个面。这样绘制好的动态天空盒就会记录下当前帧各物体所在的位置了。

但是这样做会带来非常大的性能开销,加上动态天空盒后,现在一个场景就要渲染七次,对应七个不同的渲染目标!如果要使用的话,尽可能减少所需要用到的动态天空盒数目。对于多个物体来说,你可以只对比较重要,关注度较高的反射/折射物体使用动态天空盒,其余的仍使用静态天空盒,甚至不用。毕竟动态天空盒也不是用在场景绘制,而是在物体上,可以不需要跟静态天空盒那样大的分辨率,通常情况下设置到256x256即可.

资源视图(Resource Views)回顾

由于动态天空盒的实现同时要用到渲染目标视图(Render Target View)深度模板视图(Depth Stencil View)着色器资源视图(Shader Resource View),这里再进行一次回顾。

由于资源(ID3D11Resource)本身的类型十分复杂,比如一个ID3D11Texture2D本身既可以是一个纹理,也可以是一个纹理数组,但纹理数组在元素个数为6时有可能会被用作立方体纹理,就这样直接绑定到渲染管线上是无法确定它本身究竟要被用作什么样的类型的。比如说作为着色器资源,它可以是Texture2D, Texture2DArray, TextureCube的任意一种。

因此,我们需要用到一种叫资源视图(Resource Views)的类型,它主要有下面4种功能:

  1. 绑定要使用的资源,像指针一样引用
  2. 解释该资源具体会被用作什么类型
  3. 指定该资源的数组元素范围ArraySlices,以及纹理的子资源范围MipSlices
  4. 说明该资源最终在渲染管线上的用途,这是资源视图本身确定的,并且具有一定的约束性

渲染目标视图用于将渲染管线的运行结果输出给其绑定的资源,即仅能设置给输出合并阶段。这意味着该资源主要用于写入,但是在进行混合操作时还需要读取该资源。通常渲染目标是一个二维的纹理,但它依旧可能会绑定其余类型的资源。这里不做讨论。

深度/模板视图同样用于设置给输出合并阶段,但是它用于深度测试和模板测试,决定了当前像素是通过还是会被抛弃,并更新深度/模板值。它允许一个资源同时绑定到深度模板视图和着色器资源视图,但是两个资源视图此时都是只读的,深度/模板视图也无法对其进行修改,这样该纹理就还可以绑定到任意允许的可编程着色器阶段上。如果要允许深度/模板缓冲区进行写入,则应该取消绑定在着色器的资源视图。

着色器资源视图提供了资源的读取权限,可以用于渲染管线的所有可编程着色器阶段中。通常该视图多用于像素着色器阶段,但要注意无法通过着色器写入该资源。

Render-To-Texture 技术

因为之前的个人教程把计算着色器给跳过了,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纹理,而非后备缓冲区的话,那么渲染结果将会输出到该纹理上,并且不会直接在屏幕上显示出来。然后我们就可以使用该纹理做一些别的事情,比如绑定到着色器资源视图供可编程着色器使用,又或者将结果保存到文件等等。

准备动态天空盒所需的资源

首先我们需要创建一个256x256分辨率的TextureCube,并提供一个同样是256x256分辨率的深度缓冲区:

m_pDynamicTextureCube = std::make_unique<TextureCube>(m_pd3dDevice.Get(), 256, 256, DXGI_FORMAT_R8G8B8A8_UNORM);
m_pDynamicCubeDepthTexture = std::make_unique<Depth2D>(m_pd3dDevice.Get(), 256, 256);
m_TextureManager.AddTexture("DynamicCube", m_pDynamicTextureCube->GetShaderResource());

默认情况下,TextureCube会创建完整资源的着色器资源视图、6个数组元素的着色器资源视图和渲染目标视图;Depth2D会创建深度/模板视图、

因为该动态天空盒最终是要用作着色器资源的,我们可以把它挂载到TextureManager上。

动态天空盒的绘制

绘制动态天空盒的过程如下:

对每个立方体天空盒

  1. 清空设置在像素着色器的着色器资源视图(绑定了动态天空盒资源)
  2. 对准某一个坐标轴,设置正确的观察矩阵;投影矩阵以90度垂直视场角,1.0的宽高比架设,并调整视口
  3. 清理当前天空盒面对应的纹理和深度缓冲区,并绑定到设备上下文
  4. 和往常一样绘制物体和静态天空盒
  5. 回到步骤3,继续下一个面的绘制,直到6个面都完成渲染
  6. 利用动态天空盒绘制反射/折射物体,和往常一样绘制剩余物体,并利用静态天空盒绘制天空
// 下面的代码省略视锥体裁剪 和 无关部分

// 创建动态天空盒
m_pCubeCamera = std::make_shared<FirstPersonCamera>();
m_pCubeCamera->SetFrustum(XM_PIDIV2, 1.0f, 0.1f, 1000.0f);
m_pCubeCamera->SetViewPort(0.0f, 0.0f, 256.0f, 256.0f);

static XMFLOAT3 ups[6] = {
    { 0.0f, 1.0f, 0.0f },	// +X
    { 0.0f, 1.0f, 0.0f },   // -X
    { 0.0f, 0.0f, -1.0f },  // +Y
    { 0.0f, 0.0f, 1.0f },   // -Y
    { 0.0f, 1.0f, 0.0f },   // +Z
    { 0.0f, 1.0f, 0.0f }    // -Z
};

static XMFLOAT3 looks[6] = {
    { 1.0f, 0.0f, 0.0f },	// +X
    { -1.0f, 0.0f, 0.0f },  // -X
    { 0.0f, 1.0f, 0.0f },	// +Y
    { 0.0f, -1.0f, 0.0f },  // -Y
    { 0.0f, 0.0f, 1.0f },	// +Z
    { 0.0f, 0.0f, -1.0f },  // -Z
};

// 绘制动态天空盒的每个面(以球体为中心)
for (int i = 0; i < 6; ++i)
{
    m_pCubeCamera->LookTo(m_CenterSphere.GetTransform().GetPosition(), looks[i], ups[i]);

    // 不绘制中心球
    DrawScene(false, *m_pCubeCamera, m_pDynamicTextureCube->GetRenderTarget(i), m_pDynamicCubeDepthTexture->GetDepthStencil());
}
void GameApp::DrawScene(bool drawCenterSphere, const Camera& camera, ID3D11RenderTargetView* pRTV, ID3D11DepthStencilView* pDSV)
{
    float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
    m_pd3dImmediateContext->ClearRenderTargetView(pRTV, black);
    m_pd3dImmediateContext->ClearDepthStencilView(pDSV, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    m_pd3dImmediateContext->OMSetRenderTargets(1, &pRTV, pDSV);

    D3D11_VIEWPORT viewport = camera.GetViewPort();
    m_pd3dImmediateContext->RSSetViewports(1, &viewport);
    // 绘制模型
    m_BasicEffect.SetViewMatrix(camera.GetViewMatrixXM());
    m_BasicEffect.SetProjMatrix(camera.GetProjMatrixXM());   
    m_BasicEffect.SetEyePos(camera.GetPosition());
    m_BasicEffect.SetRenderDefault();

    /*
      省略中心球绘制代码
    */
    
    m_BasicEffect.SetReflectionEnabled(false);
    m_BasicEffect.SetRefractionEnabled(false);
    
    // 绘制地面
    m_Ground.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

    // 绘制五个圆柱
    for (auto& cylinder : m_Cylinders)
        cylinder.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
    
    // 绘制五个圆球
    for (auto& sphere : m_Spheres)
        sphere.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

    // 绘制天空盒
    m_SkyboxEffect.SetViewMatrix(camera.GetViewMatrixXM());
    m_SkyboxEffect.SetProjMatrix(camera.GetProjMatrixXM());
    m_SkyboxEffect.SetRenderDefault();
    m_Skybox.Draw(m_pd3dImmediateContext.Get(), m_SkyboxEffect);
}

使用几何着色器的动态天空盒

这部分内容并没有融入到项目中,因此只是简单地提及一下。建议作为练习题尝试。

在上面的内容中,我们对一个场景绘制了6次,从而生成动态天空盒。为了减少绘制调用,这里可以使用几何着色器来使得只需要进行1次绘制调用就可以生成整个动态天空盒。

这里我们可以直接创建一个Texture2DCube,它内部会创建一个渲染目标视图,允许访问里面的6张纹理。创建RTV的过程大致为:

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()));

紧接着,就是要创建一个Depth2DArray,元素个数为6,让每个面对应一个子深度缓冲区,同样它内部会创建一个包含完整资源的深度/模板视图。创建DSV的过程大致为:

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();
	}
}



上面的代码是经过魔改的,至于与它相关的示例项目CubeMapGS11现在在网上存留了这个版本CubeMapGS11

总的来说在绘制物体不是很多的情况可以获得显著的性能提升吧,更复杂的场景就没有测试了。

这种方法有两点不那么吸引人的原因:

  1. 它使用几何着色器来输出大量的数据。不过放眼现在的显卡应该不会损失多大的性能。
  2. 在一个典型的场景中,一个三角形不会出现在两个或以上的立方体表面,不管怎样,这5次绘制都没法通过裁剪,显得十分浪费。虽然在我们的项目中,一开始的做法也是将整个场景绘制到天空盒的一面,但是我们还可以使用视锥体裁剪技术来剔除掉那些不在视锥体的物体。使用几何着色器的方法不能进行提前的裁剪。

但还有一种情况它的表现还算不俗。假如你现在有一个动态天空系统,这些云层会移动,并且颜色随着时间变化。因为天空正在实时变化,我们不能使用预先烘焙的天空盒纹理来进行反射/折射。使用几何着色器绘制天空盒的方法在性能上不会损失太大。

模型的折射

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完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

posted @ 2018-11-03 15:27  X_Jun  阅读(4318)  评论(1编辑  收藏  举报
levels of contents