DX 深度模板缓冲区

前言

​ 本篇为DX12个人学习笔记,会介绍深度缓冲区、深度冲突、模板缓冲区、如何使用模板缓冲区

深度缓冲区

  • 定义:深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色)一样,在每个片段中储存深度值信息,且和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的

  • 深度测试:若启用深度测试(Depth Testing),会将一个片段的深度值与深度缓冲的内容进行对比,若深度测试通过,深度缓冲将会更新为新的深度值。若深度测试失败,片段将会被丢弃

  • 执行阶段:深度缓冲在运行在PS中(模板测试(Stencil Testing)运行之后)且对象在屏幕空间中

Early Depth Testing(early-z)

  • 定义:现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段
  • 为什么需要?片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们
  • 限制:当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。D3D不能提前知晓深度值

深度冲突(Z-fighting)

  • 定义当两个平面非常紧密地平行排列在一起时,会导致深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺谁该处于顶端,当物体在远处时效果会更明显

    如下图,箱子被放置在地板的同一高度上,这也就意味着箱子的底面和地板是共面的,可以看到箱子的底部不断地在箱子底面与地板之间切换,形成了花纹
    image-20230330113953669

  • 深度冲突不能够被完全避免,但一些技巧有助于减轻/避免深度冲突

    1. 不要把多个物体摆得太靠近——在两个物体之间设置一个玩家无法注意到的offset

    2. 尽可能将近平面设置远一些.不过,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验来决定最适合场景的近平面距离

      如下图所示,当近平面靠近相机时视锥体只有靠近相机的那部分精度是很高的,因此我们可以让近平面远离相机,使得视锥体整体都有较高的精度
      img

    3. 使用更高精度的深度缓冲.大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度,但也会牺牲一些性能

模板(stencil)缓冲区

  • 什么是模板缓冲区?

    我们知道深度缓冲区存储每个像素的深度值z,而模板缓冲区是一个额外的buffer,它的分辨率大小和深度缓冲区相同,其中模板缓冲区的一个像素点占用8个bit,而这8bit的作用是控制颜色缓冲区和z缓冲区的渲染,比如在一个像素的模板缓冲区中存放1,表示该像素对应的空间点处于阴影体中.也就是说,模板起到的作用和印刷中所用的模板相同

    注意:模板缓冲区需要搭配深度缓冲区一起工作

  • 为什么需要模板缓冲区?通过使用模板,我们可以实现许许多多的特效

    如下图所示,中间是一面镜子且镜子四周有一些砖块,按理说我们不应看到左图中露出来的一截,因此借助模板缓冲区,即可避免这一错误
    image-20230226143802914

    又或者,若在模板缓冲区中绘制了一个空心矩形,模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形,场景中的片段将会只在片段的模板值为1的时候会被渲染,这就如同mask操作
    img

  • 如何使用模板缓冲区?

    步骤

    1. 填写struct D3D12_DEPTH_STENCIL_DESC来设置模板、深度缓冲区状态
    2. 将D3D12_DEPTH_STENCIL_DESC赋给D3D12_GRAPHICS_PIPELINE_STATE_DESC中的DepthStencilState

深度/模板缓冲区的格式及其资源数据的重置

  • 深度/模板缓冲区的格式:深度/模板缓冲区是一种纹理

    1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:深度缓冲区的格式为一个32位float,模板缓冲区的格式为一个32位UINT,其中8位UINT用于将模板缓冲区映射至范围[0,255],剩余24位不可使用仅用于填充占位
    2. DXGI_FORMAT_D24_UNORM_S8_UINT:深度缓冲区的格式为一个24位的无符号类型,并将其映射至范围[0,1];模板缓冲区的格式为8位UINT,映射至范围[0,255]
  • 重置深度/模板缓冲区的数据:调用ID3D12GraphicsCommandList::ClearDepthStencilView()

    • 定义

      void ClearDepthStencilView(
        [in] D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView,	//待重置的深度/模板缓冲区视图的描述符
        [in] D3D12_CLEAR_FLAGS           ClearFlags, //标志位,指定清理哪个缓冲区.若为D3D12_CLEAR_FLAG_DEPTH清理深度缓冲区,若为D3D12_CLEAR_FLAG_STENCIL清理模板缓冲区
        [in] FLOAT                       Depth,	   //此float值会赋值给深度缓冲区的每个像素,范围为[0,1]
        [in] UINT8                       Stencil,	   //此UINT值会赋值给模板缓冲区的每个像素,范围为[0,255]
        [in] UINT                        NumRects,   //数组pRects中的数量
        [in] const D3D12_RECT            *pRects	   //D3D12_RECT类型的数组,指定深度/模板缓冲区中要重置的区域,nullptr表示整个区域
      );
      
      typedef enum D3D12_CLEAR_FLAGS {
        D3D12_CLEAR_FLAG_DEPTH = 0x1,
        D3D12_CLEAR_FLAG_STENCIL = 0x2
      } ;
      
    • 示例

      //每一帧都需要调用
      mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
      

