面向 VRChat 的渲染入门 (2) 失败的透明
前言
失败了,姑且看看图吧,拿来照玩家的话有些不好看...

实现方法
如何:混合颜色
由于渲染管线的并行特性限制,我们并不能在一个 Pixel Shader 中读写同一个缓冲区。那么,我们如何将当前的颜色和已经绘制的颜色混合起来呢?答案是通过 Blend 指令。Blend 指令将在渲染管线的特定步骤执行,以将当前 Pixel Shader 的输出缓冲 (Source) 和屏幕缓冲区(Destination) 以指定的方式混合。典型操作表达式为:
其中 OP 代表某种操作,通常是加,也可以是下面这几十种(以 [Enum(UnityEngine.Rendering.BlendOp)] 注明)

而 Blend 指令的基础格式是:
Blend [RT] <Src> <Dst> | <SrcRGB> <DstRGB>, <SrcA> <DstA>Src 等参数统称为 Blend Mode, 其取值以 [Enum(UnityEngine.Rendering.BlendMode)] 注明,有如下几个:
我们要实现一种朦胧感,考虑让材质输出为一个灰色蒙版,随后配置 Source blend mode 为 One,Destination blend mode 为 OneMinusSrcColor。
如何:选择范围
我们当然不希望所有东西都被我们选中并且做颜色混合。总结起来,
- 一帧画面有一个渲染队列,每个 Material 可以指定其排序优先级,渲染时从低到高执行渲染
- 在 PS 输出后,根据模板缓冲 (Stencil Buffer) 做 StencilTest,如果不通过,则该像素丢弃;测试后可以按情况修改 Stencil Buffer
- 在 StencilTest 后,根据深度缓冲 (Depth Buffer) 做 ZTest,如果不通过,则该像素丢弃;测试后可以按情况修改 Depth Buffer
- 都通过的像素进行 Blend 后输出
所以实际上,我们可以控制的是 RenderQueue,StencilTest 和 ZTest 的行为。
这部分内容过多,建议查看原视频理解掌握。这里给出几张图和说明,帮助理解:

其中,手是素体的一部分,其 RenderQueue 为 2451,Stencli 为 Always Keep (0), ZWrite = true;
衣服的 RenderQueue 为 2460,Stencli 和 ZWrite 都和素体保持一样的设定。
在这前面放置一个胶囊,应用我们的材质,然后 Blend Mode 设置和上面混合颜色设置一致。

这种情况下,先绘制了素体,然后绘制我们的胶囊。胶囊绘制完成后,由于其 ZWrite 为 On,且胶囊比衣服离我们更近,所以深度缓冲被写入了更大的值;随后绘制衣服,在 ZTest 时已被胶囊绘制的部分会失败,从而维持胶囊的绘制结果,显示为部分透明。(如果设置 RenderQueue 为 2461 则这种透明消失)
在此基础上,我们再设置素体的 Stencil 为 Always Replace (22),随后在胶囊材质设置 StencilTest Mode 为 Euqal 22,可以得到:

这是因为 Stencil 测试通过的只有素体在胶囊这个范围内的部分,也只有这些东西被混合了并且写了深度。
当我们关闭深度写入,以上的一切都消失了,又回到了原本的样子。这是因为后续衣服在渲染的时候发现自身的Z小于缓冲区里的(ZTest 默认 LessEqual Pass),进行了覆盖。这就是 ZTest 的意义。
补充一点,正经的半透明物体一般做法是将渲染队列开高,然后将 ZWrite 关闭。这样就可以达到该透过的物品已经绘制了,但是位于半透明物品前(更靠近摄像机)的物体仍然可以进行正确的覆盖,符合预期。
最终代码
Shader "Unlit/ScreenMaskShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" { }
[Enum(UnityEngine.Rendering.BlendMode)]
_SrcBlend ("Source blend", Float) = 0
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend ("Destination blend", Float) = 0
[Enum(UnityEngine.Rendering.BlendOp)]
_BlendOp ("Blend Operation", Float) = 0
_StencilRef ("Stencil Ref Value", Range(0,255)) = 0
[Enum(UnityEngine.Rendering.CompareFunction)]
_StencilComp ("Stencil Compare Mode", Float) = 0
[Enum(UnityEngine.Rendering.StencilOp)]
_StencilPassOp ("Stencil Pass Operation",Float) = 0
[Enum(UnityEngine.Rendering.StencilOp)]
_StencilFailOp ("Stencil Fail Operation",Float) = 0
[Enum(UnityEngine.Rendering.CompareFunction)]
_ZTestComp ("ZTest Compare function", Float) = 0
[Enum(Off, 0, On, 1)]
_ZWrite ("ZWrite", Float) = 0
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
Blend [_SrcBlend] [_DstBlend]
// Blend Zero One, SrcAlpha OneMinusSrcAlpha
BlendOp [_BlendOp]
// ColorMask 0
Stencil
{
Ref [_StencilRef]
Comp [_StencilComp]
Pass [_StencilPassOp]
Fail [_StencilFailOp]
}
ZTest [_ZTestComp]
ZWrite [_ZWrite]
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col *= 0.08;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDHLSL
}
}
}liltoon 啥都有,但是为了简化模型还是手搓了
附注
关于各种缓冲区的结论,是使用 RenderDoc 抓帧得到的,但是仍然有如下几个点:
- 抓到的帧上下颠倒:DX11 的 Y 轴倒置。
- 深度缓冲中,离摄像机越近的 Z 越大,但是 ZTest 时行为又是离摄像机越远越大:DX11 的 Z 轴倒置。
- Game 中看到的提前写了更大深度的区域为黑色~白色闪动,但是场景中正常
暂未找到原因。