d3d12龙书学习之MiniEngine的最小化实现(七) 龙书第11章 模板缓冲

本文介绍了如何使用Direct3D 12的模板缓冲技术在MiniEngine中实现镜像世界。通过修改相机坐标系、深度和模板缓冲设置,以及创建不同的PSO,实现了镜面反射、透明混合和阴影效果。在实施过程中,作者遇到了MiniEngine模板缓冲无效的问题,通过调整深度缓冲区格式得以解决。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

本章的主要内容是通过模板缓冲来实现镜中世界。
对于镜中世界的实现来说,还可以采用一个额外的摄像机来做,当然我也没有实际操作过。
先按照本章内容来通过模板缓冲做一个实现。

模板缓冲的意义

模板缓冲和深度缓冲是一套东西,他们的缓冲区是合在一起处理的。
我们知道深度缓冲可以通过深度检测,来绘制物体的遮挡关系。而模板缓冲呢?就是按照程序设定的规则,额外进行模板检测,只有通过深度+模板检测的像素才会进行下一步的处理。也就是本身是深度缓冲的一个补充,用于用户自定义的检测。

当然也可以单独使用模板检测。

修改camera坐标系

从做第八章光照的时候,就感觉这个坐标系是有问题的。因为调整的结果不符合预期,我原本以为是自己数学水平不够导致的。
后来修改过这个shader中mul的入参顺序(在C++写常量缓冲区时对矩阵做一次转置)。
直到前几天学这个模板缓冲,发现生成的镜子在墙体的右边(d3d12龙书是在左边),然后终于发现MiniEngine采用的居然是右手坐标系。

dx默认的实际是左手坐标系,opengl才是右手坐标系,我挺讨厌这种“靠拢”的行为(甚至于mul入参顺序也是向opengl靠)

那么第一步,我这里就要把这个坐标系改成左手的(否则学习龙书,直接拿到龙书中的顶点集绘制图形,会z轴翻转)
有3个修改点

camera生成的矩阵修改

BaseCamera类只保留3个矩阵

	// 0 矩阵变换
	// 1. 渲染目标从模型坐标系转到世界坐标系--->世界变换矩阵
	// 2. 再从世界坐标系转到视角坐标系--->视角变换矩阵 m_ViewMatrix
	// 3. 从视角坐标系转换到投影坐标系--->投影变换矩阵 m_ProjMatrix
	
	// 世界坐标系转换到视角坐标系
	Matrix4 m_ViewMatrix;        // i.e. "World-to-View" matrix
	
	// 视角坐标系转到投影坐标系
	Matrix4 m_ProjMatrix;        // i.e. "View-to-Projection" matrix
	
	// 从世界坐标系直接转换到投影坐标系
	Matrix4 m_ViewProjMatrix;    // i.e.  "World-To-Projection" matrix.

Camera.cpp修改如下

void BaseCamera::SetLookDirection( Vector3 forward, Vector3 up )
{
    // 计算前方
    Scalar forwardLenSq = LengthSquare(forward);
    forward = Select(forward * RecipSqrt(forwardLenSq), Vector3(kZUnitVector), forwardLenSq < Scalar(0.000001f));

    // 根据提供的上和前方,计算右方
    Vector3 right = Cross(up, forward);
    Scalar rightLenSq = LengthSquare(right);
    right = Select(right * RecipSqrt(rightLenSq), Cross(Vector3(kYUnitVector), forward), rightLenSq < Scalar(0.000001f));

    // 正交化,计算实际的上方
    up = Cross(forward, right);

    // 计算摄像机的转换矩阵
    m_Basis = Matrix3(right, up, forward);
    m_CameraToWorld.SetRotation(Quaternion(m_Basis));
}

void BaseCamera::Update()
{
    // 计算视角变换矩阵,还没有看懂 m_CameraToWorld
    m_ViewMatrix = Matrix4(~m_CameraToWorld);
    
    // Matrix4中的*重载,故意反着写的。所以这里反着乘
    // 计算视角投影转换矩阵。这样拿到世界矩阵再乘以这个值就可以算出最终的投影坐标了
    m_ViewProjMatrix = m_ProjMatrix * m_ViewMatrix;
}