模板测试

  • 定义:允许我们在渲染片段时将模板缓冲设定为一个特定的值.通过在渲染时修改模板缓冲的内容,我们写入模板缓冲。在接下来的渲染迭代中,我们可以读取这些值,来决定丢弃/保留某个片段

  • 执行阶段:输出合并阶段。当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行

  • 大致步骤

    1. 启用模板缓冲的写入

    2. 渲染物体,更新模板缓冲的内容

    3. 禁用模板缓冲的写入

    4. 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段

  • 处理过程

    if( StencilRef & StencilReadMask \(\unlhd\) Value & StencilReadMask )
    accept pixel
    else
    reject pixel

    1. 定义的模板参考值StencilRef 与 定义的掩码值StencilReadMask进行按位AND(&)运算,此计算结果称为左运算数
    2. 正在接受模板测试的特定像素在模板缓冲区中的对应值Value 和 程序内定义的掩码值StencilReadMask进行按位AND(&)运算,此计算结果称为右运算数
    3. 采样选定的比较函数\(\unlhd\)对左运算数、右运算数进行对比,若结果位true则写入,否则禁止该像素向后台缓冲区的写操作
  • 比较函数

    该比较函数也可用于深度测试中

    • 定义

      以下我们称\(\unlhd\)运算符为比较运算符。默认为D3D12_COMPARISON_FUNC_LESS——丢弃深度值≥当前模板缓冲区的片段值

      //以下比较运算符成立则通过测试
      typedef enum D3D12_COMPARISON_FUNC {
        D3D12_COMPARISON_FUNC_NEVER = 1,	//只返回false.永远不通过深度测试
        D3D12_COMPARISON_FUNC_LESS = 2,	//"<".片段深度值小于缓冲区的深度值
        D3D12_COMPARISON_FUNC_EQUAL = 3,	//"==".片段深度值等于缓冲区的深度值
        D3D12_COMPARISON_FUNC_LESS_EQUAL = 4,	//"≤".片段深度值小于等于缓冲区的深度值
        D3D12_COMPARISON_FUNC_GREATER = 5,	//">".片段深度值大于缓冲区的深度值
        D3D12_COMPARISON_FUNC_NOT_EQUAL = 6,	//"!=".片段深度值不等于缓冲区的深度值
        D3D12_COMPARISON_FUNC_GREATER_EQUAL = 7,	//"≥".片段深度值大于等于缓冲区的深度值
        D3D12_COMPARISON_FUNC_ALWAYS = 8	//只返回true.永远通过深度测试
      } ;
      

