dx12 龙书第十一章学习笔记 -- 模板

发布于:2022-12-21 ⋅ 阅读:(227) ⋅ 点赞:(0)

模板缓冲区是一种"离屏"(off-screen)缓冲区,我们可以用它来实现一些特殊的效果。模板缓冲区、后台缓冲区以及深度缓冲区都有着相同的分辨率,所以三者相同位置上的像素就能一一对应起来。

指定一个模板缓冲区时,要将它与一个深度缓冲区配合使用

模板缓冲区如同印刷过程中所用的模板一样,我们可以用它来阻止特定的像素片段渲染至后台缓冲区中。

镜像效果应该只存在于镜子以内

要设置模板缓冲区(以及深度缓冲区)状态,就需填写D3D12_DEPTH_STENCIL_DESC结构体实例,并将其赋予PSO的D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState字段。

1.深度/模板缓冲区的格式及其资源数据的清理

深度/模板缓冲区也是一种纹理,所以必须用下列特定的数据格式来创建它,深度缓冲可用的格式:

  • DXGI_FORMAT_D32_FLOAT_S8X24_UINT:用一个32位浮点数来指定深度缓冲区,并以另一个32位无符号整数来指定模板缓冲区。其中,无符号整数里的8位用于将模板缓冲区映射到范围[0,255],另外24位不可用,仅作填充占位。
  • DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号24位深度缓冲区,将其映射到[0,1]内。另外8位用于令模板缓冲区映射至范围[0,255]
DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilDesc.Format = mDepthStencilFormat;

我们可以在绘制每一帧画面之初,用以下方法来重置模板缓冲区中的局部数据(用于清理深度缓冲区):

void ID3D12GraphicsCommanList::ClearDepthStencilView(
    D3D12_CPU_DESCRPITOR_HANDLE DepthStencilView,
    D3D12_CLEAR_FLAGS ClearFlags,
    FLOAT Depth,
    UINT8 Stencil,
    UINT NumRects,
    const D3D12_RECT *pRects
);

// DepthStencilView:
待清理的深度/模板缓冲区视图的描述符

// ClearFlags:
D3D12_CLEAR_FLAG_DEPTH:仅清理深度缓冲区
D3D12_CLEAR_FLAG_STENCIL:仅清理模板缓冲区
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL:同时清理两种缓冲区

// Depth:
将此浮点值设置到深度缓冲区的每一个像素 0<=x<=1

// Stencil:
将此整数值设置到模板缓冲区的每一个像素 0<=n<=255

// NumRects:
数组pRects中所指引的矩形数量

// pRects:
一个D3D12_RECT类型数组,它标定了一系列深度模板缓冲区内要清理的区域。若指定为nullptr则清理整个深度模板缓冲区

我们在应用程序的每一帧都已经调用此方法:

mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);

2.模板测试

我们可以通过模板缓冲区来阻止对后台缓冲区特定区域的绘制行为。这项操作其实是由模板测试来决定的,它的处理过程如下:

if(StencilRef & StencilReadMask ⊴ Value & tencilReadMask)
    accept pixel
else
    reject pixel

模板测试会随着像素的光栅化过程而执行(即在输出合并阶段进行)。若模板功能开启,则需要进行下面两处计算:

  • 左运算数(left-hand-side, LHS)由程序中定义的模板参考值(stencil reference value)StencilRef与程序内定义的掩码值(masking value)StencilReadMask通过AND(与)运算加以确定
  • 右运算数(RHS)由正在接收模板测试的特定像素位于模板缓冲区中的对应值Value与程序中定义的掩码值StencilReadMask经过AND运算加以确定

注意:LHS和RHS中的掩码值StencilReadMask都是同一个值。接下来,模板测试用程序中所选定的比较函数⊴对LHS和RHS进行比对,从而获得bool类型的返回值。如果测试结果为true,则将当前受检测的像素写入后台缓冲区(假设此像素已通过深度测试);如果为false,则禁止此像素向后台缓冲区进行写操作当然,如果一个像素因模板模板测试失败而被丢弃,它的相关数据也不会被写入深度缓冲区

