模板测试
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer),我们可以在渲染的时候更新它来获得一些很有意思的效果。
模板缓冲中的模板值(Stencil Value)通常是8位的,因此每个片段/像素共有256种不同的模板值(译注:8位就是1字节大小,可表示256个不同的值,因此和char的容量一样是256个不同值)。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。
工作过程 > 需要每次迭代前清除模板缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);- 启用模板缓冲的写入。
glEnable(GL_STENCIL_TEST); - 渲染物体,更新模板缓冲的内容。
glStencilMask, eg:
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样 glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)- 禁用模板缓冲的写入。
- 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。
- 启用模板缓冲的写入。
模板函数 > 和深度测试一样,我们对模板缓冲应该通过还是失败,以及它应该如何影响模板缓冲,也是有一定控制的。一共有两个函数能够用来配置模板测试:
glStencilFunc和glStencilOp。glStencilFunc(GLenum func, GLint ref, GLuint mask)一共包含三个参数 * func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。 * ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。 * mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。
但是glStencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,而不是我们应该如何更新缓冲。这就需要glStencilOp这个函数了。 glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项,我们能够设定每个选项应该采取的行为: * sfail:模板测试失败时采取的行为。 * dpfail:模板测试通过,但深度测试失败时采取的行为。 * dppass:模板测试和深度测试都通过时采取的行为。 * 一般参数默认值 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
行为 描述 GL_KEEP 保持当前储存的模板值 GL_ZERO 将模板值设置为0 GL_REPLACE 将模板值设置为glStencilFunc函数设置的ref值 GL_INCR 如果模板值小于最大值则将模板值加1 GL_INCR_WRAP 与GL_INCR一样,但如果模板值超过了最大值则归零 GL_DECR 如果模板值大于最小值则将模板值减1 GL_DECR_WRAP 与GL_DECR一样,但如果模板值小于0则将其设置为最大值 GL_INVERT 按位翻转当前的模板缓冲值 物体轮廓, 为物体创建轮廓的步骤如下: > OpenGL的指令开启或关闭,直到下次改变前的渲染都是同一个状态值。
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,此后物体的片段被渲染时,将模板缓冲更新为1。
cpp glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilMask(0xFF); - 渲染物体。 此时会得到之前物体作为模板缓冲。
cpp //.... glDrawArrays(GL_TRIANGLES, 0, 36); - 禁用模板写入以及深度测试。
GL_NOTEQUAL只绘制模板之外的地方cpp glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilMask(0x00); //首先深度测试不需要,另外开启的话会造成`深度冲突` glDisable(GL_DEPTH_TEST); - 将每个物体缩放一点点。 绘制纯色的物体。
cpp glDrawArrays(GL_TRIANGLES, 0, 36); - 使用一个不同的片段着色器再次绘制物体,输出一个单独的(边框)颜色。但只在它们片段的模板值不等于1时才绘制。 '绘制的纯色物体和模板进行测试'
- 再次启用模板写入和深度测试。
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,此后物体的片段被渲染时,将模板缓冲更新为1。
主要代码
cpp //1. 清除深度缓冲和模板缓冲 { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // don't forget to clear the stencil buffer! } outlineShader.use(); glm::mat4 model = glm::mat4(1.0f); glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)wWidth / (float)wHeight, 0.1f, 100.0f); // ---- 基础设置 { //设置outlineShader的VP矩阵 { outlineShader.setMat4("view", view); outlineShader.setMat4("projection", projection); } //设置shader的VP矩阵 { shader.use(); shader.setMat4("view", view); shader.setMat4("projection", projection); } } //2. 绘制Plan,并设置为不影响模板(禁用模板写入) { glStencilMask(0x00); glBindVertexArray(planeVAO); glBindTexture(GL_TEXTURE_2D, floorTexture); shader.setMat4("model", glm::mat4(1.0f)); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); } //3.绘制两个箱子,并且将箱子更新覆盖为模板缓冲(开启模板,并模板测试设置为GL_ALWAYS) { glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilMask(0xFF); glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, cubeTexture); model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f)); shader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f)); shader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); } //4. 绘制放大后的箱子,不绘制原有箱子的位置,并且不刷新模板缓冲,"关闭深度测试"(关闭模板写入,并模板测试设置为GL_NOTEQUAL,深度测试没有用此时关闭它) { glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilMask(0x00); glDisable(GL_DEPTH_TEST); outlineShader.use(); float scale = 1.1; // cubes glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_2D, cubeTexture); model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f)); model = glm::scale(model, glm::vec3(scale, scale, scale)); outlineShader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f)); model = glm::scale(model, glm::vec3(scale, scale, scale)); outlineShader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); } //5. 最后重置选项 { glBindVertexArray(0); glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 0, 0xFF); glEnable(GL_DEPTH_TEST); }UnityShader 另类描边实现 (不使用模板和normal) > 两次绘制,纯色绘制
放大+ 偏移cpp Shader "Unlit/StencilTest" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 pass { CGPROGRAM #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 { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } //Outline pass Pass { name "Outline" CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct v2f { UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; v2f vert (float4 vertex : POSITION) { v2f o; // 缩放系数`1.2`可调 float3 vvertex = vertex.xyz*1.2 - (ObjSpaceViewDir(vertex).xyz); o.vertex = UnityObjectToClipPos(float4(vvertex, vertex.w)); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(0.7, 0.85, 0.3, 1); } ENDCG } } }