创建绑定深度/模板状态

  • 创建:通过填写struct D3D12_DEPTH_STENCIL_DESC来描述深度/模板状态

  • 定义

    typedef struct D3D12_DEPTH_STENCIL_DESC {
      BOOL                       DepthEnable;	//是否开启深度测试,TRUE则开启.默认值为TRUE
      D3D12_DEPTH_WRITE_MASK     DepthWriteMask;	//是否禁止深度写入,若为D3D12_DEPTH_WRITE_MASK_ZERO则不可进行深度写入,若为D3D12_DEPTH_WRITE_MASK_ALL则可以.默认值为D3D12_DEPTH_WRITE_MASK_ALL
      D3D12_COMPARISON_FUNC      DepthFunc;		//指定比较函数。指定为比较函数中的一员
      BOOL                       StencilEnable;	//是否开启模板测试,若为true则开启.默认值为FALSE
      UINT8                      StencilReadMask;	//定义的掩码值,用于模板测试
      UINT8                      StencilWriteMask;	//写掩码值,用于屏蔽对应位的写入操作
      D3D12_DEPTH_STENCILOP_DESC FrontFace;		//指示根据测试和深度测试的结果,对正面朝向的三角形进行什么模板运算
      D3D12_DEPTH_STENCILOP_DESC BackFace;		//指示根据测试和深度测试的结果,对背面朝向的三角形进行什么模板运算
    } D3D12_DEPTH_STENCIL_DESC;
    
    //是否禁止深度写入
    typedef enum D3D12_DEPTH_WRITE_MASK {
      D3D12_DEPTH_WRITE_MASK_ZERO = 0,
      D3D12_DEPTH_WRITE_MASK_ALL = 1
    } ;
    
    //用于模板测试的掩码值的默认值——不会屏蔽任何一位模板值
    #define D3D12_DEFAULT_STENCIL_READ_MASK (0xff)
    
    //写掩码值的默认值——不会屏蔽任何一位模板值
    #define D3D12_DEFAULT_STENCIL_WRITE_MASK (0xff)
    
    //描述基于模板测试的结果被执行的模板运算符
    typedef struct D3D12_DEPTH_STENCILOP_DESC {
      D3D12_STENCIL_OP      StencilFailOp;	//描述当片元在模板测试失败时,应如何更新模板缓冲区
      D3D12_STENCIL_OP      StencilDepthFailOp;	//描述当片元通过模板测试,但在深度测试失败时,应如何更新模板缓冲区
      D3D12_STENCIL_OP      StencilPassOp;	//描述当片元通过模板测试、深度测试时,应如何更新模板缓冲区
      D3D12_COMPARISON_FUNC StencilFunc;	//模板测试中所用的比较函数
    } D3D12_DEPTH_STENCILOP_DESC;
    
    //指定在深度/模板测试期间被执行的模板运算符
    typedef enum D3D12_STENCIL_OP {
      D3D12_STENCIL_OP_KEEP = 1,	//不修改模板缓冲区
      D3D12_STENCIL_OP_ZERO = 2,	//将模板缓冲区中的元素置为0
      D3D12_STENCIL_OP_REPLACE = 3,	//将模板缓冲区中的元素替换为用于模板测试的模板参考值(StencilRef).只有当深度/模板缓冲区状态块绑定至管线时,才能设定该值
      D3D12_STENCIL_OP_INCR_SAT = 4,//对模板缓冲区中的元素进行递增.若超出范围会进行钳制
      D3D12_STENCIL_OP_DECR_SAT = 5,//对模板缓冲区中的元素进行递减.若超出范围会进行钳制
      D3D12_STENCIL_OP_INVERT = 6, 	//对模板缓冲区中的元素按二进制位进行反转
      D3D12_STENCIL_OP_INCR = 7,	//对模板缓冲区中的元素进行递增.若超出范围会回到最小值0
      D3D12_STENCIL_OP_DECR = 8		//对模板缓冲区中的元素进行递增.若超出范围会回到最大值255
    } ;
    
  • 绑定:填写完D3D12_DEPTH_STENCIL_DESC后,即可将其赋值给D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState

  • 设置模板参考值(StencilRef):通过调用ID3D12GraphicsCommandList::OMSetStencilRef()

    • 定义

      void OMSetStencilRef(
        [in] UINT StencilRef
      );
      

示例

实现平面镜

​ 为了简易性,此处仅完成平面上的镜面

  • 实现平面镜需要解决两个问题
    1. 平面反射物体的原理
    2. 要将镜像显示在镜子中,必须对表面的镜面部分做标记

实现镜像

​ 存在的问题:下图描述了镜像的渲染过程——反射至镜面的背面,但这存在一个问题——现实中只有透过镜子才能看到镜像,但此处的镜像却是另一个实物,若没有东西遮挡他,我们同样可以看到他,这并不是我们想要的效果

image-20230226172106463

​ 解决方法:模板缓冲区来制止超出镜面范围的镜像绘制操作