运算符 ⊴D3D12_COMPARISON_FUNC枚举类型所定义的比较函数之一:

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
} D3D12_COMPARISON_FUNC;

3.描述深度/模板状态

填写D3D12_DEPTH_STENCIL_DESC实例:

typedef struct D3D12_DEPTH_STENCIL_DESC {
    BOOL DepthEnable; // 默认值:true
    D3D12_DEPTH_WRITE_MASK DepthWriteMask; // 默认值:D3D12_DEPTH_WRITE_MASK_ALL
    D3D12_COMPARISON_FUNC DepthFunc; // 默认值:D3D12_COMPARISON_LESS

    BOOL StencilEnable; // 默认值:false
    UINT8 StencilReadMask; // 默认值:0xff,即D3D12_DEFAULT_STENCIL_WRITE_MASK
    UINT8 StencilWriteMask; // 默认值:0xff,即D3D12_DEFAULT_STENCIL_WRITE_MASK
    D3D12_DEPTH_STENCILLOP_DESC FrontFace;
    D3D12_DEPTH_STENCILLOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;

①深度信息的相关设置
// DepthEnable:
true:开启深度缓冲,false:禁止
如果深度测试被禁止,那么物体的绘制顺序就十分重要
若深度缓冲被禁止,则深度缓冲区中的元素便不会被更新,DepthWriteMask项也就不会起作用

// DepthWriteMask: -- 控制深度数据的写能力
可设置为D3D12_DEPTH_WRITE_MASK_ZERO或D3D12_DEPTH_WRITE_MASK_ALL,但两者不能共存
如果DepthEnable设置为true,而此参数设置为...Zero,那么会被禁止对深度缓冲区的写操作,但仍可进行深度测试;若讲此项设置为...ALL,则通过深度测试与模板测试的深度数据将被写入深度缓冲区

// DepthFunc:
D3D12_COMPARISON_FUNC枚举类型的成员之一,以此来定义深度测试所用的比较函数
此项一般被设置为D3D12_COMPARISON_FUNC_LESS -- 即,若给定像素片段的深度值小于位于深度缓冲区中对应像素的深度值,则接收该像素(深度值小,离摄像机近)


②模板信息的相关设置
// StencilEnable:
true:开启模板测试,false:禁用

// StencilReadMask:
是模板测试左右两侧都有的掩码值,进行与&运算
若采用该项的默认值,则不会屏蔽任何一位模板值 #define D3D12_DEFAULT_STENCIL_READ_MASK (0xff)

// StencilWriteMask: -- 控制模板数据的写能力
当模板缓冲区被更新时,我们可以通过写掩码来屏蔽特定位的写入操作
比如,如果希望防止前4位数据被改写,便可以将写掩码设置为0x0f
默认配置,则不会屏蔽任何一位模板值 #define D3D12_DEFAULT_STENCIL_WRITE_MASK (0xff)

// FrontFace/BackFace:
填写D3D12_DEPTH_STENCILOP_DESC结构体实例,以指示根据模板测试与深度测试的结果,应对正/背面朝向的三角形要进行何种模板运算

typedef struct D3D12_DEPTH_STENCILOP_DESC {
    D3D12_STENCIL_OP StencilFailOp; // 默认值:D3D12_STENCIL_OP_KEEP
    D3D12_STENCIL_OP StencilDepthFailOp; // 默认值:D3D12_STENCIL_OP_KEEP
    D3D12_STENCIL_OP StencilPassOp; // 默认值:D3D12_STENCIL_OP_KEEP
    D3D12_COMPARISON_FUNC StencilFunc; // 默认值:D3D12_COMPARISON_FUNC_ALWAYS
} D3D12_DEPTH_STENCILOP_DESC;

    // StencilFailOp: 描述当像素片段在模板测试失败时,应该怎样更新模板缓冲区
    // StencilDepthFailOp: 描述了当像素片段通过模板测试,却在深度测试失败时,应如何更新模板缓冲区
    // StencilPassOp: 描述了当像素片段通过模板测试与深度测试时,应该如何更新模板缓冲区
    // StencilFunc: 定义模板测试的比较函数

    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, // 对模板缓冲区中的元素进行递增操作,如果递增值超过最大值(8位模板缓冲区最大值255),则将此模板缓冲区元素限定为最大值
        D3D12_STENCIL_OP_DECR_SAT = 5, // 递减操作.如果递减值小于0,则将该模板缓冲区元素限定为
        D3D12_STENCIL_OP_INVERT = 6, // 对模板缓冲区的元素数据按二进制位进行反转
        D3D12_STENCIL_OP_INCR = 7, // 递增操作,如果超过最大值,则环回至0
        D3D12_STENCIL_OP_DECR = 8 // 递减操作,如果小于0,则环回至最大值      
    } D3D12_STENCIL_OP;

