zoukankan      html  css  js  c++  java
  • UnityShader学习笔记- Stencil Buffer

    模板测试(Stencil Test)是现代渲染流水线的一环,其中涉及到的就是模板缓冲(Stencil Buffer),模板缓冲可以用来制作物体的遮罩、轮廓描边、阴影、遮挡显示等等效果

    为屏幕上的每一个像素保存一个8位的无符号整数,跟模板缓冲区进行比较并决定是否保留像素称为模板测试

    模板测试发生在透明度测试之后,深度测试之前

    img

    模板缓冲区默认值为0(测试得到),并且我推测模板缓冲区每帧执行完会进行一个刷新

    要加模板测试,就在Shader的Pass开头写Stencil{ }结构体。如果每个Pass都用,则可以提到外面

    Stencil 常见语法格式

    Stencil{
        Ref referenceValue          // 参考值 默认值为 0
        Comp comparisonFunction     // 定义参考值与缓冲值比较的方法 默认值为 Always
        Pass stencilOperation       // 定义当通过模板测试时,根据参考值对缓冲值的处理方法 默认值为 keep
        Fail stencilOperation       // 定义当没有通过模板测试时,根据参考值对缓冲值的处理方法 默认为 keep
        ZFail stencilOperation      // 定义当通过模板测试却没有通过深度测试时,根据参考值对缓冲值的处理方法 默认为 keep
    }
    

    举个实际例子

    Stencil{    
        Ref 1
        Comp Equal
        Pass Keep
     }
    

    上述代码的意思是: 我们自己设定了 Ref 参考值为 1。渲染 Pass 得到像素颜色后,拿参考值 1 与模板缓冲中此像素位置的缓冲值比对,只有 Equal 相等才算通过,并且 Keep 保持原有缓冲值,否则丢弃此像素颜色。

    关键字

    stencil{
    	Ref referenceValue
    	ReadMask  readMask
    	WriteMask writeMask
    	Comp comparisonFunction
    	Pass stencilOperation
    	Fail stencilOperation
    	ZFail stencilOperation
    }
    

    Ref

    Ref referenceValue
    Ref用来设定参考值referenceValue,这个值将用来与模板缓冲中的值进行比较。referenceValue是一个取值范围位0-255的整数。

    ReadMask

    ReadMask readMask
    ReadMask 从字面意思的理解就是读遮罩,readMask将和referenceValue以及stencilBufferValue进行按位与(&)操作,readMask取值范围也是0-255的整数,默认值为255,二进制位11111111,即读取的时候不对referenceValue和stencilBufferValue产生效果,读取的还是原始值。

    WriteMask

    WriteMask writeMask
    WriteMask是当写入模板缓冲时进行掩码操作(按位与【&】),writeMask取值范围是0-255的整数,默认值也是255,即当修改stencilBufferValue值时,写入的仍然是原始值。

    Comp

    Comp comparisonFunction
    Comp是定义参考值(referenceValue)与缓冲值(stencilBufferValue)比较的操作函数,默认值:always

    Pass

    Pass stencilOperation
    Pass是定义当模板测试(和深度测试)通过时,则根据(stencilOperation值)对模板缓冲值(stencilBufferValue)进行处理,默认值:keep

    Fail

    Fail stencilOperation

    Fail是定义当模板测试(和深度测试)失败时,则根据(stencilOperation值)对模板缓冲值(stencilBufferValue)进行处理,默认值:keep

    ZFail

    ZFail是定义当模板测试通过而深度测试失败时,则根据(stencilOperation值)对模板缓冲值(stencilBufferValue)进行处理,默认值:keep

    Comp,Pass,Fail 和ZFail将会应用给背面消隐的几何体(只渲染前面的几何体),除非Cull Front被指定,在这种情况下就是正面消隐的几何体(只渲染背面的几何体)。你也可以精确的指定双面的模板状态通过定义CompFront,PassFront,FailFront,ZFailFront(当模型为front-facing geometry使用)和ComBack,PassBack,FailBack,ZFailBack(当模型为back-facing geometry使用)

    自定义一些值

    img

    img

    Comp比较函数

    Greater Only render pixels whose reference value is greater than the value in the buffer.
    GEqual Only render pixels whose reference value is greater than or equal to the value in the buffer.
    Less Only render pixels whose reference value is less than the value in the buffer.
    LEqual Only render pixels whose reference value is less than or equal to the value in the buffer.
    Equal Only render pixels whose reference value equals the value in the buffer.
    NotEqual Only render pixels whose reference value differs from the value in the buffer.
    Always Make the stencil test always pass.
    Never Make the stencil test always fail.

    Operation

    Keep Keep the current contents of the buffer.
    Zero Write 0 into the buffer.
    Replace Write the reference value into the buffer.
    IncrSat Increment the current value in the buffer. If the value is 255 already, it stays at 255.
    DecrSat Decrement the current value in the buffer. If the value is 0 already, it stays at 0.
    Invert Negate all the bits.
    IncrWrap Increment the current value in the buffer. If the value is 255 already, it becomes 0.
    DecrWrap Decrement the current value in the buffer. If the value is 0 already, it becomes 255.

    模板测试判断依据

    和深度测试一样,在unity中,每个像素的模板测试也有它自己一套独立的依据,具体公式如下:

    if(referenceValue&readMask comparisonFunction stencilBufferValue&readMask)
    
    通过像素
    
    else
    
    抛弃像素
    

    Unity ShaderLab  模板缓存(Stencil Buffer) 基本概念

    轮廓描边

    img

    思路+Code

    两个Pass,第一个Pass正常渲染,第二个Pass把vertex沿着模型法线膨胀一点然后基于上一个Pass的模板缓冲区来剔除重叠部分

    Shader "Unlit/Edge"
    {
        Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
            _Color("Color",Color) = (1,1,1,1)
            _RefValue("Stencil RefValue",Int) = 0
            _Outline("OutLine Width",Range(0,1)) = 0.05
            _OutlineColor("OutLineColor",Color) = (0,0,0,1)
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            //stencil buffer if zero default and it will be reset at the end of one frame Render
            Stencil{
                Ref [_RefValue]
                Comp Equal
                Pass IncrSat
            }
            CGINCLUDE
             struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                    float3 normal:NORMAL;
                };
                struct v2f
                {
                    float2 uv : TEXCOORD0;   
                    float4 vertex : SV_POSITION;
                };
                #include "UnityCG.cginc"         
                sampler2D _MainTex;
                float4 _MainTex_ST;
                float4 _Color;
                float _Outline;
                float4 _OutlineColor;
            ENDCG
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag            
                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);
                    return col*_Color;
                }
                ENDCG
            }
             Pass
            {
                CGPROGRAM
                #pragma vertex _vert
                #pragma fragment _frag 
                v2f _vert (appdata v)
                {
                    v2f o;
                    v.vertex = v.vertex+float4(normalize(v.normal)*_Outline,1);
                    o.vertex = UnityObjectToClipPos(v.vertex);                        
                    return o;
                }
                fixed4 _frag (v2f i) : SV_Target
                {               
                    return _OutlineColor;
                }
                ENDCG
            }
        }
    }
    

    产生的问题

    1、边界交融:两个物体物体在屏幕上有z先后关系时相交部分不会有外轮廓线

    2、边界竞争:写入了模板缓冲区,并根据模板缓冲区进行剔除,摄像机位置变动,物体的渲染顺序发生变化,先谢了模板缓冲的物体会覆盖后了模板缓冲的物体的模型

    解决边界竞争的关键在于模型本体的渲染不能被模板缓冲区影响,所以两个Pass之间使用不同的Stencil测试,第一个Pass渲染本体并对模板缓冲区进行初始化,也就是把Comp设置成Always,第二个Pass做之前一样的模板测试

    第一个Pass

    Pass
            {
                Stencil{
                    Ref [_RefValue]
                    Comp Always
                    Pass IncrSat
                }
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                //...
                ENDCG
            }
    

    第二个Pass

     Pass
            {
                Stencil{
                    Ref 0
                    Comp Equal
                    Pass Keep
                }
                CGPROGRAM
                #pragma vertex _vert
                #pragma fragment _frag           
                //...
                ENDCG
                    
             }
    

    实现Unity遮罩的方法

    • 搞一个渲染队列靠前的平面,然后做模板缓冲区写入,后来的物体做模板测试就好
      • 如何初始化模板缓冲区,用一个渲染队列在前面的物体,调成Always
    • 使用透明物体的写深度方式

    非欧几里得空间

    那非欧几里得空间,又简单来说:违反现实三维空间几何规律的空间就可以认为是非欧几里得空间

    img

    每个面显示一个空间

    想要达成非欧几里得的效果,只需要如下设置:

    1. 一个面世界中,只有通过这个四边形面片(Quad),才能看到这个里面的三维物体(GameObjects)。
    2. 各个面世界不相互干扰,一个面只负责显示一个世界。

    遮罩的处理

    Quad Shader注意点

    1. 渲染顺序 Queue 标签,要比其他物体先渲染。
    2. 关闭 Zwrite 深度写入,否则后面的物体ZTest不过不会显示。

    image-20200522123234826

    多个面互相不干扰

    img

    要想让面世界之间互不干扰:你显示你的,我显示我的。就像上图所显示那样。

    其实很简单,只需要为每个面世界设置不同的 Ref 参考值就好了。

    比如左边显示圆球的面世界中,四边形面片(Quad)与其中的物体们(GameObjects)的参考值都设置为 1

    右边显示圆柱的面世界中,四边形面片(Quad)与其中的物体们(GameObjects)的参考值都设置为 2

    代码部分

    Mask

     Properties
        {
            _RefValue("Stencil Value",Int) = 1
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" "Opaque" = "Geometry-1"}     
            Pass
            {           
                 Stencil{
                    Ref [_RefValue]
                    Comp Always
                    Pass Replace
                }
                ZWrite Off
                ColorMask 0
                
                CGPROGRAM
                //...
                ENDCG
            }
        }
    

    Obj

     Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
            _Color("Color",Color) = (1,1,1,1)
            _RefValue("Stencil Value",Int) = 1
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" "Opaque" = "Geometry"}
            Pass
            {           
                 Stencil{
                    Ref [_RefValue]
                    Comp Equal
                    Pass Keep
                }
                
                CGPROGRAM
                //...
                ENDCG
            }
        }
    

    基于Stencil的镜面效果

    镜面效果往往需要额外创建一个摄像机,根据摄像机的图像反转位置来渲染镜子中的内容,利用stencil进行镜面区域限定,配合顶点镜面反转,也可以实现镜面效果

    如何反转?

    给镜子下建立一个子物体,子物体的某一条轴垂直镜面方向,然后把世界空间的物体变换到建立的子物体的空间下,再反转垂直的轴,即可形成虚像

    虚像的处理需要关闭深度测试,或者让他总是通过也行

    image-20200522153109933
    Quad物体就是Mirror,有一条轴垂直镜面的子物体WtoMW_Object:

    image-20200522153156996

    传送矩阵的工具物体

    子物体上挂载一个脚本,用于传送矩阵给材质

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    //Set World to Mirror World Matrix
    public class SetWtoMWMatrix : MonoBehaviour
    {
        //WtoMW_Object 的 transform;
        Transform refTransform;
        //”Wrold“ To ”MirrorWorld“ Matrix(世界转换到镜子世界的矩阵)
        Matrix4x4 WtoMW;
        public Material material;
        //Y 轴对称反转矩阵
        Matrix4x4 YtoNegativeZ = new Matrix4x4(
          new Vector4(1, 0, 0, 0),
          new Vector4(0, 1, 0, 0),
          new Vector4(0, 0, -1, 0),
          new Vector4(0, 0, 0, 1));
        private void Start()
        {
            //material采用拖拽赋值的形式
            refTransform = GameObject.Find("WtoMW_Object").transform;
        }
        void Update()
        {
            //模型的坐标,从世界空间转到镜子空间(本质就是把一个要镜像的物体变换到目前建立的子物体的空间上),再经由反转Y轴得到镜子空间的镜像,
            //反转Y轴是因为子物体的y轴即是镜面朝向,其实子物体哪个轴朝外反转到那个轴就行,然后把镜像再转换回世界坐标
            WtoMW = refTransform.localToWorldMatrix * YtoNegativeZ * refTransform.worldToLocalMatrix;
            material.SetMatrix("_WtoMW", WtoMW);
        }
    }
    

    MirrorObj

    对于要被镜子照到的物体我们需要形成虚像,所以需要两个Pass,一个虚像一个实像

    Shader "Unlit/MirrorObj"
    {
        Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
            _RefValue("Ref Value",Int) = 1
        }
        SubShader{     
            Tags { "RenderType"="Opaque" "Queue"="Geometry" }
            //渲染队列在后一点
            CGINCLUDE
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
                struct v2f
                {
                    float2 uv : TEXCOORD0;   
                    float4 vertex : SV_POSITION;
                    float3 worldNormal:TEXCOORD1;
                };
                #include "UnityCG.cginc"
                float4x4 _WtoMW; //矩阵
                sampler2D _MainTex;
                float4 _MainTex_ST;
            ENDCG
                
            //这里渲染虚像的 Pass
            Pass
            {
              Stencil{
                Ref [_RefValue]
                //由于stencil buffer默认是0,所以建议给个1,等于1时说明在镜面区域内,则可以显示虚像
                Comp Equal
                Pass keep
                ZFail keep
              }
              ZTest Off
              Cull Front //镜面显示背面而不显式正面
              
              CGPROGRAM
               #pragma vertex vert
               #pragma fragment frag
              //顶点函数  
              v2f vert (appdata v)
              {
                v2f o;
                //首先将模型顶点转换至世界空间坐标系
                float4 worldPos = mul(unity_ObjectToWorld,v.vertex);
                //再把顶点从世界空间转换至镜子空间
                float4 mirrorWorldPos = mul(_WtoMW,worldPos);
                //最后就后例行把顶点从世界空间转换至裁剪空间
                o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);          
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
              }
              //frag 函数和实体的是一样的..
              fixed4 frag (v2f i) : SV_Target
              {
                 fixed4 col = tex2D(_MainTex, i.uv);
                 return col;
              }
              ENDCG
            }
            //这里渲染实体的 Pass
            Pass
            {
              CGPROGRAM
               // ...
              ENDCG
    
            }
        }
    }
    

    Mirror

    没什么好说的,就模板缓冲区初始化,然后搞成透明的

    Shader "Unlit/Mirror"
    {
      Properties
      {
        _MainTex ("Texture", 2D) = "white" {}
        _RefValue("Ref Value",Int) = 1
        _Color("Color Tint",Color) = (0,0,0,1)
      }
      SubShader
      {
         //注意渲染队列
        Tags { "RenderType"="Opaque" "Queue"="Geometry-1" } 
        Stencil{
          Ref [_RefValue]
          Comp Always
          Pass Replace
        }//所谓模板缓冲区初始化
        Pass{
          //这里镜子的正常渲染(默认我使用 Unlit 的代码
                ZWrite Off
                ColorMask 0
                //不让他往颜色缓冲区写东西,这样就是一个透明的镜子了
                CGPROGRAM
                //不写主要流程也没关系,想给镜子写点光照反射就写,然后记得把上面的ColorMask 0去掉
                ENDCG
        }
      }
    }
    

    img
    [文中案例来自INDIENOVA阿创]: https://indienova.com/u/1149119967

  • 相关阅读:
    推荐书单
    图解Android
    图解Android
    图解Android
    图解Android
    图解Android
    图解Android
    个人博客平台 http://craft6.cn 上线
    数据库设计教程系列——相关知识点整理
    O2O研究系列——O2O知识思维导图整理
  • 原文地址:https://www.cnblogs.com/FlyingZiming/p/12937642.html
Copyright © 2011-2022 走看看