DX12 实现 模板——物体轮廓

发布时间 2023-05-05 23:34:50作者: 爱莉希雅

前言

本篇将展示如何运用深度模板缓冲区来实现游戏中的物体轮廓效果

源代码model_outline

基础知识

模板测试过程

// compare_func:定义的比较函数。对两个参数进行比较
// StencilRef:模板参考值
// StencilReadMask:位于D3D12_DEPTH_STENCIL_DESC
// Value:正在接受模板测试的值
if(compare_func(StencilRef & StencilReadMask, Value & StencilReadMask))
    accept pixel
else
    reject pixel

不懂这个式子的含义也没关系,接下来的内容能理解就行

深度模板描述符

该描述符作用于PSO,因此填写完后赋予PSO即可

D3D12_DEPTH_STENCIL_DESC (d3d12.h)

D3D12_DEPTH_WRITE_MASK (d3d12.h)

D3D12_COMPARISON_FUNC (d3d12.h)

D3D12_DEPTH_STENCILOP_DESC (d3d12.h)

D3D12_STENCIL_OP (d3d12.h)

typedef struct D3D12_DEPTH_STENCIL_DESC {
  BOOL                       DepthEnable;		// 启用/禁用深度测试
  D3D12_DEPTH_WRITE_MASK     DepthWriteMask;	// 启用/禁用深度写入
  D3D12_COMPARISON_FUNC      DepthFunc;			// 深度测试的比较函数
  BOOL                       StencilEnable;		// 启用/禁用模板测试
  UINT8                      StencilReadMask;	// 读取模板值时,禁止对应位的读取.
												// 默认不屏蔽任何一位:D3D12_DEFAULT_STENCIL_READ_MASK
  UINT8                      StencilWriteMask;	// 写入模板值时,禁止对应位的写入(深度写入).
    											// 默认不屏蔽任何一位:D3D12_DEFAULT_STENCIL_WRITE_MASK
  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,	// Turn off writes to the depth-stencil buffer.
  D3D12_DEPTH_WRITE_MASK_ALL = 1	// Turn on writes to the depth-stencil buffer.
} ;

typedef enum D3D12_COMPARISON_FUNC {
  D3D12_COMPARISON_FUNC_NEVER = 1,			// 不通过测试
  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			// 通过测试
} ;

#define	D3D12_DEFAULT_STENCIL_WRITE_MASK	( 0xff )
#define	D3D12_DEFAULT_STENCIL_READ_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, // 模板缓冲区的数据的值+1,并将该该值限制(clamp)在一定范围内
  D3D12_STENCIL_OP_DECR_SAT = 5, // 模板缓冲区的数据的值-1,并将该该值限制(clamp)在一定范围内
  D3D12_STENCIL_OP_INVERT = 6,	 // 模板缓冲区的数据的值按二进制位进行反转
  D3D12_STENCIL_OP_INCR = 7,	 // 模板缓冲区的数据的值+1,如果该值超过最大值,则进行回环(warp)
  D3D12_STENCIL_OP_DECR = 8		 // 模板缓冲区的数据的值-1,如果该值小于最小值,则进行回环(warp)
  // warp:对于8位的模板缓冲区来说,其最大值为255,若+1等于255则使其置于0
} ;

几个重点:

  • 启用/禁用深度测试:DepthEnable
  • 启用/禁用深度写入:DepthWriteMask
  • 启用/禁用模板测试:StencilEnable
  • 启用/禁用模板写入:StencilWriteMask

设置模板参考值

ID3D12GraphicsCommandList::OMSetStencilRef (d3d12.h)

void OMSetStencilRef(
  [in] UINT StencilRef
);

实现物体轮廓

思路

实现物体轮廓需要分两次渲染

  • 第一次渲染。启用深度测试、深度写入、模板测试、模板写入,将模板比较函数设为D3D12_COMPARISON_FUNC_ALWAYS(总是通过测试),当物体的片段进行渲染时,在模板缓冲区中将对应的片段更新为1(StencilPassOp = D3D12_STENCIL_OP_REPLACE,OMSetStencilRef(1))
  • 第二次渲染。禁用深度测试(防止被其他物体覆盖)、模板写入(需要和StencilRef进行比较),把物体稍微放大一丢丢,将模板函数设为D3D12_COMPARISON_FUNC_NOT_EQUAL(绘制模板值不等于1的片元,也就是放大后多出的轮廓),使用一个单独的PS绘制边框颜色。记得再启用深度测试、模板写入

shader

m_shaders["Outline_VS"] = CompileShader(GetAssetFullPath(L"Outline_VS.hlsl").c_str(), nullptr, "main", "vs_5_1");
m_shaders["Outline_PS"] = CompileShader(GetAssetFullPath(L"Outline_PS.hlsl").c_str(), nullptr, "main", "ps_5_1");

cbuffer ObjectCB : register(b0)
{
    float4x4 world;
}