深度测试中位于比较函数左侧的是给定像素片段的深度值,而右侧是深度缓冲区中对应像素的深度值。这与模板测试的左右运算数顺序相反?

因为背面剔除的缘故,当背面剔除时,BackFace的设置无足轻重。但如果要满足特定的图形学算法或透明几何体,那么该设置变得特别重要。

创建和绑定深度/模板状态:

填写完D3D12_DEPTH_STENCIL_DESC实例,我们就可以赋予PSO的对应字段DepthStencilState,借助PSO来进行渲染设置。

另外如何设置模板参考值,该操作可由ID3D12GraphicsCommandList::OMSetStencilRef方法来实现,它以一个无符号整数作为参数:

mCommandList->OMSetStencilRef(1); // 模板值设置为1

4.实现平面镜效果

这里仅完成平面镜的效果,注意镜子是光滑的平面,在现实生活中比如光滑的大理石地板或悬挂在墙上的镜子,但是对于新买的小轿车(车体光滑、具有弧线但不是平面),不在我们的考虑范围之内。

平面镜实现需要解决的两个问题:①根据平面反射物体的相关原理,正确地绘制镜像②我们一定要将镜像显示在镜子中,即必须以某种方式“标记”出表面内地镜面部分[换句话说:现实中的镜像只有在镜子中能够看到]。-- 问题①:解析几何学知识 问题②:模板缓冲区 

①镜像概述:

在绘制镜像时,我们也需要将光源反映到镜子所在的平面内,否则会造成镜像中的光照不够精准。

实现上述②问题的步骤:

1.将地板、墙壁以及骷髅头实物照常渲染到后台缓冲区内(不包括镜子),注意,此步骤不修改模板缓冲区

2.清理模板缓冲区,整体置零

3.仅将镜面渲染到模板缓冲区中。若要禁止其他颜色数据写入到后台缓冲区,可用下列设置所创建的混合状态:

D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;

再通过以下配置来禁止向深度缓冲区的写操作:

D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;

在向模板缓冲区渲染镜面时,我们将模板测试设置为每次都成功(D3D12_COMPARISON_ALWAYS),并且在通过测试时用1(StencilRef模板参考值)来替换(D3D12_STENCIL_OP_REPLACE)模板缓冲区元素。如果深度测试失败(这是可能发生的,因为骷髅头可能遮住部分镜子),则应当采用枚举项D3D12_STENCIL_OP_KEEP,使模板缓冲区的对应像素保持不变(保持0)。由于仅向模板缓冲区绘制了镜面,因此在模板缓冲区内,除了镜面可见部分的对应像素为1,其他像素皆为0。因此更新后的模板缓冲区:

 保证先绘制骷髅头,后将镜面渲染至模板缓冲区的顺序是很重要的。这样一来,深度测试的失败会令镜面的部分像素被骷髅头实物的像素所覆盖,这样镜面被覆盖的部分就不用对模板缓冲区进行二次修改了。因为我们不希望骷髅头实物位于镜面前方的范围内也能显示出镜面内容。

