以下的例子实现方式有多种多样,这里为了做示例测试,制作其一一种。
// jave.lin 2019.07.08
Shader "Test/MyStencilMasker" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Luma("Luma", Range(0, 3)) = 1
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }
Pass {
ColorMask 0
ZWrite off
ZTest always
Stencil {
Ref 1
Comp always // default
Pass replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Luma;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv);
fixed luma = dot(col.xyz, 1);
if (luma <= _Luma) discard;
return col;
}
ENDCG
}
}
}
留意Stencil设置
Stencil {
Ref 1 // 参考值为1
Comp always // default,比较方式:不比较,直接通过
Pass replace // stencil 通过后的操作:将参考值替代到缓存值
}
该Stencil设置的意思:不用去测试模板缓存的值,只要有片段输出了,那么就Ref的值替换到光栅片段对应屏幕像素坐标的模板缓存值
然后配合上frag函数的判断亮度值不够的,都discard来丢弃片段内容,从而达到控制是否需要运行到模板缓存这一环节,如果丢弃了光栅片段,那么这个frag处理单元就会直接闭塞等待(完成,且等待:意思就不会再往渲染光线走了)其他片段的操作完成
// jave.lin 2019.07.08:被镂空的
Shader "Test/MyStencilBeMasked1" {
Properties { _MainTex ("Texture", 2D) = "white" {} }
SubShader
{
Tags { "RenderType"="Opaque" }
Pass {
Stencil {
Ref 1
Comp notequal
Pass keep // default
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Luma;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); }
ENDCG
}
}
}
留意Stencil的设置
Stencil {
Ref 1 // 参考值为1
Comp notequal // 缓存值不等于参考值
Pass keep // default,如果比较通过,keep会让缓存值不会
}
该Stencil设置的意思是:当模板缓存中的值不等于1时,才能通过模板测试,且通过后不修改模板缓存的值
注意: 这里头要注意Masker的Subshader的tags中,添加了:“Queue”=“Geometry-1”(“Queue默认是Geometry),使用了Geometry-1,Geometry是2000的绘制队列编号(编号越小,绘制越早),所以Masker的绘制队列编号是1999。在Be Masked中没有设置"Queue”,所以是2000的队列编号。所以Masker会比Be Masked先绘制。因为要确保被镂空对象绘制前,就有模板数据来做测试而达到镂空效果,如果Queue不设置的话,可能会因为绘制顺序问题,绘制实体对象时模板缓存数据没有写入,导致镂空无效,等会的运行效果会加上Masker的subshader中的Queue标签值删掉的情况。
绘制顺序的问题,可以理解为:绘制的对象需要模板数据来做效果时,先确保模板缓存已有写入你想要的数据,所以先将写入模板缓存的对象先绘制,接着再绘制应用模板缓存的对象。
下面再看看遮罩的情况
// jave.lin 2019.07.08:被遮罩的
Shader "Test/MyStencilBeMasked" {
Properties { _MainTex ("Texture", 2D) = "white" {} }
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
Stencil {
Ref 1
Comp equal
Pass keep // default
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Luma;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); }
ENDCG
}
}
}
留意Stencil的设置
Stencil {
Ref 1 // 参考值为1
Comp equal // 缓存值等于参考值
Pass keep // default,如果比较通过,keep会让缓存值不会
}
该Stencil设置的意思为:当模板缓存中的值等于Ref的值时,才通过模板测试,而且通过后,保持模板缓存的值不变
Masker得shader代码中,将Subshader的tags中"Queue"=“Geometry-1"的”-1"删除掉,或是整个"Queue"的键值对删除。下面是运行异常的情况
可以看到镂空的效果突然出现,突然消失。
这是因为Unity在渲染不透明的Geometry队列的渲染对象是,排序是从前到后的(从靠近的到远离的渲染),因为慢慢的将Masker往屏幕拉近,所以Masker的渲染顺序突然提前了(可能是按物体质心点来算位置的)。这是不希望得到的结果。
所以一定要留意使用Stencil时,确保你需要比较用的模板缓存值已写入。
稍微与上面的一个渲染对象镂空多个不同渲染对象的方式不一样,因为描边效果是在一个shader里,多个pass实现的。这种方式有利有弊。
描边思路,我们这儿只使用其一一种,有很多种方式处理描边的。
这儿使用的是:
Shader代码:
// jave.lin 2019.07.08
Shader "Test/Outline" {
Properties {
[KeywordEnum(P,NP)] _P("Perspective", Float) = 0
_MainTex ("Texture", 2D) = "white" {}
_OutlineWidth ("Outline width", Range(0, 0.1)) = 0.01
_OutlineColor ("Outline color", Color) = (1,0,0,1)
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Stencil {
ref 1
comp always
pass replace // mark-up
zfail replace // mark-up
}
// draw model and write stencil
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); }
ENDCG
}
// draw outline
Pass {
Stencil {
ref 0
comp equal
}
ZTest Always
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _P_P _P_NP
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
half4 _OutlineColor;
fixed _OutlineWidth;
float4 vert (appdata v) : SV_POSITION {
obj space,会有透视变化的问题,我们该用projection space下的pos.xy * posw消除透视,因为
//v.vertex.xyz += v.normal * _OutlineWidth;
//return UnityObjectToClipPos(v.vertex);
// projection space
float4 pos = UnityObjectToClipPos(v.vertex);
fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
fixed2 offset = TransformViewToProjection(vNormal.xy);
//pos.xy += offset * _OutlineWidth * pos.w;
#if _P_P // 有透视
pos.xy += offset * _OutlineWidth;
#else // 无透视
// 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
// 这样无论多远多近都可以按恒定的描边边宽来显示
pos.xy += offset * _OutlineWidth * pos.w;
#endif
return pos;
}
fixed4 frag () : SV_Target { return _OutlineColor; }
ENDCG
}
}
}
边框与深度无关系
(下面的吐槽一下,由于firefox浏览器问题,导致我重写新了第二次的伪代码,无语了)
Stencil Test 的伪代码(pseudocode):
// jave.lin 2019.07.09
// macro defines
#ifdef STENCIL8BITS
#define STENCILMASK 255
#elif STENCIL4BITS
#define STENCILMASK 16
#elif STENCIL1BITS
#define STENCILMASK 1
#else
#define STENCILMASK 255
#endif
// 参考值与模板比较的枚举
enum StencilComp {
always,
never,
equal,
notequal,
less,
greater,
lequal,
gequal
}
// 使用参考值对缓存值操作的枚举
enum StencilOp {
keep,
zero,
replace,
incr,
decr,
invert,
incrwrap,
decrwrap
}
// 深度写入枚举
enum DepthWrite {
on,
off
}
// 深度比较枚举
enum DepthComp {
always,
never,
equal,
notequal,
less,
greater,
lequal,
gequal
}
// draw call fragment 的上下文
struct DepthContext {
DepthWrite write;
DepthComp comp;
};
// draw call fragment 的上下文
struct FragmentContext {
...
float x,y,z;
...
};
// draw call stencil 的上下文
struct StencilContext {
uint ref;
StencilComp comp;
StencilOp pass;
StencilOp fail;
StencilOp zfail;
uint readkMask;
uint writeMask;
};
// draw call context
struct DrawCallContext {
...
FragmentContext f;
StencilContext s;
DepthContext d;
bool discard;
...
};
// draw call variables 变量声明
static byte[,] stencilbuff = new byte[screenw, screenh]; // 模板缓存
static float[,] depthbuff = new float[screenw, screenh]; // 深度缓存
static bool stenciltest_enabled = true; // 模板测试开关
// 模板测试函数
bool StencilTestFunc(FragmentContext f, StencilContext s, DepthContext d) {
bool pass = StencilCompFunc(f, s); // 模板测试是否通过
if (pass) {
bool zpass = DepthTestFunc(f, d); // 深度测试是否通过
if (zpass) {
StencilOpFunc(s.pass, f, s); // 模板测试 与 深度测试 都通过
return true;
} else {
StencilOpFunc(s.zfail, f, s); // 模板测试通过了,但是深度测试没通过
return false;
}
} else {
StencilOpFunc(s.fail, f, s); // 没有通过模板测试
return false;
}
}
bool StencilCompFunc(FragmentContext f, StencilContext s) {
switch(s.comp) {
case StencilComp.always: return true;
case StencilComp.never: return false;
case StencilComp.equal: return (stencilbuff[f.x,f.y] & s.readkMask) == s.ref;
case StencilComp.notequal: return (stencilbuff[f.x,f.y] & s.readkMask) != s.ref;
case StencilComp.less: return (stencilbuff[f.x,f.y] & s.readkMask) > s.ref;
case StencilComp.greater: return (stencilbuff[f.x,f.y] & s.readkMask) < s.ref;
case StencilComp.lequal: return (stencilbuff[f.x,f.y] & s.readkMask) >= s.ref;
case StencilComp.gequal: return (stencilbuff[f.x,f.y] & s.readkMask) <= s.ref;
default: /* log error here */ return false;
}
}
void StencilOpFunc(StencilOp op, FragmentContext f, StencilContext s) {
uint buffv = 0;
switch(op) {
case StencilOp.keep: /* noops */ break;
case StencilOp.zero: stencilbuff[f.x,f.y] = 0; break;
//https://www.khronos.org/opengl/wiki/Stencil_Test#Stencil_operations介绍只有replace有用到writemask
//其他的就先取消掉使用readmask与writemask
case StencilOp.replace: stencilbuff[f.x,f.y] = (f.ref & s.writeMask); break;
case StencilOp.incr: stencilbuff[f.x,f.y] = min(STENCILMASK, stencilbuff[f.x,f.y] + 1); break;
case StencilOp.decr: stencilbuff[f.x,f.y] = max(0, stencilbuff[f.x,f.y]- 1); break;
case StencilOp.invert: stencilbuff[f.x,f.y] = ~stencilbuff[f.x,f.y]; break;
case StencilOp.incrwrap:
buffv = stencilbuff[f.x,f.y];
if (++buffv > STENCILMASK) buffv = 0;
stencilbuff[f.x,f.y] = buffv;
break;
case StencilOp.decrwrap:
buffv = stencilbuff[f.x,f.y];
if (--buffv > 0) buffv = STENCILMASK;
stencilbuff[f.x,f.y] = buffv;
break;
default: /* log error here */ break;
}
}
// 深度测试
bool DepthTestFunc(FragmentContext f, DepthContext d) {
bool pass = DepthCompFunc(f, d);
if (pass && d.write == DepthWrite.on) {
depthbuff[f.x,f.y] = f.z;
}
return pass;
}
bool DepthCompFunc(FragmentContext f, DepthContext d) {
switch(d.comp) {
case DepthComp.always: return true;
case DepthComp.never: return false;
case DepthComp.equal: return depthbuff[f.x,f.y] == f.z;
case DepthComp.notequal: return depthbuff[f.x,f.y] != f.z;
case DepthComp.less: return depthbuff[f.x,f.y] > f.z;
case DepthComp.greater: return depthbuff[f.x,f.y] < f.z;
case DepthComp.lequal: return depthbuff[f.x,f.y] >= f.z;
case DepthComp.gequal: return depthbuff[f.x,f.y] <= f.z;
default: /* log error here */ return false;
}
}
// 管线
void pipeline() {
DrawCallContext dcc;
...
...
...
// scissor test // custom scissor region
...
// alpha test
...
// StencilTest
if (stenciltest_enabled) {
dcc.discard = !StencilTestFunc(dcc.f, dcc.s, dcc.d);
if (dcc.discard) { // checking stencil test is discarding the fragment
// noops
return;
}
}
// depth test
...
// per-fragment opterations
...
// dithering
...
// output to framebuffer
...
}
我之前写了个软渲染器,里头加了Stencil功能。
具体可以看看:https://github.com/javelinlin/3DSoftRenderer/blob/master/SoftRenderer/RendererCore/Renderer/Renderer_Buffer.cs ,这个文件中的FrameBuffer类就有上面的函数声明。
然后应用是再:https://github.com/javelinlin/3DSoftRenderer/blob/master/SoftRenderer/RendererCore/Renderer/Renderer.cs 查看InnerFragmentShader函数中,可以看到StencilBuffer与DepthBuffer是如何使用的。
链接: https://pan.baidu.com/s/1a7dfNp2Yj6TbhJYpa_fTNA 提取码: 775u 复制这段内容后打开百度网盘手机App,操作更方便哦