​ 重点

  1. 将除镜子外的物体按正常流程渲染至后台缓冲区中

  2. 清除模板缓冲区,将其整体置零

  3. 仅将镜面渲染至模板缓冲区

    过程如下:将模板测试设为每次都通过(D3D12_COMPARISON_FUNC_ALWAYS),且在通过测试时以1(模板参考值)来更新(D3D12_STENCIL_OP_REPLACE)模板缓冲区元素。若深度测试失败(如物体挡住部分镜子),则采用D3D12_STENCIL_OP_KEEP(不修改模板缓冲区),使得模板缓冲区对应的像素保持不变。由于仅仅向模板缓冲区绘制镜面,因此只有镜面对应的像素为1,而其他像素都为0
    image-20230226174638177

    采用如下设置来禁止其他颜色数据写入至后台缓冲区

    D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;	//禁止写入后台缓冲区中的任何颜色通道
    D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;		//禁止深度写入
    
  4. 将镜像渲染至后台缓冲区、模板缓冲区

    ​ 只有通过模板测试的像素才能渲染至后台缓冲区,如此只有在镜面中的部分物体才会被渲染

  5. 以正常方式结合blend将镜面渲染至后台缓冲区

    ​ 由于要呈现镜像效果,我们需要运用blend来渲染镜面。假设不使用blend,会导致物体的z值小于镜面的z值,镜像被镜子挡住

  • 定义镜面材质:将漫反射alpha设为0.3,,再设置blend状态——\(C = 0.3 · C_{src} + 0.7 · C_{dst}\)

    auto icemirror = std::make_unique<Material>();
    icemirror->Name = "icemirror";
    icemirror->MatCBIndex = 2;
    icemirror->DiffuseSrvHeapIndex = 2;
    icemirror->DiffuseAlbedo = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.3f);
    icemirror->FresnelR0 = XMFLOAT3(0.1f, 0.1f, 0.1f);
    icemirror->Roughness = 0.5f;
    

定义镜像的深度/模板状态

​ 为了实现以上效果,需要用到两个PSO,一个用于在绘制镜面时标记模板缓冲区内镜面部分的像素,而另一个用于绘制镜面的镜像

  1. 用于标记模板缓冲区内镜面部分的像素的PSO

    CD3DX12_BLEND_DESC mirrorBlendState(D3D12_DEFAULT);
    mirrorBlendState.RenderTarget[0].RenderTargetWriteMask = 0;	//禁止对渲染目标进行写操作
    
    //定义深度模板状态描述符
    D3D12_DEPTH_STENCIL_DESC mirrorDSS;
    mirrorDSS.DepthEnable = true;	//开启深度测试
    mirrorDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;	//禁止深度写入
    mirrorDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;	//比较运算符为小于
    mirrorDSS.StencilEnable = true;	//开启模板测试
    mirrorDSS.StencilReadMask = 0xff;	//模板参考值
    mirrorDSS.StencilWriteMask = 0xff;	//模板全部可写
    
    //正面朝向的多边形
    mirrorDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;	//模板测试失败时,不修改模板缓冲区
    mirrorDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;	//通过模板测试,但在深度测试失败时,不修改模板缓冲区
    mirrorDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;	//通过模板测试、深度测试时,将模板缓冲区中的元素替换为用于模板测试的模板参考值
    mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;	//比较函数只返回TRUE
    
    //不渲染背面朝向的多边形
    mirrorDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
    mirrorDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
    mirrorDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
    mirrorDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
    
    D3D12_GRAPHICS_PIPELINE_STATE_DESC markMirrorsPsoDesc = opaquePsoDesc;
    markMirrorsPsoDesc.BlendState = mirrorBlendState;
    markMirrorsPsoDesc.DepthStencilState = mirrorDSS;
    ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&markMirrorsPsoDesc, IID_PPV_ARGS(&mPSOs["markStencilMirrors"])));
    
  2. 用于绘制镜面的镜像的PSO

    D3D12_DEPTH_STENCIL_DESC reflectionsDSS;
    reflectionsDSS.DepthEnable = true;
    reflectionsDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;	//开启深度写入
    reflectionsDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
    reflectionsDSS.StencilEnable = true;
    reflectionsDSS.StencilReadMask = 0xff;
    reflectionsDSS.StencilWriteMask = 0xff;
    
    reflectionsDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
    reflectionsDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
    reflectionsDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;	//通过模板测试、深度测试时,不修改模板缓冲区
    reflectionsDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;	//比较运算符为"=="
    
    reflectionsDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
    reflectionsDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
    reflectionsDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
    reflectionsDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
    
    D3D12_GRAPHICS_PIPELINE_STATE_DESC drawReflectionsPsoDesc = opaquePsoDesc;
    drawReflectionsPsoDesc.DepthStencilState = reflectionsDSS;
    drawReflectionsPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
    drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;
    ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawReflectionsPsoDesc, IID_PPV_ARGS(&mPSOs["drawStencilReflections"])));
    

绘制场景

// 绘制不透明的物体
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);