4.现在我们将骷髅头的镜像渲染至后台缓冲区以及模板缓冲区中。前面提到,只有通过模板测试的像素才能渲染到后台缓冲区。我们这样设置:仅当模板缓冲区的值为1时,才能通过模板测试。这可通过令StencilRef为1,且模板运算符为D3D12_COMPARISON_FUNC_EQUAL来实现。如此一来,只有模板缓冲区中元素数值为1的骷髅头镜像部分才能得以渲染。由于只有镜面可见部分所对应的模板缓冲区中元素数值为1,所以仅有这一范围内的骷髅头镜像才能被渲染出来。

5.最后,像往常那样将镜面渲染到后台缓冲区中。但是为了透过镜面观察到骷髅头的镜像,所以通过透明混合技术来渲染镜面。为此,我们只需为镜面定义一个新的材质配置实例:将漫反射alpha通道分量设为0.3,使镜子的不透明度达到30%,再利用上一章中透明混合状态来渲染镜面:

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

上述设置可用下列混合公式来表示:

C=0.3\cdot C_{src}+0.7\cdot C_{dst}

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

为了实现上述算法,我们要用到两个PSO对象。第一个用于在绘制镜面时标记模板缓冲区内镜面部分的像素,第二个用于绘制镜面可见部分内的骷髅头镜像。

//
// 用于标记模板缓冲区中镜面部分的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.PassOp = D3D12_STENCIL_OP_REPLACE;
mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;

// 我们不渲染背部朝向的多边形,因而对这些参数的设置不关心
mirrorDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.PassOp = 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"])));

//
// 用于渲染模板缓冲区中反射镜像的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; // 默认值:false。true则将三角形绕序变反,即若三角形三个顶点绕序方向为逆时针方向,则法线朝向我们
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawReflectionsPsoDesc, IID_PPV_ARGS(&mPSOs["drawStencilReflections"])));

③绘制场景:

// 绘制不透明物体: floors, walls, skull
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]);

// 只绘制镜子范围内的镜像(即仅绘制模板缓冲区中标记为1的像素)
// ※注意:我们必须要使用两个单独的渲染过程缓冲区passCB来完成工作,一个存储物体镜像,另一个保存光照镜像????
// 因为绘制物体镜像时,还涉及光照的镜像,光照在cbPass中定义,所以需要额外的渲染过程常量缓冲区,用以存储光照的镜像
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常量缓冲区设置如下:

PassConstants mMainPassCB;
PassConstants mReflectedPassCB;

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

	XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy平面 -- 镜子的平面
	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);
	}

	// 将光照镜像的渲染过程常量数据存于渲染过程常量缓冲区中索引1的位置
	auto currPassCB = mCurrFrameResource->PassCB.get();
	currPassCB->CopyData(1, mReflectedPassCB);
}

④绕序与镜像:

drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;

因为三角形通过镜面反射后的镜像,其绕序跟原来保持一致,也就是说,法线方向都朝着之前的方向,实际物体的外向法线在镜像中却变成了内向法线。为了纠正这一点,使用以上代码,改变D3D的默认绕序规则,将默认绕序顺时针方向法线朝外改成逆时针方向法线朝外。

⑤这个镜像物体到底怎么画上去的?顶点数据?

//
// 构造渲染项:
//

auto skullRitem = std::make_unique<RenderItem>();
skullRitem->World = MathHelper::Identity4x4();
skullRitem->TexTransform = MathHelper::Identity4x4();
skullRitem->ObjCBIndex = 2;
skullRitem->Mat = mMaterials["skullMat"].get();
skullRitem->Geo = mGeometries["skullGeo"].get();
skullRitem->PrimitiveType = D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
skullRitem->IndexCount = skullRitem->Geo->DrawArgs["skull"].IndexCount;
skullRitem->StartIndexLocation = skullRitem->Geo->DrawArgs["skull"].StartIndexLocation;
skullRitem->BaseVertexLocation = skullRitem->Geo->DrawArgs["skull"].BaseVertexLocation;
mSkullRitem = skullRitem.get();
mRitemLayer[(int)RenderLayer::Opaque].push_back(skullRitem.get());

