Unity Shader学习随笔

发布时间 2023-12-26 11:11:11作者: 被迫吃冰淇淋的小学生

阴影:

光源看不到,但相机看得到的地方,就是阴影

变体:

一个普通的Shader可能会有很多种效果

例如一个火焰溶解效果,写在Shader里,但其实在未触发之前我们不需要去计算该效果

因此需要在未触发前,将火焰溶解的效果计算关闭

这就用到了变体,把火焰溶解的效果计算变成变体

无论如何都会被编译的变体multi_compile

#pragma multi_compile _ _NAME

变体名必须要全大写

Properties
{  //在属性面部设置一个开关,来表示是否要启用变体(需求全大小)
  [Toggle]_DISSLOVEEABLED("Disslove Eabled", int) = 0
}
SubShader
{
  pass
  {
    ...
    //在pass中的命名,前面的‘_’表示空变体,之所以有这个是为了不默认开启 _DISSLOVEEABLED 变体
    #pragma multi_compile _ _DISSLOVEEABLED_ON
    ...
    fixed4 func()
    {
      ...
      #if _DISSLOVEEABLED_ON
                       fixed4 dis = tex2D(_Dissolve, i.uv.zw);
                       fixed weight = dis.r - _Clip;
                       clip(weight);
                       if(weight < _Clip) {
                         tex = tex * lerp(_Color0, _Color1, weight/_Scope);
                      }
                  #endif
      ...
    }
  }
 
}

通过材质使用情况来决定是否进行编译的变体shader_feature

一般用于特效,即运行过程中不会去开启该变体

属性中就是要暴露一个开关:

[Toggle]_MaskEnabled("启用遮罩", int) = 0

pass中要启用变体:

#pragma shader_feature _MASKENABLED_ON

然后就是和上面的判断一样了

#if

  xxx;

#else

  xxx;

#endif

这就可以做到:

同一个Shader可以给不同的材质球使用;

是否开启某个变体由材质球决定,并且在打包时不会将该材质球未开启的变体打进包里。

然后信息存在于这里

常用函数:

abs(x) 绝对值
frac(x) 取小数
floor(x) 向下取整
ceil(x) 向上取整
max(x, y) 取大的
min(x, y) 取小的
pow(x, y) x的y次方
rcp(x) x的倒数
exp(x) e的x次方
exp2(x) 2的x次方
fmod(x, y) x%y
saturate(x) 将x限制在0~1
clamp(x, a, b) 将x限制在a~b
sqrt(x) 对x开方
rsqrt(x) 对x开方后取倒数
lerp(x, y, a) x+(y-x)*a
sin(x)
cos(x)
distance(x, y) 返回xy之间的距离(几维都可以)
length(x) 返回模长(必须是二维及以上)
step(x, y) 返回 x >=y ?1 : 0
smoothstep(a, b, x) 返回介于0和1之间的平滑 Hermite 内插

 

渲染队列Queue

2500以下会被认为是不透明物体,从前往后渲染,效率更高

2500以上就是透明物体,从后往前渲染,效率低,但这是为了保证显示效果正确

Queue=Background,1000,最先渲染

Queue=Geometry,2000,默认场景中的渲染对象

Queue=AlphaTest,2450,要么不透要么全透,常常用来实现美术部分的透贴

Queue=Transparent,3000,场景中的半透明对象

Queue=Overlay,4000,叠加效果,最后渲染的东西放这,如镜头光晕等

混合Blend

Blend后面一般跟随两至三个参数

Blend One Zero

Blend One One

第一个参数就是针对片段着色器pass计算出来的SrcFactor源颜色,要对该参数干什么

第二个参数就是针对存在于帧缓冲区的DstFactor目标颜色,要对该参数干什么

其实中间还有一个,不填就是默认加法

Blend SrcAlpha OneMinusSrcAlpha - 传统透明度

Blend One OneMinusSrcAlpha - 自左乘透明度

Blend One One

Blend OneMinusDstColor One - 软合并

Blend DstColor Zero - 乘法

Blend DstColor SrcColor - 双倍乘法

面剔除Cull

剔除不需要的面,优化渲染速度

 

Cull Front - 剔除正面

Cull Back - 剔除背面

 

正面背面是指模型的正面背面(顶点环绕方向,及法线),不是玩家看到的是正面

Cull Off - 关闭剔除

屏幕后处理

实现热扭曲的效果

1.做好场景

2.由代码抓取当前一帧的内容

3.获取受扭曲影响的部分屏幕坐标

4.利用屏幕坐标对抓取的图片采样

5.再采样扰动贴图做扭曲

使用unity参数组成屏幕坐标

查看代码
 Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct v2f
    {
        float2 uv : TEXCOORD0;
    };

    sampler2D _GrabTex;
    sampler2D _MainTex;float4 _MainTex_ST;
    fixed _SpeedX, _SpeedY, _Distort;

    v2f vert (float4 vertex : POSITION, float2 uv : TEXCOORD0, out float4 pos : SV_POSITION)
    {
        pos = UnityObjectToClipPos(vertex);
        v2f o;
        o.uv = TRANSFORM_TEX(uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x;
        return o;
    }
    //由于VPOS和SV_POSITION冲突,因此需要在vert里out
    fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos:VPOS) : SV_Target
    {
        //最初的版本,通过unity参数去计算,_ScreenParams.xy是屏幕的宽高
        fixed2 uv = lerp(screenPos.xy / _ScreenParams.xy, tex2D(_MainTex, i.uv).xy, _Distort);
        fixed4 grabTex = tex2D(_GrabTex, uv);
        return grabTex;
    }
    ENDCG
}