void Camera::UpdateProjMatrix( void )
{
    DirectX::XMMATRIX mat = XMMatrixPerspectiveFovLH(m_VerticalFOV, m_AspectRatio, m_NearClip, m_FarClip);

    SetProjMatrix(Matrix4(mat));
}

SetLookDirection这个函数中,实际可以直接采用XMMatrixLookAtLH直接算出世界转投影的矩阵。但考虑到一些其他地方的调用,这里暂时保留以前的方式。只是把内部的计算按照左手坐标系修改一下。

还有,Matrix4这个类本身把乘法的操作符重载做了反向的操作,因为调用太多,暂时先不管。对于矩阵乘法就先按照反向的来吧。

深度缓冲区默认值以及默认深度\模板检测函数的修改

修改DepthBuffer.h中ClearDepth的默认值为1.0f(因为在左手坐标系中,z越大代表越远)
修改DepthStateReadWrite的深度比较函数为D3D12_COMPARISON_FUNC_LESS(这里小于等于一般也没什么问题的)

默认PSO采用的光栅化修改

在以前的学习中,我们默认的PSO使用的光栅化参数为Graphics::RasterizerDefault
这里需要反过来,改成逆时针绘制顶点RasterizerDefaultCw(这个顺时针逆时针会影响面的法向量朝向,以确定这个面是正向还是反向,然后来绘制以及做背面剔除)

绘制基础图形(地板、墙、头骨)

这个绘制实际上没有什么好说的。前边已经绘制物体很多次了。
这里主要是修改了一下绘制结构,更向d3d12龙书靠拢了。

因为修改过坐标系,所以这里也重新搞了一下镜头旋转的处理。主要还是更方便的使用吧。
看下绘制基础图形的效果图:
在这里插入图片描述
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/39fe9e5da4226248f05837363606ade2f87e5ab8

绘制镜中世界

对于镜中世界,实际上就是把会映照到镜中的物体在对称的位置再绘制一次,然后玻璃这做透明混合,其他地方不绘制。
这个透明混合,前边章节做过,换个PSO就可以。而对于其他地方不绘制这就用到了本章的学习内容:模板缓冲
整个绘制流程如下

  • 创建几个PSO对象
    1 默认PSO: 用于绘制现实世界的地板、墙壁、头骨
    2 模板PSO: 开启深度测试(仅测试,禁止深度写入,否则这一步的绘制会污染我们的深度数据)、开启模板测试。对于通过了深度测试+模板测试的像素点,在模板缓冲中写入1。这个PSO仅仅绘制镜子。也就是说最终结果就是只有未被挡住的镜子区域的像素位置会写入1,本章仅仅在这里写入了模板缓冲,实际上如果是多次使用,这里之前还需要清空模板缓冲。
    3 模板检测绘制PSO:这里使用正常的深度测试+仅仅判断相等的模板测试。只有模板中对应的像素点通过了模板测试,才会绘制。采用的光栅化配置为顺时针(因为镜中世界都是镜像的)这一步仅仅绘制镜中的世界。
  • 渲染流程
    1 设置默认PSO。绘制现实世界的地板、墙壁、头骨
    2 设置模板PSO,设置模板写入值为1。开始绘制镜子(实际这里并不会绘制镜子,因为这里禁用了深度写入,像素并不会写入渲染缓冲区)。对于未被遮挡的镜子区域(也就是通过了深度+模板测试的区域)设置为1
    3 设置模板检测绘制PSO。这里,因为我们的检测值依旧是1,而只有未被遮挡的镜子区域的模板像素才为1。这里只有像素值为1才会通过模板检测。这里绘制镜中世界,也就是只有未被遮挡的镜子区域才会绘制像素到渲染缓冲区
    4 恢复模板检测值为0