// Reflected skull will have different world matrix, so it needs to be its own render item
// 镜像与实体之间的区别:世界矩阵不同,所以放在两个渲染项中,其余一样,所以材质、Geo、几何图元等指向同样的目标即可
auto reflectedSkullRitem = std::make_unique<RenderItem>();
*reflectedSkullRitem = *skullRitem;
reflectedSkullRitem->ObjCBIndex = 3;
mReflectedSkullRitem = reflectedSkullRitem.get();
mRitemLayer[(int)RenderLayer::Reflected].push_back(reflectedSkullRitem.get());


//
// OnKeyboardInput:我们可以通过wsad控制骷髅头实体的移动
//

// Update the new world matrix.
XMMATRIX skullRotate = XMMatrixRotationY(0.5f*MathHelper::Pi);
XMMATRIX skullScale = XMMatrixScaling(0.45f, 0.45f, 0.45f);
XMMATRIX skullOffset = XMMatrixTranslation(mSkullTranslation.x, mSkullTranslation.y, mSkullTranslation.z);
XMMATRIX skullWorld = skullRotate*skullScale*skullOffset;
XMStoreFloat4x4(&mSkullRitem->World, skullWorld);

// Update reflection world matrix.
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
XMStoreFloat4x4(&mReflectedSkullRitem->World, skullWorld * R); // 世界矩阵乘以转换矩阵(镜面反射)

∴所以总结来说,绘制镜面内的镜像的实际做法是:在空间中实体关于镜面对称的地方,绘制一个新的实体,当然这个新的实体需要与原来对称的世界矩阵,也需要与原来对称的光照,通过对称的光照在"镜面内"渲染该新几何体【在镜子的视角再渲染一次】。最后通过模板缓冲区“截取”我们可看到的部分。

5.实现平面阴影

平面阴影(planar shadow):投射于平面内的阴影

我们借助几何建模的方式首先找到物体经光照投向平面的阴影,从而渲染出平面阴影效果。这可以通过3D数学知识来轻松实现。接下来,再运用表示阴影的50%透明度黑色材质来渲染阴影区域中的三角形即可。渲染阴影这类工作有可能会引入双重渲染的渲染问题,这一概念在后续小节会讲解。届时,我们将利用模板缓冲区来避免双重混合的发生

①平行光阴影:

由平行光源经物体所投射出的阴影。给定方向L的平行光源,并用r(t)=p+tL来表示途径顶点p的光线。光线r(t)与阴影平面(n,d)的交点为s。【给r(t)的t设置不同的值,就能得到光线上不同的点】以此光源射出的光线照射到物体的每个顶点,用这些映射到平面上的交点集合便可以定义几何体所投射出的阴影状态。它的阴影投影可由下列公式求得:

s=r(t_s)=p-\frac{n\cdot p +d}{n\cdot L}L

射线:r(t)=p+tL 

平面:n\cdot p+d=0

[射线中的p:射线上某点p(已知);平面中的p:平面上任意点p(未知)]

射线与平面的交点(p的投影s):

\\n(p+tL)+d=0\\t=\frac{-n\cdot p-d}{n\cdot L}

再将求得的t反代入射线方程:

s=r(t_s)=p-\frac{n\cdot p+d}{n\cdot L}L

s就是p点在平面上的投影点

s=r(t_s)=p-\frac{n\cdot p +d}{n\cdot L}L可以改写成矩阵的形式

s'=[p_x\ p_y\ p_z\ 1]\begin{bmatrix} n\cdot L-L_xn_x & -L_yn_x & -L_zn_x & 0 \\ -L_xn_y & n\cdot L-L_yn_y & -L_zn_y & 0 \\ -L_xn_z & -L_yn_z & n\cdot L-L_zn_z & 0 \\ -L_xd & -L_yd & -L_zd & n\cdot L \end{bmatrix}