cbuffer PassCB : register(b1)
{
    float4x4 view;
    float4x4 inverseView;
    float4x4 projection;
    float4x4 inverseProjection;
    float4x4 viewProjection;
    float4x4 inverseViewProjection;
    
    float3 eyeWorldPosition;

    Light lights[MAX_LIGHT_COUNT];
}

static float4x4 scale =
{
    1.2f, 0.f, 0.f, 0.f,
	0.f, 1.2f, 0.f, 0.f,
	0.f, 0.f, 1.2f, 0.f,
	0.f, 0.f, 0.f, 1.f
};

struct VSInput
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 uv : TEXCOORD;
};

struct PSInput
{
    float3 positionW : POSITION;
    float4 positionH : SV_POSITION;
    float3 normalW : NORMAL;
    float2 uv : TEXCOORD;
};

// VS
PSInput main(VSInput input)
{
    PSInput output;
    
    float4 positionW = mul(mul(float4(input.position, 1.f), world), scale);
    
    output.positionW = positionW.xyz;
    output.normalW = mul(mul(input.normal, (float3x3) world), (float3x3)scale);
    
    output.positionH = mul(positionW, viewProjection);
    
    output.uv = input.uv;
    
	return output;
}

// PS
float4 main() : SV_TARGET
{
	return float4(0.04f, 0.28f, 0.26f, 1.0f);
}

PSO & 深度模板描述符

//
// 第一次渲染
//
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaquePSODesc = {};
{
    // set depth & stencil test
    D3D12_DEPTH_STENCIL_DESC outlineDesc;
    outlineDesc.DepthEnable = TRUE; // enable depth test
    outlineDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;    // Turn on writes to the depth-stencil buffer
    outlineDesc.DepthFunc = D3D12_COMPARISON_FUNC_LESS; // if less pass the depth test

    outlineDesc.StencilEnable = TRUE;
    outlineDesc.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;   // no mask
    outlineDesc.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK; // no mask  

    outlineDesc.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS; // pass stencil test
    outlineDesc.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;      // no change
    outlineDesc.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; // no change
    outlineDesc.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;   // use stencil reference to replace data of stencil buffer

    // default
    outlineDesc.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
    outlineDesc.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
    outlineDesc.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
    outlineDesc.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;

    opaquePSODesc.DepthStencilState = outlineDesc;
}

ThrowIfFailed(m_device->CreateGraphicsPipelineState(&opaquePSODesc, IID_PPV_ARGS(&m_PSOs["opaque"])));

//
// 第二次渲染
//
D3D12_GRAPHICS_PIPELINE_STATE_DESC outlinePSODesc = opaquePSODesc;

D3D12_DEPTH_STENCIL_DESC outlineDesc;
outlineDesc.DepthEnable = FALSE; // forbie depth test
outlineDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;    // Turn on writes to the depth-stencil buffer
outlineDesc.DepthFunc = D3D12_COMPARISON_FUNC_LESS; // if less pass the depth test

outlineDesc.StencilEnable = TRUE;
outlineDesc.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;   // no mask
outlineDesc.StencilWriteMask = 0x00; // forbie stencil write

outlineDesc.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_NOT_EQUAL; // pass stencil test
outlineDesc.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;      // no change
outlineDesc.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; // no change
outlineDesc.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;   // no change

// default
outlineDesc.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
outlineDesc.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
outlineDesc.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
outlineDesc.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;

outlinePSODesc.VS =
{
    reinterpret_cast<BYTE*>(m_shaders["Outline_VS"]->GetBufferPointer()),
    m_shaders["Outline_VS"]->GetBufferSize()
};

outlinePSODesc.PS =
{
    reinterpret_cast<BYTE*>(m_shaders["Outline_PS"]->GetBufferPointer()),
    m_shaders["Outline_PS"]->GetBufferSize()
};

ThrowIfFailed(m_device->CreateGraphicsPipelineState(&outlinePSODesc, IID_PPV_ARGS(&m_PSOs["Outline"])));

渲染

// 绘制物体
m_commandList->OMSetStencilRef(1);	// 将模板参考值设为1
m_pCurrentFrameResource->PopulateCommandList(m_commandList.Get(), m_renderLayers[(int)Core::RenderLayer::Opaque], m_cbvSrvHeap.Get(), m_passCBVOffset, m_frameIndex, m_cbvSrvDescriptorSize);

// 绘制轮廓
m_commandList->SetPipelineState(m_PSOs["Outline"].Get());
m_pCurrentFrameResource->PopulateCommandList(m_commandList.Get(), m_renderLayers[(int)Core::RenderLayer::Opaque], m_cbvSrvHeap.Get(), m_passCBVOffset, m_frameIndex, m_cbvSrvDescriptorSize);

输出

image-20230505230357005

现在实现游戏中外挂的透视效果也是轻轻松松

reference

[LearnOpenGL ](https://learnopengl-cn.github.io/04 Advanced OpenGL/02 Stencil testing/)

Direct3D 12 programming guide

Directx12 3D 游戏开发实战