Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边

文章目录

  • 镂空/遮罩
    • 镂空
      • Masker shader:
      • 再看看Be Masked被镂空的shader:
      • 注意绘制顺序的问题
      • 运行效果
    • 遮罩
      • 看看Be Masked被遮罩的shader:
      • 运行效果
      • 看一下绘制顺序的问题
  • 描边
    • 描边思路
      • 整体效果
      • 运行效果
        • 描边有透视的
        • 描边无透视的
  • 便于理解模板缓存的伪代码
  • 上面的伪代码的验证的地方
  • Project
  • References

关于Unity的模板测试介绍,可查看之前翻译的一篇:
Unity Shader - ShaderLab: Stencil 模板(缓存)


以下的例子实现方式有多种多样,这里为了做示例测试,制作其一一种。


镂空/遮罩

镂空

  • Masker:确定屏幕空间中,需要镂空的像素的位置,通常绘制对应形状的图元就好,将模板缓存值设置为1(ref 1,comp always(comp always是default值,可以不写,为了方便理解而写上),pass replace),不输入任何颜色(colormask 0)。
  • Be Masked:绘制实体对象,只要是模板缓存值为1的,我们都不绘制(ref 1,comp notequal,pass keep(pass keep是default值,可以不写,为了方便理解而写上))

Masker shader:

// 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处理单元就会直接闭塞等待(完成,且等待:意思就不会再往渲染光线走了)其他片段的操作完成

再看看Be Masked被镂空的shader:

// 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标签值删掉的情况。

绘制顺序的问题,可以理解为:绘制的对象需要模板数据来做效果时,先确保模板缓存已有写入你想要的数据,所以先将写入模板缓存的对象先绘制,接着再绘制应用模板缓存的对象

运行效果

Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第1张图片

下面再看看遮罩的情况

遮罩

  • Masker:和上面的“镂空”的Masker一样,我们这个例子中就是同一样对象。
  • Be Masked:绘制实体对象,我们只绘制模板缓存值为1的,(ref 1,comp equal,pass keep(pass keep是default值,可以不写,为了方便理解而写上))。

看看Be Masked被遮罩的shader:

// 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的值时,才通过模板测试,而且通过后,保持模板缓存的值不变

运行效果

Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第2张图片

看一下绘制顺序的问题

Masker得shader代码中,将Subshader的tags中"Queue"=“Geometry-1"的”-1"删除掉,或是整个"Queue"的键值对删除。下面是运行异常的情况
Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第3张图片

可以看到镂空的效果突然出现,突然消失。
这是因为Unity在渲染不透明的Geometry队列的渲染对象是,排序是从前到后的(从靠近的到远离的渲染),因为慢慢的将Masker往屏幕拉近,所以Masker的渲染顺序突然提前了(可能是按物体质心点来算位置的)。这是不希望得到的结果。

所以一定要留意使用Stencil时,确保你需要比较用的模板缓存值已写入。

描边

稍微与上面的一个渲染对象镂空多个不同渲染对象的方式不一样,因为描边效果是在一个shader里,多个pass实现的。这种方式有利有弊。

  • 利:使用方便,不用太考虑绘制顺序的问题,因为绘制的第一个pass,我们就将缓存写入了,接着第二个pass用的时候就已经有模板缓存数据了
  • 弊:会打断draw call的batching合批。

描边思路

描边思路,我们这儿只使用其一一种,有很多种方式处理描边的。

这儿使用的是:

  • 第一个pass先绘制提示对象,并标记上stencil缓存值为1。
  • 第二个pass先将对象空间下的顶点,按法线方向挤出(就是objSpacePos += objSpaceNormal * offset),然后Stencil比较只允许不等于1值的,那就剩下边缘挤出的那部分片段区域了。

整体效果

Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第4张图片

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

运行效果

Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第5张图片
可调整描边边宽,和描边颜色

描边有透视的

Unity Shader - 模板简单测试 - 实现镂空/遮罩、描边_第6张图片
边框随深度变大而变小

描边无透视的


边框与深度无关系

便于理解模板缓存的伪代码

(下面的吐槽一下,由于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是如何使用的。

Project

链接: https://pan.baidu.com/s/1a7dfNp2Yj6TbhJYpa_fTNA 提取码: 775u 复制这段内容后打开百度网盘手机App,操作更方便哦

  • 镂空/遮罩的测试场景为:Mask.unity
  • 描边测试场景为:Outline.unity

References

  • Unity Shader-描边效果 这篇博文的描边将得很好,推荐看看
  • Unity Shader - ShaderLab: Stencil 模板(缓存) 这个是之前翻译的

你可能感兴趣的:(unity,unity-shader,Unity,Stencil,Test,Unity,Stencil描边,Unity,Stencil镂空,Unity,Stencil遮罩)