我们称以上的4x4矩阵为方向光阴影矩阵(directional shadow matrix, 或称为平行光阴影矩阵),用{\color{Red} S_{dir}}表示。

为了证明此矩阵与式子是等价的,我们用乘法运算来加以验证。首先可以看出,此矩阵改变了w分量,即s_w=n\cdot L。这样一来,当执行透视除法时,s中的每个坐标都会除以n\cdot L,这便是矩阵能做到式子中除法运算的原因

在完成透视除法之后,我们得到x坐标:

\\ \frac{(n\cdot L)p_x-L_xn_x\cdot p_x-L_xn_y\cdot p_x-L_xn_z\cdot p_x-L_xd}{n\cdot L} \\ =\frac{(n\cdot L)p_x-L_xn_x\cdot p_x-L_xd}{n\cdot L} \\ =p_x-\frac{n_x\cdot p_x+d}{n\cdot L}L_x

注意:比如n_y \ p_z是相互垂直的,相乘为0,所以可以去掉。

yz坐标同理,将p_xL_x分别改成p_yL_y \ p_zL_z。因此我们得到投影点的坐标s'

s'=p-\frac{n\cdot p+d}{n\cdot L}L

s'与之前的s完全相同,因此,我们验证了该光阴影矩阵的正确性。

为了运用阴影矩阵,我们将它与世界矩阵组合在一起。但是,在世界变换之后,由于透视除法还没有执行,因而几何体的阴影还未被投射到阴影平面上。此时便出现了一个问题:若s_w=n\cdot L<0,则w坐标将变为负值。在透视投影的处理过程中,我们一般将z坐标复制到w坐标,若w坐标为负值则表明此点位于视锥体之外而应将其裁剪掉(裁剪操作在透视除法之前的其次空间内执行)。这对于平面阴影来讲是个大问题,因为除了计算透视除法之外,我们还要用w坐标来实现阴影效果。

上图就展示了一种n·L<0却存在阴影的情况,但此时这个阴影却无法显示出来。

L是光向量,上图这种情况也就说明,光线来自于平面的背面,光线不应该被物体遮挡,更不应该产生阴影。??????????????????????????????????

为了纠正这个问题,我们用指向无穷远处光源的方向向量\tilde{L}=-L来取代光线方向向量L。可以看出,r(t)=p+tL \ \ r(t)=p+t\tilde{L}定义的是相同的3D直线,且直线与平面之间的交点也是一致的(利用不同的交点参数值t_s来弥补\tilde{L}L之间的符号差异)。因此使用\tilde{L}=-L会得到与L相同的计算结果,但是会保证s_w=n\cdot L>0,以此来绕开w坐标为负值的这个坑。

❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓❓

②点光阴影:

点光阴影与平行光阴影大体相同,唯一不同的是平行光阴影中使用的射线方程:r(t)=p+tL[因为平行光的L是固定的],而电光阴影中使用的射线方程:r(t)=p+t(p-L) -- 这里L的含义不同:此处L是指点光源的位置L,平行光则为无穷远处光源的方向向量(光向量)(与平行光光线传播方向相反的向量)。

 根据射线方程与平面方程,可以得到关于顶点p的阴影投影:

s=r(t_s)=p-\frac{n\cdot p +d }{n\cdot (p-L)}(p-L)

区别:L换成P-L

 该式子也可以写作矩阵方程:

s_{point}=\begin{bmatrix} n\cdot L+d-L_xn_x & -L_yn_x & -L_zn_x & -n_x \\ -L_xn_y & n\cdot L+d-L_yn_y & -L_zn_y & -n_y \\ -L_xn_z & -L_yn_z & n\cdot L+d-L_zn_z & -n_z \\ -L_xd & -L_yd & -L_zd & n\cdot L \end{bmatrix}

 

 为了证明这个矩阵方程等价于上述式子,我们采取上一节同样的方法验证(略)。对于该矩阵的最后一列中并没有0项,则有:S_w=-p_xn_x-p_yn_y-p_zn_z+n\cdot L=-p\cdot n+n\cdot L,这是因为我们需要透视除法除以n\cdot(p-L)=n\cdot p-n\cdot L