这就是简单的一个模板使用的流程。
当然了,镜中世界像素全部是镜面反转(本项目中是基于yz面镜像),而因为头骨还可以操作移动,所以这里需要动态的计算镜中物体的实时modelToWorld矩阵,用于把顶点转换到现实世界(也就是我们的镜中世界)。按照龙书中的操作来写就可以。

MiniEngine模板缓冲无效的bug

一顿操作之后,发现并没有生效。
于是我详细对比了龙书代码和本例中PSO的参数,发现都是一致的。
尝试修改模板测试的的一些参数并没有生效。问题应该就是模板缓冲就没有被使用。
继续阅读代码,定位到是这个MiniEngine中的深度缓冲区格式错误。
龙书中说的很清楚,模板缓冲仅支持2种格式。
于是修改了Graphics::g_SceneDepthBuffer的创建格式。发现运行报错。。

而这个报错,网上都搜不到什么信息,官方文档更是没有。查了半天,终于找到了怎么修改。
https://github.com/Microsoft/DirectX-Graphics-Samples/issues/281

在DepthBuffer.cpp的函数CreateDerivedViews中作如下修改:

if (stencilReadFormat != DXGI_FORMAT_UNKNOWN)
{
    if (m_hStencilSRV.ptr == D3D12_GPU_VIRTUAL_ADDRESS_UNKNOWN)
        m_hStencilSRV = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

    SRVDesc.Format = stencilReadFormat;
    SRVDesc.Texture2D.PlaneSlice = 1;   // 增加这一行
    Device->CreateShaderResourceView( Resource, &SRVDesc, m_hStencilSRV );
}

终于可以运行成功了。效果也对。问题在于,根本不知道这个修改的意义在哪里,而且查不到对应的资料。
对于dx12的学习来说,这种问题很严重。除非过去已经积累了足够的图形学的开发经验,否则靠自己想破脑袋都想不出来怎么改。
这里再次感谢下这位高手的issue,如果没有这个,估计这一章得卡很久。而且大概率会导致弃用MiniEngine或者做大范围修改才行。

万幸!

看下目前的效果:
在这里插入图片描述

绘制透明镜子

这里没什么好说的。写一个透明混合PSO,再绘制一次镜子就可以了。结果如下:
在这里插入图片描述到目前为止的github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/bf0495b9c7db701811d18e12318655f42a96ca02

绘制现实阴影

龙书中采用的阴影绘制很暴力。因为我以前仅仅知道ShadowMap的方式绘制
龙书中采用的是拿到需要绘制阴影的所有顶点和索引(本例中就是这个头骨),然后根据光源方向,把这些定点全部映射到阴影平面上,然后采用黑色绘制这些顶点。

  • 添加一个影响渲染目标(与头骨一样),通过纹理参数控制为黑色半透明(半透明是防止过于黑)
  • 计算渲染目标的modelToWorld矩阵(用于把顶点映射到阴影平面)
  • 绘制该目标

简单看下效果:
在这里插入图片描述

双重混合?

这个阴影中有些黑点,这是怎么回事?
按照上边的绘制方式,顶点投影到平面绘制,会有一些顶点重叠导致了多次绘制,于是出现了这种效果。这个解决办法也简单
创建一个阴影PSO,开启模板测试。模板初始值设置为0,模板比较函数设置为相等,一个像素点通过了深度+模板测试后为模板中该位置+1,也就是第二次绘制同一个像素时,会无法通过模板测试,于是就不会出现多次渲染了

结果如下:
在这里插入图片描述

镜中阴影?

那么镜中阴影该怎么实现呢?因为绘制镜中世界时,需要使用模板检测,而绘制阴影也同样需要这个模板检测。
除非说是能用两个模板缓冲区。
对这一块我没有做研究。

先以龙书的学习为主。感觉后边真是越来越难了。

本章github地址:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/tree/4ac322900cad8da127fab309de9079aa495dc58d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值