// 将模板缓冲区中可见的镜面像素标记为1
mCommandList->OMSetStencilRef(1);
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);

// 只绘制镜子范围内的镜像
//使用两个单独的渲染过程常量缓冲区,一个存储物体镜像,另一个存储光照镜像
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);

// 恢复主渲染过程常量数据和模板参考值
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->OMSetStencilRef(0);

// 绘制透明的镜面
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);

在绘制镜像时,镜像也需要有光照效果,因此使用两个单独的渲染过程常量缓冲区,一个存储物体镜像,另一个存储光照镜像

PassConstants mMainPassCB;
PassConstants mReflectedPassCB;

void StencilApp::UpdateReflectedPassCB(const GameTimer& gt)
{
    mReflectedPassCB = mMainPassCB;

    XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
    XMMATRIX R = XMMatrixReflect(mirrorPlane);

    // 光照镜像
    for(int i = 0; i < 3; ++i)
    {
        XMVECTOR lightDir = XMLoadFloat3(&mMainPassCB.Lights[i].Direction);
        XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
        XMStoreFloat3(&mReflectedPassCB.Lights[i].Direction, reflectedLightDir);
    }

    auto currPassCB = mCurrFrameResource->PassCB.get();
    currPassCB->CopyData(1, mReflectedPassCB);
}

镜像的绕序

​ 当三角形反射到镜像时,由于该绕序不会发生改变,从而使得它的平面法线方向也没有变化,因此这会造成它的法线为内向法线,而原本物体为外向法线。为了纠正这个问题,需告知D3D将逆时针绕序的三角形看作是正面朝向,顺时针绕序的三角形看作背面朝向 (原本规定的是若我们看到的是三角形的正面,则此三角形为正面朝向且顶点顺序为顺时针;看到的是三角形反面,则此三角形为背面朝向且定点顺序为逆时针)

drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;

image-20230227125018206

实现平面阴影

​ 思路:借助几何建模的方式找到物体经光照投向平面的阴影

平行光阴影

​ 给定方向为L的平行光源,以\(r(t) = p + tL\)表示经过顶点p的光线,光线r与阴影平面(n,d)的交点为s,使用这些光线和阴影平面的交点即可定义几何体所的阴影形状,投影公式:\(r(t_s) = p - \frac{n·p + d}{n·L}L\)
image-20230227132656866

​ 投影公式推导:此处实则是求解投影平面与光线求交
image-20230227133720190

  1. 表示射线:\(p = p_0 + tu\),其中\(p_0\)为该射线的起点,\(u\)为与该射线平行的向量,\(t\)用于获得该射线上任意一点
  2. 表示平面:\(n · (p - p_0) = n · p - n·p_0 = n · p + d = 0\),其中$d = -n · p_0 \(.将平面视为一个xyz轴上的长度都是无限长的平面,其中\)n\(为平面的法向量,\)p_0,p\(为平面内一点,当且仅当\)(p - p_0)\(正交于\)n$时才可表示此平面
  3. 解得射线与平面求交的t:\(n·p(t) + d = 0 \rightarrow n(p_0 + tu) +d = 0 \rightarrow n · p_0 + tn·u + d = 0 \rightarrow t = \frac{-n·p_0 - d}{n·u}\)
  4. 代入射线公式:\(p = p_0 + \frac{-n·p_0 - d}{n·u}u \rightarrow p_0 - \frac{n·p_0 + d}{n·u}u\)

​ 将投影公式转换为矩阵:\(r(t_s) = [p_x, p_y, p_z, 1] \left[\begin {array} {c} n·L - L_x n_x & -L_y n_x & -L_z n_x & 0 \\ -L_x n_y & n·L - L_y n_y & -L_z n_y & 0 \\ -L_x n_z & -L_y n_z & n · L - L_zn_z & 0 \\ -L_x d & -L_y d & -L_z d & n·L \end {array} \right]\)。该矩阵被称为方向光阴影矩阵

​ 该矩阵有个注意点:为了引用该阴影矩阵,我们应当将其进行MVP变换,但在进行世界变换后,由于透视除法并未执行,因此几何体的阴影还未进行投射,这边出现了一个问题——若\(n·L < 0\),则投影矩阵w坐标为负值,但在将投影矩阵乘以透视投影矩阵时,计算过程中会使得w的值等于z的值,造成w的坐标为负,而w坐标为负说明该点位于视锥体外部,会对其进行裁剪,结果便是这段阴影无法显示