③通用阴影矩阵:

我们用齐次坐标创建一个同时应用于电光与方向光的通用阴影矩阵:

如果Lw=0,则L表示无穷远处光源的方向向量(光向量) -- 对应S_{dir};如果Lw=1,则L表示点光的位置 -- 对应S_{point}

阴影矩阵表示由顶点p到其投影s的变换:

{\color{Red} S=\begin{bmatrix} n\cdot L+dL_w-L_xn_x & -L_yn_x & -L_zn_x & -L_wn_x \\ -L_xn_y & n\cdot L+dL_w - L_yn_y & -L_zn_y & -L_wn_y \\ -L_xn_z & -L_yn_z & n\cdot L+dL_w-L_zn_z & -L_wn_z \\ -L_xd & -L_yd & -L_zd & n\cdot L \end{bmatrix}}

DirectX的数学库提供了以下函数,用以构建在特定平面内投射阴影所用的相应矩阵,如果w=0,表示平行光,而w=1表示点光

inline XMMATRIX XM_CALLCONV XMMatrixShadow(
    FXMVECTOR ShadowPlane,
    FXMVECTOR LightPosition // w=0:平行光;w=1:点光
);

④使用模板缓冲区防止双重混合:

将物体的几何形状投射到平面而形成阴影时,可能(实际上也经常出现)会有两个甚至更多的平面阴影三角形相互重叠。若此时用透明度这一混合技术来渲染阴影,则这些三角形的重叠部分会混合多次,使之看起来更暗。

👆图可以看到,阴影中存在颜色更暗的"粉刺"(acne)部分,这些区域是骷髅头平面投影三角形重合的部分,由此导致了"双重混合"的发生。正常阴影效果应该是如👇图:

在第一次渲染阴影像素时,由于模板缓冲区元素为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;

为了防止双重混合,我们用下列的深度/模板状态来设置PSO:

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;

// We are not rendering backfacing polygons, so these settings do not matter.
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"])));

接着用StencilRef值为0的阴影PSO来绘制骷髅头阴影:

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平面
XMVECTOR toMainLight = -XMLoadFloat3(&mMainPassCB.Lights[0].Direction); // 平行光的光向量
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f);
XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S * shadowOffsetY);

注意,我们将投影网格沿着y轴做了少量的偏移调整,以防止发生深度冲突(z-fighting),所以阴影网格不会与地板网格相交,得到的最终效果是阴影会略高于地板。如果这两种网格相交,则由于深度缓冲区的精度限制,将导致地板与阴影的网格像素为了各自的完全显性而发生闪烁的现象。


问题1:如果我们位于平面镜与物体之间的位置呢?

问题2:深度测试(Depth Buffer Test)与模板测试(Stencil Test)在渲染流水线中的次序?

(32 封私信 / 2 条消息) 深度测试/模版测试/透明度测试先后顺序是什么样的? - 知乎 (zhihu.com)

裁剪测试 -- alpha测试 -- 模板测试 -- 深度测试 -- blending

只有这些测试都通过了,这个片段才会被渲染!

所以,第一次标记模板缓冲区中mirror部分,但不实际绘制mirror,该操作是由以下步骤实现的:

①开启深度测试:不考虑被不透明实体遮挡住的镜面部分

②关闭深度写入:因为并没有实际绘制物体,所以关闭深度写入

③RenderTargetWriteMask = 0:禁止向后台缓冲区的某些通道写入值,设置为0表示RGBA四个通道都不允许写入 -- 这就是为什么mirror没有被实际绘制的原因


②与③是有区别的:②是禁止向深度缓冲区写入数据,③是禁止向后台缓冲区写入数据

问题3:深度测试的成功与否,与关闭深度写入的关系?

