d3d12龙书学习之MiniEngine的最小化实现(十四) 龙书第18章 skybox天空盒、动态天空盒

文章目录

  • 项目准备
  • 什么是天空盒
  • 静态天空盒实现
  • 镜面反射实现
  • 动态天空盒实现
    • 实现一个天空盒缓冲区
  • 总结

项目准备

因为天空盒用的是柱子那个场景。后续做裁剪时修复了摄像机的bug,把15和17章的代码整合在一起。
这样保证我们新的项目中底层代码始终是最新的。

项目准备后的效果图:

什么是天空盒

我们玩游戏,可以看到很远的景色。
那这些很远的景色真的是真实绘制的吗?如果是的话,对于那么遥远的物体绘制,需要调大摄像机的深度,而由此会带来非常大的效率问题。因为深度越大,视野内的物体就越多。

天空盒就是做一个假的远方纹理,当我们看的时候,看的其实就是一张贴图,这样摄像机的深度可以用比较低的数值。

对于玩家在一张地图上游玩,可以提供静态的天空盒,也可以根据坐标范围,绘制不同的天空盒。

静态天空盒实现

静态天空盒的实现非常的简单。

  1. 维护天空盒的纹理(ddsloader支持直接通过文件载入天空盒)
  2. 生成一个巨大的球(半径就是我们的摄像机深度)
  3. 对球上的每个顶点计算出相应的纹理(从天空盒的纹理对象中直接通过api获取)
  4. 球的中心点始终位于摄像机的位置(这样永远也无法到达天空盒边缘)
  5. 绘制天空盒的PSO,设置深度比较为 less_equal
  6. 绘制天空盒的PSO,设置背面不剔除

镜面反射实现

对于柱子上的球,要做的是镜面反射(在ps shader中实现)

  1. 根据当前顶点的坐标、法向量、摄像机位置,计算出反射光线
  2. 根据反射光线,从天空盒中取出纹理
  3. 采用菲涅尔方程计算镜面反射后的像素

总体结果如下:

动态天空盒实现

动态天空盒,就是我们要实时的绘制在某个点的6方向的图,然后存成一个Texture2DCube的纹理结构。

实现一个天空盒缓冲区

因为MiniEngine中并不没有这个结构的类,所以这里新添加一个类,用于支持天空盒的纹理缓冲
仿照ColorBuff实现ColorCubeBuff

核心代码

void ColorCubeBuffer::CreateDerivedViews(ID3D12Device* Device, DXGI_FORMAT Format, uint32_t ArraySize, uint32_t NumMips)
{
    ID3D12Resource* Resource = m_pResource.Get();

    // Create the shader resource view
    D3D12_SHADER_RESOURCE_VIEW_DESC SRVDesc = {};
    SRVDesc.Format = Format;
    SRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    SRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;			// 把资源当作立方体纹理,生成SRV
    SRVDesc.TextureCube.MipLevels = 1;

    if (m_SRVHandle.ptr == D3D12_GPU_VIRTUAL_ADDRESS_UNKNOWN)
        m_SRVHandle = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    
    Device->CreateShaderResourceView(Resource, &SRVDesc, m_SRVHandle);

    // Create the render target view
    for (int i = 0; i < 6; ++i)
    {
        D3D12_RENDER_TARGET_VIEW_DESC RTVDesc = {};
        RTVDesc.Format = Format;
        RTVDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DARRAY;		// 对于RTV来说,该资源解释为6个纹理数组。每个纹理数组1张图
        RTVDesc.Texture2DArray.MipSlice = 0;
        RTVDesc.Texture2DArray.PlaneSlice = 0;
        RTVDesc.Texture2DArray.FirstArraySlice = i;		// 数组的切片起点,这样6个数组使用的就是资源中不同的位置了
        RTVDesc.Texture2DArray.ArraySize = 1;

        if (m_RTVHandle[i].ptr == D3D12_GPU_VIRTUAL_ADDRESS_UNKNOWN)
            m_RTVHandle[i] = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

        Device->CreateRenderTargetView(Resource, &RTVDesc, m_RTVHandle[i]);
    }
}

使用方式跟ColorBuff一样。

动态天空盒实现步骤

  1. 选定一个摄像机的位置,计算出6个方向的转换矩阵
  2. 正式绘制开始前,挪到摄像机位置,对着6个方向绘制6次,存储成cube纹理
  3. 回到原先的摄像机位置,开始绘制场景
  4. 在绘制需要动态反射环境的物体,把原天空盒纹理替换成我们动态生成的纹理。

效果:

总结

我们可以发现,在给shader传入描述符的时候,天空盒纹理和普通纹理是作为一个数组的整体传进去的。
占据的是t1-t4
而这个t4纹理就是天空盒纹理,而我们要使用的是另外一种类型,这里直接指定就可以。

// 占据t1-t4
Texture2D gDiffuseMap[3] : register(t1);
// gDiffuseMap占据的是t1-t4,而天空盒纹理本身是放在t4位置,这里转成cube类型
TextureCube gCubeMap : register(t4);

这就像以前说过的,srv的table之类的,占据的是线性的结构。上边这种操作就相当于直接把天空盒纹理地址转成了我们需要的对象。

而对于这个cube格式,我认为就是6个texture2D直接组成的,只不过换成这个类型,可以直接使用对应的api

也是依据这个理解,可以看到代码中直接切天空盒纹理写法如下:

	// 设置全部的纹理资源
    gfxContext.SetDynamicDescriptors(3, 0, 4, &m_srvs[0]);

    gfxContext.SetPipelineState(m_mapPSO[E_EPT_DEFAULT]);
    drawRenderItems(gfxContext, m_vecRenderItems[(int)RenderLayer::Opaque]);

    // 渲染中间的水晶球,输入纹理是上边动态生成的天空盒
    // 设置动态的天空盒资源
    gfxContext.SetDynamicDescriptors(3, 3, 1, &Graphics::g_SceneCubeBuff.GetSRV());
    drawRenderItems(gfxContext, m_vecRenderItems[(int)RenderLayer::OpaqueDynamicReflectors]);

    // 绘制天空盒
     gfxContext.SetPipelineState(m_mapPSO[E_EPT_SKY]);
     // 设置原始的天空盒资源
     gfxContext.SetDynamicDescriptors(3, 3, 1, &m_srvs[3]);
     drawRenderItems(gfxContext, m_vecRenderItems[(int)RenderLayer::Sky]);

天空盒是个很实用的技术,可以让玩家“看到”很远的景色。
游戏开发中会有很多的Trick,仅仅牺牲一点点用户几乎无法觉察的体验,就可以极大地提升游戏的性能。

你可能感兴趣的:(DirectX12,龙书学习)