使用正交空间下的pos计算屏幕坐标

查看代码
 Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct a2f
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
    };

    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 pos : SV_POSITION;
        float2 screenUV : TEXCOORD1;
    };

    sampler2D _GrabTex;
    sampler2D _MainTex;float4 _MainTex_ST;
    fixed _SpeedX, _SpeedY, _Distort;

    v2f vert (a2f v)
    {
        v2f o;
        o.uv = TRANSFORM_TEX(v.uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x;
        o.pos = UnityObjectToClipPos(v.vertex);
        //1.此时pos是放在正交矩形中的顶点
        //2.因此可以直接拿该值当做屏幕坐标
        //3.但要注意pos的范围是-1到1
        //4.因此需要把它规范到0到1,但OpenGL和DX11的屏幕坐标原点不同
        //5.OpenGL的在左下角,仅需将xy * 0.5 + 0.5即可
        //6.但DX11的在左上角,需要将y轴反转,即y = 1 - y
        // o.screenUV = o.pos.xy / o.pos.w * 0.5 + 0.5;
        // o.screenUV.y = 1 - o.screenUV.y;
        //7.上面就是根据正交中的顶点坐标转换成屏幕坐标的操作
        //8.可以注意到稍微有点扭曲,原因是顶点不是片元,顶点和顶点之间的参数都是靠插值算出来的
        // o.screenUV = ComputeScreenPos(o.pos) / o.pos.w;
        //9.当然,unitycg提供了方法,上面
        //10.这里除以一个w是因为:正交矩形中是正交的,而相机是透视的,要将其还原
        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
        fixed2 uv = lerp(i.screenUV, tex2D(_MainTex, i.uv).xy, _Distort);
        fixed4 grabTex = tex2D(_GrabTex, uv);
        return grabTex;
    }
    ENDCG
}

使用片元计算后的pos计算屏幕坐标

查看代码
 Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct a2f
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
    };

    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 pos : SV_POSITION;
        float2 screenUV : TEXCOORD1;
    };

    sampler2D _GrabTex;
    sampler2D _MainTex;float4 _MainTex_ST;
    fixed _SpeedX, _SpeedY, _Distort;

    v2f vert (a2f v)
    {
        v2f o;
        o.uv = TRANSFORM_TEX(v.uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.screenUV = o.pos;
        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
        // return i.screenUV.x;
        // return i.pos.x;
        //可以看到,我明明对screenUV赋了pos的值,但上面两个居然不一样
        //那是因为SV_POSITION在中间经过了改变
        //在经过计算后,pos就是屏幕像素大小了,不再是-1到1了,那这样就更方便了;
        i.screenUV = i.pos.xy / _ScreenParams.xy;
        //OK,就上面这一行就解决战斗了
        fixed2 uv = lerp(i.screenUV, tex2D(_MainTex, i.uv).xy, _Distort);
        fixed4 grabTex = tex2D(_GrabTex, uv);
        return grabTex;
    }
    ENDCG
}

[PerRendererData]

属性的特性

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    [PerRendererData]_Color ("Color", Color) = (0,0,0,0)
}

该材质cs被调用时,会被打包进cs 的MaterialPropertyBlock

每帧修改一万个对象的颜色,

方法1,使用材质的方法去修改颜色,开销是16ms每帧

方法2,使用GetPropertyBlock(MaterialPropertyBlock prop)的方法修改颜色,开销是7ms每帧

Color

模板测试

模板缓冲区

自己的话

是屏幕大小的区域,例如1920*1080的大小

每个像素是8位

当某一个物体被渲染到屏幕上时,它看到带有材质,也就是肯定带有shader

而shader中可以定义,当前我被渲染出来的区域的模板值

因此,当另一个物体被渲染时,另一个shader上面的也会有模板值

两个模板值比较,新的模板值大会怎么样,小会怎么样,可以自己定义

因此就可以做出类似PS遮罩的效果

官方的话

模板缓冲区可以为屏幕上的每个像素点保存一个无符号的整数值

这个值的意义视程序的具体操作而定

在渲染过程中

可以用这个值与一个预先设定的参考值相比较

根据比较的结果来决定是否更新相应的像素点的颜色值,这个比较的过程被称为模板测

代码部分

公式

将StencilBuffer的值与ReadMask进行与运算,然后与Ref值进行Comp比较,结果为True时进行Pass操作,否则进行Fail操作

操作值写入StencilBuffer前先与WriteMask进行与运算

(Ref & ReadMask)Comp(StencilBuffer & ReadMask)? Pass : Fail

Comp

Less    <

Greater  >

Lequal  <=

Gequal  >=

Equal   =

NotEqual  !=

Always  总为true

Never  总为false

实践

UI组件加了Mask之后

会有这个,这是由UGUI自己去写的

Id就是公式中的StencilBuffer ,已经被写到深度缓冲区中了

我们写的新shader就是要和这个Id:1去比

Properties
{
    [PerRendererData]_MainTex ("Texture", 2D) = "white" {}
    _Color ("Color", Color) = (0,0,0,0)
    _Ref("Stencil Ref", int) = 0
    [Enum(UnityEngine.Rendering.CompareFunction)]_Comp("Stencil Comp", int) = 0
    [Enum(UnityEngine.Rendering.StencilOp)]_OP("Stencil OP", int) = 0
}
Stencil//深度测试
{
    Ref [_Ref]
    //ReadMask [0~255]
    //WriteMask [0~255]
    Comp [_Comp]
    Pass [_OP]
}

这就是Enum

此时_Ref = 1

这是Mask

这是效果