解决:用指向无穷远的光源的方向向量\(-L\)取代L,采用适当的t值也可以得到与之前的效果,如此即可使得\(n·L > 0\)

image-20230227141053224

点光阴影

下图描述的是点光源L所投射出的物体阴影

image-20230227150945866

​ 其中,点光源投射的光线公式:\(r(t) = p + t(p-L)\),投影公式:\(r(t_s) = p - \frac{n·p + d}{n·(p-L)}(p-L)\),投影矩阵为:$s_{point} = \left[ \begin {array} {c} n·L + d - L_x n_x & -L_y n_x & -L_z n_x & -n_x \
-L_x n_y & n·L + d -L_y n_y & -L_z n_y & -n_y \
-L_x n_z & -L_y n_z & n·L + d - L_z n_z & -n_z \
-L_x d & -L_y d & -L_z d & n·L \end {array} \right] $

通用阴影矩阵

​ 通用阴影矩阵的表示:\(S = \left[ \begin {array} {c} n·L + dL_w - L_x n_x & -L_y n_x & -L_z n_x & -L_w n_x \\ -L_xn_y & n·L + d L_w - L_y n_y & -L_z n_y & -L_w n_y \\ -L_x n_z & -L_y n_z & n·L + d L_w - L_z n_z & -L_w n_z \\ -L_x d & -L_y d & -L_z d & n·L \end {array} \right]\)

​ 对于通用阴影矩阵有以下两个规定:

  1. \(L_w = 0\),L为指向方向光光源的向量
  2. \(L_w = 1\),L为点光源的位置

​ DirectX数学库提供了对应函数用于计算阴影矩阵:

inline XMMATRIX XM_CALLCONV XMMatrixShadow(
	FXMVECTOR ShadowPlane,
	FXMVECTOR LightPosition	//w = 0为方向光,w = 1为点光源
);

防止双重混合

​ 问题:对物体进行投射形成阴影时,经常会出现有两个以上的平面阴影三角形相互重叠,此时使用透明渲染来渲染阴影,会使得这些三角形blend多次,最终结果会更暗

如下图,左边重叠后的透明渲染,右边是正确结果
image-20230227153958317

​ 解决方案:模板缓冲区

  1. 将模板缓冲区中的阴影范围的像素清为0
  2. 设置模板测试,仅接受模板缓冲区中元素为0的像素。若通过模板测试,将相应像素点的值赋为1,使得若覆写已通过的区域,模板测试会失效

实现阴影

​ 将阴影视为具有50%透明度的黑色材质

//材质
auto shadowMat = std::make_unique<Material>();
shadowMat->Name = "shadowMat";
shadowMat->MatCBIndex = 4;
shadowMat->DiffuseSrvHeapIndex = 3;
shadowMat->DiffuseAlbedo = XMFLOAT4(0.0f, 0.0f, 0.0f, 0.5f);
shadowMat->FresnelR0 = XMFLOAT3(0.001f, 0.001f, 0.001f);
shadowMat->Roughness = 0.0f;

//防止双重混合
D3D12_DEPTH_STENCIL_DESC shadowDSS;
shadowDSS.DepthEnable = true;
shadowDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
shadowDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
shadowDSS.StencilEnable = true;
shadowDSS.StencilReadMask = 0xff;
shadowDSS.StencilWriteMask = 0xff;

shadowDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;

// 不渲染背面朝向的三角形
shadowDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;

D3D12_GRAPHICS_PIPELINE_STATE_DESC shadowPsoDesc = transparentPsoDesc;
shadowPsoDesc.DepthStencilState = shadowDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&shadowPsoDesc, IID_PPV_ARGS(&mPSOs["shadow"])));

//绘制阴影
mCommandList->OMSetStencilRef(0);
mCommandList->SetPipelineState(mPSOs["shadow"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);

//更新阴影的矩阵
XMVECTOR shadowPlane = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // xz plane
XMVECTOR toMainLight = -XMLoadFloat3(&mMainPassCB.Lights[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f);	//将投影网格沿y轴做少量offset,来防止发生深度冲突,使得阴影略高于地面
XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S * shadowOffsetY);

reference

Directx12 3D 游戏开发实战

https://github.com/QianMo/Game-Programmer-Study-Notes

[深度测试 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/01 Depth testing/#_1)

posted @ 2023-02-27 16:20  爱莉希雅  阅读(427)  评论(0)    收藏  举报