无直接关系,深度测试的开启/关闭,成功/失败,是通过比较操作符来判定的,而深度写入的意思是深度测试成功后,是否将深度值进行覆写。且深度测试如果被关闭,不代表深度测试失败,像素仍然可能被渲染,只是这时绘制顺序十分重要。

(32 封私信 / 2 条消息) 为什么半透明模型的渲染要使用深度测试而关闭深度写入? - 知乎 (zhihu.com)


🍖课后练习题:

3.修改程序,使镜像超出镜面部分:

关闭模板测试

reflectionsDSS.StencilEnable = false; // true->false

4.修改程序,不采取措施防止"双重混合"的发生,观察"粉刺"的生成:

关闭模板测试

shadowDSS.StencilEnable = false; // true->false

8.深度复杂性(depth complexity)是指通过深度测试竞争,向后台缓冲区中某一特定元素写入像素片段的次数。事实上,显卡可能会在每一帧中将某一像素填充多次,这种重复绘制的行为会对性能造成影响,因为显卡把时间浪费在了覆写最终看不到的像素上。

统计深度复杂性:我们使用模板缓冲区作为计数器,每个像素的初始模板值从0开始,每绘制一次该像素,该值+1(D3D12_STRNCIL_OP_INCR),当然比较函数使用D3D12_COMPARISON_FUNC_ALWAYS。严格意义上来讲,在统计深度复杂多样性时,我们只需将场景渲染到模板缓冲区即可。

可视化深度复杂性:我们进行如下操作使深度复杂性通过模板缓冲区可视化

  1. 令颜色C_k与深度复杂性k关联起来:深度复杂性为1~5的像素为蓝色,6~10的像素为绿色等
  2. 设定模板缓冲区的运算方法为D3D12_STENCIL_OP_KEEP,当我们为模板缓冲区可视化书写代码时,只从模板缓冲区读取数据而不写入数据
  3. 对于每种深度复杂性级别k而言,模板比较函数...Equal,且设置模板参考值为k,用特定颜色来绘制整个投影窗口内容。

为不同深度复杂性等级设置不同的颜色的方法:通过RenderTargetWriteMask禁止不同颜色通道的写入,可以创造出2^3=8种颜色的混合

每次设置一个模板参考值,并绘制所有渲染项,筛选出符合的像素

最后PS直接输出白色,这样结合RenderTargetWriteMask就有8种

※没有方法能直接将画面中的像素直接提取出来,根据模板缓冲区中对应的值画不同的颜色?

如何通过像素着色器PS修改深度值:

struct PixelOut
{
    float4 color : SV_Target;
    float depth : SV_Depth;
};

PixelOut PS(VertexOut pin)
{
    PixelOut pout;

    // 常规像素处理
    // [...]
    pout.color = float4(litColor, alpha);

    pout.depth = pin.PosH.z - 0.05f; // 将像素深度值设置在归一化范围[0,1]内
    
    return pout;
}
// SV_Position元素的z坐标(pin.PosH.z)表示的是未加改动的像素深度值,通过SV_Depth,输出经过修改的深度值

9.另一种实现深度复杂性可视化的方法是使用加法混合技术。首先清理后台缓冲区,使之成为黑色,并禁用深度测试(?)。其次,设源混合因子与目标混合因子都为D3D12_BLEND_ONE,再将混合运算方法置为D3D12_BLEND_OP_ADD,使混合公式变为C=C_{src}+C_{dst}。观察此公式可知,针对每个像素,我们将其所有像素片段的颜色都累加在一起并回写。最后,再用像素着色器以类似于(0.05,0.05,0.05)这种低强度的颜色输出方式。这样一来,此场景渲染完成之后,我们通过观察像素的颜色强度便可了解场景中深度复杂性的分布情况。

我采取的(0.05,0.05,0.05,0.05):

12.从渲染项阴影的世界矩阵中移除垂直方向上(z坐标)的偏移量,以便观察深度冲突:


网站公告

今日签到

点亮在社区的每一天
去签到