unity-shader(中级)

1. 更复杂的光照

1. Unity 渲染路径

  • Forward
  • Deferred
  • Legacy Vertex Lit

2. LightMode

  1. LightMode 用于定义该Pass通道在Unity渲染流水中的角色,以下是它可供选择的入参。

    标签名 描述
    Always 不管使用哪种渲染路径,该pass总会被渲染,但不会计算任何光照
    ForwardBase 用于前向渲染。该pass会计算环境光,最重要的平行光,逐顶点/SH光源和Lightmaps
    ForwardAdd 用于前向渲染。该pass会计算额外的逐像素光源,每个pass对应一个光源。
    Deferred 用于延迟渲染。该pass会渲染G缓冲(G-buffer)
    ShadowCaster 把物体的深度信息渲染到阴影映射纹理或一张深度纹理中
    PrepassBase (已过时)延迟渲染。该pass会渲染法线和高光反射的指数部分。
    PrepassFinal (已过时)延迟渲染。该pass会通过合并纹理,光照和自发光来渲染得到最后颜色
    Vertex, VertexLMRGBM和VertexLM (已过时)顶点照明渲染
  2. LightMode的渲染模式和摄像机参数的渲染路径息息相关。当使用一种渲染方式的时候,另一种是看不见的。

  3. 所有光源都有一个选项名为:渲染模式,这意味着光照采用哪种方式进行渲染。
    当光源设置为Import时,是逐像素光源。(不受限制与质量设置里面pixel light count )(Forward Add)
    设置 重要 时:光源会强制作用于前向渲染,计算额外的逐像素光源(ForwardAdd)。
    设置非重要时:光源会计算额外的逐顶点光源(Forward Base)。
    设置 自动 时:其重要光源个数限制取决于 项目设置->质量->像素光源数量 设置的值。

  4. 最重要的平行光为逐像素光源。(Forward Base)

  5. unity默认要求逐顶点光源不超过4个,超过的按SH(球谐函数【环境光】)光源处理

3. Stencil(模板测试/蒙版测试)

1. 概述

与深度测试,透明度测试类似,决定一个片元是否被扔掉。深度测试的比较数据在深度缓冲中,透明度测试的比较对象是颜色缓冲中的值,而模版测试的比较数据在Stencil中,并且模板测试要先于深度测试与透明度测试,在fragment函数之前就会执行模板测试。

参数 描述
Ref 就是参考值,当参数允许赋值时,会把参考值赋给当前像素
ReadMask 对当前参考值和已有值进行mask操作,默认值255,一般不用
WriteMask 写入Mask操作,默认值255,一般不用
Comp 比较方法。是拿Ref参考值和当前像素缓存上的值进行比较。默认值Always
Pass 当模版测试和深度测试都通过时,进行处理
Fail 当模版测试和深度测试都失败时,进行处理
ZFail 当模版测试通过而深度测试失败时,进行处理

参数:

  1. Comp :
    Always
    Greater - 大于
    GEqual - 大于等于
    Less - 小于
    LEqual - 小于等于
    Equal - 等于
    NotEqual - 不等于
    Always - 永远通过
    Never - 永远通不过
  2. pass,Fail,ZFail:
    Keep 保持(即不把参考值赋上去,直接不管)
    Zero 归零
    Replace 替换(拿参考值替代原有值)
    IncrSat 值增加1,但不溢出,如果到255,就不再加
    DecrSat 值减少1,但不溢出,值到0就不再减
    Invert 反转所有位,如果1就会变成254
    IncrWrap 值增加1,会溢出,所以255变成0
    DecrWrap 值减少1,会溢出,所以0变成255
2. 示例
  1. ZTest使得对象在所有对象最上方,修改_refVal来决定前后渲染层级,_refVal越大的材质渲染越靠上
Tags
{
    
    
    "LigehtMode" = "ForwardBase"
    "Queue" = "2"
}
ZTest Always

Stencil
{
    
    
    Ref[_refVal]
    // 比较成功条件,大于等于
    Comp GEqual
    // 条件成立 参考值赋给当前像素 stencil enable
    Pass Replace
}
  1. 设置对象不同的Queue层级,层级低的拥有更高的_refVal,先行写入Stencil,这样层级高的对象因为Stencil的_refVal没有缓存值高,而无法正常写入。
    设置层级低的对象ColorMask 0可实现类似镂空的效果

镂空体

Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry+1" }
LOD 100

Pass
{
    
    
    Tags {
    
     "LigehtMode"="ForwardBase" }
    ColorMask 0
    Stencil
    {
    
    
        Ref[_refVal]
        Comp GEqual
        Pass Replace
    }
...
Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry+2" }
LOD 100

Pass
{
    
    
    Tags {
    
     "LigehtMode"="ForwardBase" }
    Stencil
    {
    
    
        Ref[_refVal]
        Comp GEqual
        Pass Replace
    }
...
  1. 设置Stencil中的Comp为Equal,并将层级设置的更高。这样该对象只能在先行写入的Stencil且_refVal与该对象相同的对象中留下裁剪图像。
    可实现类似蒙版的效果。

被蒙版

Tags {
    
     "RenderType"="Opaque" "Queue" = "Geometry+1"}
LOD 100

Pass
{
    
    
    Tags {
    
     "LigehtMode" = "ForwardBase"}
    Stencil
    {
    
    
        Ref[_refVal]
        Comp Equal
        Pass Replace
    }

蒙版

Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry" }
LOD 100

Pass
{
    
    
    Tags {
    
     "LigehtMode"="ForwardBase" }
    Stencil
    {
    
    
        Ref[_refVal]
        Comp Always
        Pass Replace
    }

4. 前向渲染

1. 概述

在这里插入图片描述
如图所示,7个点光源中只有4个ForwardAdd,1个ForwardBase

在这里插入图片描述

2. 实现原理
  1. 作为首要的逐像素光源渲染,需要新建一个Pass通道,单独实现像素光源渲染。并设置Tags { "LightMode"="ForwardAdd" }
  2. Additional Pass中开启了混合模式,如果不开启就会替换之前的渲染。通常使用Blend One One
  3. 在Pass通道中定义#pragma multi_compile_fwdadd以让该通道可以接受除世界平行光外其他的光源,光照衰减值等。该Pass通道的光源默认情况下是没有阴影的。可以改用#pragma multi_compile_fwdadd_fullshadows来打开阴影。
  4. 默认光是不会随着距离增加而减小光强,因此需要一组函数来判断光强。
    • 引入库:#include "AutoLight.cginc"
    • LIGHTING_COORDS(x, y):它同时声明了用于阴影纹理采样坐标(_ShadowCoord)和用于衰减纹理采样坐标(_LightCoord2)。参数(x, y)指的是TEXCOORD(x, y)
    • TRANSFER_VERTEX_TO_FRAGMENT(o);:与宏LIGHTING_COORDS协同工作,它会根据该pass处理的光源类型( spot 或 point 或 directional )来计算光源坐标的具体值,以及进行和 shadow 相关的计算等。主要计算光源与该对象之间的距离,方便在LIGHT_ATTENUATION函数中判断光照衰减。注意:参数o中语义为SV_POSITION的变量必须命名为pos,因为辅助函数TRANSFER_VERTEX_TO_FRAGMENT在进行阴影坐标计算的时候,会指定调用该变量。
    • LIGHT_ATTENUATION(i);:与宏LIGHTING_COORDS协同工作,计算光照衰减值。
  5. 在原Pass通道中,实现逐顶点光源和球谐函数【环境光】。除了设置标签Tags { "LightMode"="ForwardBase" }外,还需定义#pragma multi_compile_fwdbase以让该通道可以接受除世界平行光外其他的光源。
  6. 计算时,需在顶点着色器中判断为LIGHTMAP_OFF(光照渲染被关闭,被认定为是不重要的光源时)和VERTEXLIGHT_ON(开启逐顶点光源)
  7. 使用ShadeSH9(float4(v.normal, 1.0));球谐函数生成柔和的环境光照,取代常规环境光。若LIGHTMAP_OFF未通过则使用常规环境光。
  8. 计算4个逐顶点光源Shade4PointLights(lpos, lpos, lpos, lrgb, lrgb, lrgb, lrgb,latten, pos, normal);
2. 示例:
Tags {
    
     "RenderType"="Opaque" }
LOD 100

Pass
{
    
    
    Tags {
    
     "LightMode"="ForwardBase" }
    CGPROGRAM
    // 只有使用了这样的指令,才可以在相关的pass中得到其他光源的光照变量,例如光照衰减值等。
    #pragma multi_compile_fwdbase
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"
    
    fixed4 _Diffuse;
    fixed4 _Specular;
    fixed _Gloss;

    struct v2f
    {
    
    
        float4 vertex : SV_POSITION;
        float3 worldNormal : TEXCOORD0;
        float3 worldPos : TEXCOORD1;
        float3 vertexLight : TEXCOORD2;
    };

    v2f vert (appdata_base v)
    {
    
    
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);

        // 宏判断,当光照渲染被关闭(被认定为是不重要的光源时)
        #ifdef LIGHTMAP_OFF
            // 球谐函数,可用于生成柔和的环境光照,取代常规环境光。
            float3 shLight = ShadeSH9(float4(v.normal, 1.0));
            o.vertexLight = shLight;
            // 宏判断,当开启逐顶点光源
            #ifdef VERTEXLIGHT_ON
                // 4个逐顶点光源
                fixed3 vertexLight = Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, 
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, o.worldPos, o.worldNormal
                    );
                o.vertexLight += vertexLight;
            #endif
        #else
            // 环境光
            o.vertexLight = UNITY_LIGHTMODEL_AMBIENT.xyz;
        #endif

        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    
    
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        // 漫反射
        fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLightDir));
        // 高光
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed3 halfDir = normalize(worldLightDir + viewDir);
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfDir)), _Gloss);
        return fixed4(diffuse + specular + i.vertexLight, 1);
    }
    ENDCG
}

Pass
{
    
    
    Tags {
    
     "LightMode"="ForwardAdd" }
    // 开启了混合模式,如果不开启就会替换之前的渲染
    Blend One One
    CGPROGRAM
    #pragma multi_compile_fwdadd
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"
    #include "AutoLight.cginc"

    fixed4 _Diffuse;
    fixed4 _Specular;
    fixed _Gloss;

    struct v2f
    {
    
    
        // 该变量必须命名为pos,因为辅助函数TRANSFER_VERTEX_TO_FRAGMENT在进行阴影坐标计算的时候,会指定调用该变量
        float4 pos : SV_POSITION;
        float3 worldNormal : TEXCOORD0;
        float3 worldPos : TEXCOORD1;
        // 它同时声明了用于阴影纹理采样坐标(_ShadowCoord)和用于衰减纹理采样坐标(_LightCoord2)。(2, 3)指的是TEXCOORD(2, 3)
        LIGHTING_COORDS(2, 3)
    };

    v2f vert (appdata_base v)
    {
    
    
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        // 与宏LIGHTING_COORDS协同工作,它会根据该pass处理的光源类型( spot 或 point 或 directional )来计算光源坐标的具体值,以及进行和 shadow 相关的计算等。主要计算光源与该对象之间的距离,方便在LIGHT_ATTENUATION函数中判断光照衰减。
        TRANSFER_VERTEX_TO_FRAGMENT(o);
        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    
    
        // 未必所有的光都是平行光,因此必须要使用该函数确保获取到正确的光源方向
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLightDir));
        // 高光
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed3 halfDir = normalize(worldLightDir + viewDir);
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfDir)), _Gloss);

        // 与宏LIGHTING_COORDS协同工作,计算光照衰减,默认光不会随着距离增加而减小光强,因此需要该函数减小光强。
        fixed atten = LIGHT_ATTENUATION(i);
        return fixed4((diffuse + specular) * atten, 1);
    }

    ENDCG
}
3. 补充点
  1. Base Pass中支持一些光照特性,如lightmap。
  2. 环境光和自发光是在Base Pass中计算的。
  3. 前向渲染一般会定义一个Base Pass(除双面渲染等)以及一个Additional Pass。一个Base Pass仅会执行一次,而一个Additional Pass会根据影响该物体的其他逐像素光源数目被多次调用,即每个逐像素光源会执行一次Additional Pass.
  4. ForwardAdd这个Pass需要和ForwardBase一起使用,否则会被Unity忽略掉(unity5.x),在新版本中,不会忽略,但是渲染会出错。
  5. 在Forward和Deferred渲染路径下,Forward的Pass均能被正常渲染。
  6. ForwardAdd对每一个逐像素光源都会执行一次Pass,所以不要在ForwardAdd里面计算 unity_4LightPos[x,y,z]0中的数据。会重复计算。

4. 延迟渲染

1. 概述
  1. 延迟渲染主要包含了两个Pass。
    在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。
    在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫发射系数等,进行真正的光照计算。

  2. 缺点:

    • 不支持真正的抗锯齿功能。
    • 不能处理半透明物体。
    • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

    优点:

    • 所有光都是逐像素光源。计算复杂度前向渲染 O(m*n),延迟渲染O(m+n)。
    • 制作后处理等,可直接获取深度值。

G-Buffer

  1. 第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反射颜色、高光发射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。对于每个物体来说,这个Pass仅会执行一次。
    第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

  2. 默认的G缓冲区(注意,不同Unity版本的渲染纹理存储内容会有所不同)包含了以下几个渲染纹理(Render Texture,RT)。

    RT0:格式是ARGB32(每个通道8位),RGB通道用于存储漫反射颜色,A通道储存遮挡。
    RT1:格式是ARGB32(每个通道8位),RGB通道用于存储高光反射颜色,A通道用于存储高光反射的指数部分。
    RT2:格式是ARGB(2AAA),RGB通道用于存储世界空间法线,A通道没有被使用。
    RT3:格式是ARGB(2AAA)/ARGBHalf(每个通道16位),(高动态光照渲染/低动态光照渲染)用于存储自发光+lightmap+反射探针深度缓冲和模板缓冲。

    当在第二个Pass中计算光照时,默认情况下仅可以使用Unity内置的Standard 光照模型。其配置信息在如图所示位置
    在这里插入图片描述

2. 实现原理
  1. 作为用于渲染G缓冲的Pass通道,需设置Tags { "LightMode"="ForwardAdd" }
  2. shader块中只需实现一个通道即可,用于计算真正的光照模型的第二个Pass在Unity中设置。

延迟渲染-渲染G缓冲的Pass通道

Tags {
    
     "RenderType"="Opaque" }
LOD 100

Pass
{
    
    
    Tags {
    
    "LightMode" = "Deferred"}

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"
    
    sampler2D _MainTex;
    fixed4 _MainTex_ST;
    fixed4 _Diffuse;
    fixed4 _Specular;
    fixed _Gloss;

    struct v2f
    {
    
    
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float3 worldNormal : TEXCOORD1;
        float3 worldPos : TEXCOORD2;
    };

    // 渲染G缓冲输出的四张图片
    struct deferredOutput
    {
    
    
        float4 gBuffer0 : SV_TARGET0;
        float4 gBuffer1 : SV_TARGET1;
        float4 gBuffer2 : SV_TARGET2;
        float4 gBuffer3 : SV_TARGET3;
    };

    v2f vert (appdata_base v)
    {
    
    
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        return o;
    }

    deferredOutput frag (v2f i)
    {
    
    
        deferredOutput o;
        fixed3 color = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
        o.gBuffer0.rgb = color;
        o.gBuffer0.a = 1;
        o.gBuffer1.rgb = _Specular.rgb;
        o.gBuffer1.a = _Gloss / 32.0;
        o.gBuffer2 = float4(normalize(i.worldNormal * .5 + .5), 1);
        
        // 在非HDR模式下(即LDR),需对纹理颜色进行编码转换
		#if !defined(UNITY_HDR_ON)
			color.rgb = exp2(-color.rgb);
		#endif
		
        o.gBuffer3 = fixed4(color, 1);
        return o;
    }

    ENDCG
}

可供覆盖Unity默认光照模型的后处理

SubShader
{
    
    
    Pass
    {
    
    
        ZWrite Off
        Blend [_SrcBlend] [_DstBlend]
        // Blend One One

        CGPROGRAM
        // 目标在Shader Mode 3.0以上
        #pragma target 3.0
        #pragma vertex vert
        #pragma fragment frag
        // 只有使用了这样的指令,才可以在相关的pass中得到特定信息。
        #pragma multi_compile_lightpass
        // 排除不支持MRT的硬件
        #pragma exclude_renderers norm
        // 关联Unity宏
        #pragma multi_compile __ UNITY_HDR_ON

        #include "UnityCG.cginc"
        // 使用的函数库
        #include "UnityDeferredLibrary.cginc"
        #include "UnityGBuffer.cginc"

        sampler2D _CameraGBufferTexture0;
        sampler2D _CameraGBufferTexture1;
        sampler2D _CameraGBufferTexture2;

        unity_v2f_deferred vert (appdata_base v)
        {
    
    
            unity_v2f_deferred o;
            o.pos = UnityObjectToClipPos(v.vertex);
            // 计算机屏幕位置
            o.uv = ComputeScreenPos(o.pos);
            o.ray = UnityObjectToViewPos(v.vertex) * float3(-1, -1, -1);
            // _LightAsQuad 当再处理四边形(直射光)时返回1,否则返回0
            o.ray = lerp(o.ray, v.normal, _LightAsQuad);
            return o;
        }

        
        #ifdef	UNITY_HDR_ON
        half4
        #else
        fixed4
        #endif
        frag (unity_v2f_deferred i) : SV_Target
        {
    
    
            float3 worldPos;
            float2 uv;
            half3 lightDir;
            // 衰减
            float atten;
            // 阴影消失距离
            float fadeDist;
            // 常用照明数据计算(方向、衰减等)
            UnityDeferredCalculateLightParams (i, worldPos, uv, lightDir, atten, fadeDist);

            // 解析灯光颜色
            fixed3 lightColor = _LightColor.rgb * atten;

            // 解析G缓冲区
            half4 gbuffer0 = tex2D(_CameraGBufferTexture0, uv);
            half4 gbuffer1 = tex2D(_CameraGBufferTexture1, uv);
            half4 gbuffer2 = tex2D(_CameraGBufferTexture2, uv);
            
            // 漫反射颜色
            half3 diffuseColor = gbuffer0.rgb;
            // 高光颜色
            half3 specularColor = gbuffer1.rgb;
            // 高光系数
            fixed3 gloss = gbuffer1.a * 32;

            float3 worldNormal = normalize(gbuffer2.xyz * 2 - 1);
            // 摄像机的世界空间位置。 = normalize(_WorldSpaceCameraPos - worldPos);
            fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
            fixed3 halfDir = normalize(lightDir + viewDir);

            // 漫反射
            half3 diffuse = lightColor * diffuseColor * saturate(dot(worldNormal, lightDir));
            // 高光
            half3 specular = lightColor * specularColor * pow(saturate(dot(worldNormal, halfDir)), gloss);

            half4 color = float4(diffuse + specular, 1);

            #ifdef UNITY_HDR_ON
            return color;
            #else 
            return exp2(-color);
            #endif
        }
        ENDCG
    }
        
    //LDR Blend DstColor Zero    HDR : Blend One One
    //转码pass,主要是对于LDR转码
    // 针对HDR和LDR转码
    Pass
    {
    
    
        // 任何情况下都会渲染
        ZTest Always
        Cull Off
        ZWrite Off
        Stencil
        {
    
    
            // _StencilNonBackground:unity提供的天空盒遮蔽模板
            ref[_StencilNonBackground]
            readMask[_StencilNonBackground]
            
            CompBack equal
            CompFront equal
        }

        CGPROGRAM
        // 目标在Shader Mode 3.0以上
        #pragma target 3.0
        #pragma vertex vert
        #pragma fragment frag
        // 排除不支持MRT的硬件
        #pragma exclude_renderers nomrt

        #include "UnityCG.cginc"

        sampler2D _LightBuffer;

        struct v2f
        {
    
    
            float4 vertex : SV_POSITION;
            float2 texcoord : TEXCOORD0;
        };

        v2f vert (float4 vertex: POSITION, float2 texcoord: TEXCOORD0)
        {
    
    
            v2f o;
            o.vertex = UnityObjectToClipPos(vertex);
            o.texcoord = texcoord.xy;
            // 返回将当前眼睛的比例和偏移应用于UV中的纹理坐标的结果。这仅在定义unity_Single_PASS_STEREO时发生,否则纹理坐标将原封不动地返回。
            #ifdef UNITY_SINGLE_PASS_STEREO
            o.texcoord = TransformStereoScreenSpaceTex(o.texcoord, 1.0);
            #endif
            return o;
        }

        
        fixed4 frag (v2f i) : SV_Target
        {
    
    
            return -log2(tex2D(_LightBuffer,i.texcoord));
        }
        ENDCG
    }
}

5. 光照衰减

  1. Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了再光源空间中不同位置的点的衰减值。
  2. 例如(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了再光源空间中所关心的距离最远的点的衰减。
  3. 先将世界坐标与_LightMatrix0相乘得到在光源空间中的位置,用光源空间中顶点距离的平方来对纹理采样,然后,使用宏UINITY_ATTEN_CHANNEL来得到衰减纹理中的衰减值所在的分量,以得到最终的衰减值。
// 将世界坐标转换到光源空间(0~1)
float3 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1)).xyz;  
// 为减小开销,采样的坐标是正常坐标的平方
// 衰减纹理中衰减值所在分量
fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL; 

数学公式计算光照衰减

float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);  
atten = 1.0/distance;  

6. 阴影映射

1. 原理
  1. Shadow Map:它会首先把摄像机位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是摄像机看不到的地方。
  2. Screenspace Shadow Map:Unity首先会通过调用LightMode 为 ShadowCaster的Pass来得到可投射阴影是光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却出于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。

一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
3. 如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
4. 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity 中,这个过程通过为该物体执行LightMode 为ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投射映射技术,Unity还会使用这个Pass 产生一张摄像机的深度纹理。

2. 详解
  1. 使用SHADOW_COORDS(3) 完成对接受阴影的数据封装。
  2. 使用TRANSFER_SHADOW(o); 与宏协SHADOW_COORDS同工作,对宏协SHADOW_COORDS中定义的变量进行处理。
  3. 使用UNITY_LIGHT_ATTENUATION(shadow, i, i.worldPos); 与宏协SHADOW_COORDS同工作,包含了光照衰减以及阴影。
    注意:若使用此投置阴影,需保证存将对象渲染为阴影投射的(LightMode为ShadowCaster)Pass通道,否则会出现奇怪的透明效果。
  4. ForwardAdd的Pass通道中,需加入多阴影编译注释 pragma #multi_compile_fwdadd_fullshadows

AlphaTest(裁剪透明)

  1. 需设置 Tags { "RenderType"="TransparentCutOut" "Queue"="AlphaTest" "IgnoreProjector" = "True"}

AlphaTes(裁剪透明)

  1. 需在LightMode=ForwardBase的Pass通道内设置 ZWrite offBlend SrcAlpha OneMinusSrcAlpha
  2. 正常来说透明物体应设置Queue=Transparent,但这也将意味着对象不再能接受投影。对象接收阴影的最大渲染队列为AlphaTest
2. 示例:
  1. 投射阴影(需要额外增加一个pass通道)
Pass 
{
    
    
    Name "ShadowCaster"
    Tags {
    
     "LightMode" = "ShadowCaster" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 2.0
    // 阴影的对应宏。只有使用了这样的指令,才可以在相关的pass中得到阴影变量。
    #pragma multi_compile_shadowcaster
    // 允许大多数着色器的实例化阴影过程,在任何一个可以投射阴影的地方投射阴影。
    #pragma multi_compile_instancing 
    #include "UnityCG.cginc"

    struct v2f {
    
    
        // 声明阴影投射过程输出所需的所有数据(任何阴影方向/深度/距离根据需要),以及剪辑空间位置。
        V2F_SHADOW_CASTER;
        // 
        UNITY_VERTEX_OUTPUT_STEREO
    };

    v2f vert(appdata_base v)
    {
    
    
        v2f o;
        UNITY_SETUP_INSTANCE_ID(v);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
        // 顶点着色器部分,支持法线偏移阴影。要求位置和法线出现在顶点输入中。
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag(v2f i) : SV_Target
    {
    
    
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}
  1. 接收阴影(修改原本的ForwardBase和ForwardAdd通道)
Pass
{
    
    
    Tags {
    
     "LightMode"="ForwardBase" }
    CGPROGRAM
    // 只有使用了这样的指令,才可以在相关的pass中得到其他光源的光照变量,例如光照衰减值等。
    #pragma multi_compile_fwdbase
    #pragma vertex vert
    #pragma fragment frag

    #include "AutoLight.cginc"
    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"
    
    fixed4 _Diffuse;
    fixed4 _Specular;
    fixed _Gloss;

    struct v2f
    {
    
    
        float4 pos : SV_POSITION;
        float3 worldNormal : TEXCOORD0;
        float3 worldPos : TEXCOORD1;
        float3 vertexLight : TEXCOORD2;
        // 接受阴影的数据封装。(3)指的是TEXCOORD(3)
        SHADOW_COORDS(3)
    };

    v2f vert (appdata_base v)
    {
    
    
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);

        #ifdef LIGHTMAP_OFF
            float3 shLight = ShadeSH9(float4(v.normal, 1.0));
            o.vertexLight = shLight;
            #ifdef VERTEXLIGHT_ON
                fixed3 vertexLight = Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, 
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, o.worldPos, o.worldNormal
                    );
                o.vertexLight += vertexLight;
            #endif
        #else
            o.vertexLight = UNITY_LIGHTMODEL_AMBIENT.xyz;
        #endif
        // 与宏协SHADOW_COORDS同工作,对宏协SHADOW_COORDS中定义的变量进行处理。
        TRANSFER_SHADOW(o);

        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    
    
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLightDir));
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed3 halfDir = normalize(worldLightDir + viewDir);
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfDir)), _Gloss);

        // 与宏协SHADOW_COORDS同工作,得到阴影值
        // fixed shadow = SHADOW_ATTENUATION(i);
        // 宏协SHADOW_COORDS同工作,包含了光照衰减以及阴影。但由于ForwardBase逐像素光源一般是方向光,衰减固定为1,因此此时衰减无意义,与上式相同。
        UNITY_LIGHT_ATTENUATION(shadow, i, i.worldPos);

        return fixed4((diffuse + specular) * shadow + i.vertexLight, 1);
    }
    ENDCG
}

Pass
{
    
    
    Tags {
    
     "LightMode"="ForwardAdd" }
    Blend One One
    CGPROGRAM
    // 多阴影编译注释
    #pragma multi_compile_fwdadd_fullshadows
    #pragma multi_compile_fwdadd
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"
    #include "AutoLight.cginc"

    fixed4 _Diffuse;
    fixed4 _Specular;
    fixed _Gloss;

    struct v2f
    {
    
    
        float4 pos : SV_POSITION;
        float3 worldNormal : TEXCOORD0;
        float3 worldPos : TEXCOORD1;
        // 已包含:SHADOW_COORDS(3)
        LIGHTING_COORDS(2, 3)
    };

    v2f vert (appdata_base v)
    {
    
    
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        // 已包含:TRANSFER_SHADOW(o);
        TRANSFER_VERTEX_TO_FRAGMENT(o);
        return o;                
    }

    fixed4 frag (v2f i) : SV_Target
    {
    
    
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLightDir));
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed3 halfDir = normalize(worldLightDir + viewDir);
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfDir)), _Gloss);
        
        // fixed atten = LIGHT_ATTENUATION(i);
        // 计算包含了光照衰减以及阴影
        UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
        return fixed4((diffuse + specular) * atten, 1);
    }
    ENDCG
}
  1. AlphaTest的投射阴影(需要额外增加一个pass通道)
Pass 
{
    
    
	Tags {
    
     "LightMode" = "ShadowCaster" }

	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#pragma target 2.0
	#pragma multi_compile_shadowcaster
	#pragma multi_compile_instancing 
	#include "UnityCG.cginc"

	uniform sampler2D _MainTex;
	uniform float4 _MainTex_ST;
	uniform fixed _Cutoff;
	uniform fixed4 _Diffuse;

	struct v2f {
    
    
		V2F_SHADOW_CASTER;
		float2  uv : TEXCOORD1;
		UNITY_VERTEX_OUTPUT_STEREO
	};

	v2f vert( appdata_base v )
	{
    
    
		v2f o;
		UNITY_SETUP_INSTANCE_ID(v);
		UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
		TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
		o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
		return o;
	}

	float4 frag( v2f i ) : SV_Target
	{
    
    
		// clip 的区域即为裁剪掉的区域
		fixed4 texcol = tex2D( _MainTex, i.uv );
		clip( texcol.a * _Diffuse.a - _Cutoff );

		SHADOW_CASTER_FRAGMENT(i)
	}
	ENDCG
}

2. SurfaceShader

1. 基本组件

1. 编译注释

#pragma surface surfaceFunction lightModel [optionalparams]

  1. Surface Shader和CG其他部分一样,代码也是要写在CGPROGRAM和ENDCG之间。但区别是,它必须写在SubShader内部,而不能写在Pass内部。Surface Shader自己会自动生成所需的各个Pass。由上面的编译格式可以看出,surfaceFunctionlightModel是必须指定的。
  2. surfaceFunction通常就是名为surf的函数(函数名可以任意),它的函数格式是固定的:
void surf (Input IN, inout SurfaceOutput o)
void surf (Input IN, inout SurfaceOutputStandard o)
void surf (Input IN, inout SurfaceOutputStandardSpecular o)
  1. lightModel也是必须指定的。由于Unity内置了一些光照函数——Lambert(diffuse)和Blinn-Phong(specular),因此这里在默认情况下会使用内置的Lambert模型。当然我们也可以自定义。
    注意:自定义函数需以Lighting开头,在其后面的才是函数名。如LightingOcean的名字为Ocean。
  2. optionalparams包含了很多可用的指令类型,包括开启、关闭一些状态,设置生成的Pass类型,指定可选函数等。除了上述的surfaceFuntion和lightModel,我们还可以自定义两种函数:vertex:VertexFunction(顶点)和finalcolor:ColorFunction(最终颜色)。也就是说,Surface Shader允许我们自定义四种函数。
  3. 流程:vertex -> surf -> lightModel -> finalcolor

可选参数(操作符)
透明度混合与透明度测试

  • alpha or alpha:auto 为简单光照选择褪色透明度(等同于alpha:fade) ,以及基于物理照明的预乘透明度(等同于alpha:premul)
  • alpha:blend 开启透明度混合
  • alpha:fade 开启传统渐变透明
  • alpha:premul 开启预乘a透明度
  • alphatest:VariableName 根据 VariableName 的变量来控制透明度混合和透明度测试, VariableName 是一个float型的变量,剔除不满足条件的片元,此时往往需要用到addshadow来生成正确阴影投射的Pass
  • keepalpha 默认不透明表面着色器将1写入A通道,不管alpha输出值以及光照函数的返回值
  • decal:add 对其他表面上的物体使用additive blending
  • decal:blend 对其他表面上的物体使用alpha blending

自定义修改函数

  • vertex:VertexFunction 顶点修改函数,用于修改计算顶点位置、信息等
  • finalcolor:ColorFunction 最终颜色修改函数
  • finalgbuffer:ColorFunction 自定义延迟路径,用于更改gbuffer
  • finalprepass:ColorFunction 自定义预处理路径

阴影

  • addshadow 生成一个阴影投射的Pass,为一些使用了顶点动画、透明度测试的物体产生正确的阴影
  • fullforwardshadows 支持前向渲染路径中所有光源类型的阴影,shader默认只支持最重要平行光的阴影,添加该参数可以支持点光源或聚光灯的阴影效果
  • tessellate:TessFunction 使用DX11 GPU曲面细分

控制代码生成(表面着色器默认处理所有坑能的光照、阴影、光照烘培,可手动调整跳过一些不必要的加载提升性能)

  • exclude_path:deferred, exclude_path:forward, exclude_path:prepass 不为某个渲染路径生成代码
  • noshadow 禁用阴影
  • noambient 不应用任何环境光以及光照探针
  • novertexlights 在前向渲染路径中不应用任何逐顶点光照及光照探针
  • nolightmap 不应用任何光照烘培
  • nodynlightmap 不应用实时GI
  • nodirlightmap 不应用directional lightmaps
  • nofog 不应用任何雾效
  • nometa 生成meta这个Pass(that’s used by lightmapping & dynamic global illumination to extract surface information)
  • noforwardadd 不应用前向渲染中所有的additive pass,使得shader只支持一个重要平行光,其他光用逐顶点/SH光源计算光照影响,使shader更精简
  • nolppv 不应用光照探针代理Light Probe Proxy Volume(LPPV)
  • noshadowmask 不应用Shadowmask

其他

  • softvegetation 只有当Soft Vegetation(软植被)开启时该shader才被渲染
  • interpolateview 在顶点而不是片元着色器中计算 view direction并插值,需多使用一张纹理插值器,提升渲染速度
  • halfasview Pass half-direction vector into the lighting function instead of view-direction. Half-direction will be computed and normalized per vertex. This is faster, but not entirely correct.
  • dualforward 在前向渲染中使用dual lightmaps
  • dithercrossfade 使表面着色器支持 dithering effects
2. 可插入的计算函数
  1. 最重要的计算函数
void surf(Input IN, inout SurfaceOutput o)
void surf(Input IN, inout SurfaceOutputStandard o)
void surf(Input IN, inout SurfaceOutputStandardSpecular o)
  1. 顶点修改
void vert(inout appdata_full v)
void vert(inout appdata_full v, out Input o)
  1. Lighting
// unity老版本(新版本兼容)不包含GI的
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten)
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
// unity新版本(包含GI,要自定义GI函数)
half4 Lighting<Name> (SurfaceOutput s, UnityGI gi)
half4 Lighting<Name> (SurfaceOutput s, half3 viewDir, UnityGI gi)
  1. 自定义GI函数
half4 Lighting<Name>_GI(SurfaceOutput s, UnityGIInput data, inout UnityGI gi);
  1. 延迟渲染
// 遗留的延迟渲染
half4 Lighting<Name>_PrePass(SurfaceOutput s, half4 light)
// 延迟渲染
half4 Lighting<Name>_Deferred(SurfaceOutput s, UnityGI gi, out half4 outDiffuseOcclusion, out half4 outSpecSmoothness, out half4 outNormal)
  1. 最终颜色修改
void final(Input IN, SurfaceOutput o, inout fixed4 color)
void final(Input IN, SurfaceOutputStandard o, inout fixed4 color)
void final(Input IN, SurfaceOutputStandardSpecular o, inout fixed4 color)

2. 结构体

结构体可以使用两个:struct InputSurfaceOutput。其中Input结构体是允许我们自定义的。如下表。这些变量只有在真正使用的时候才会被计算生成。在一个贴图变量之前加上uv两个字母,就代表提取它的uv值,例如uv_MainTex 。

1. Input
变量 描述
float3 viewDir 视角方向 (view direction)。可用于计算边缘光照等。
float4 with COLOR semantic 每个顶点插值后的颜色
float4 screenPos 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。
float3 worldPos 世界空间中的位置。
float3 worldRefl 如果shader没有赋值o.Normal,将会包含世界反射向量。
float3 worldNormal 如果shader没有赋值o.Normal,将会包含世界法向量。
float3 worldRefl; INTERNAL_DATA 一旦在任意位置赋值o.Normal就会归零。使用WorldReflectionVector(IN, o.Normal) 重新变化为世界空间坐标系中反射的向量
float3 worldNormal; INTERNAL_DATA 一旦在任意位置赋值o.Normal就会归零。使用WorldNormalVector(IN, o.Normal) 重新变化为世界空间坐标系中法线的向量
2. SurfaceOutput

包括:SurfaceOutput、SurfaceOutputStandard和SurfaceOutputStandardSpecular。

我们也可以自定义这个结构体内的变量,自定义最少需要有4个成员变量:Albedo、Normal、Emission和Alpha,缺少一个都会报错。关于它最难理解的也就是每个变量的具体含义以及工作机制(对像素颜色的影响)。:

struct SurfaceOutput
{
    
    
    fixed3 Albedo;  // diffuse color,反射率,纹理颜色
    fixed3 Normal;  // tangent space normal, if written,法线
    fixed3 Emission;  //自发光
    half Specular;  // specular power in 0..1 range 镜面反射度
    fixed Gloss;    // specular intensity 光泽度    
    fixed Alpha;    // alpha for transparencies 透明度
};
struct SurfaceOutputStandard
{
    
    
    fixed3 Albedo;      // base (diffuse or specular) color
    fixed3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Metallic;      // 0=non-metal, 1=metal
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
};
struct SurfaceOutputStandardSpecular
{
    
    
    fixed3 Albedo;      // diffuse color
    fixed3 Specular;    // specular color
    fixed3 Normal;      // tangent space normal, if written
    half3 Emission;
    half Smoothness;    // 0=rough, 1=smooth
    half Occlusion;     // occlusion (default 1)
    fixed Alpha;        // alpha for transparencies
};
  • Albedo:我们通常理解的对光源的反射率。它是通过在Fragment Shader中计算颜色叠加时,和一些变量(如vertex lights)相乘后,叠加到最后的颜色上的。(漫反射颜色)
  • Normal:即其对应的法线方向。只要是受法线影响的计算都会受到影响。
  • Emission:自发光。会在Fragment 最后输出前(调用final函数前,如果定义了的话),使用下面的语句进行简单的颜色叠加:c.rgb += o.Emission;
  • Specular(Metallic):高光反射中的指数部分的系数。影响一些高光反射的计算。按目前的理解,也就是在光照模型里会使用到(如果你没有在光照函数等函数——包括Unity内置的光照函数,中使用它,这个变量就算设置了也没用)。有时候,你只在surf函数里设置了它,但也会影响最后的结果。这是因为,你可能使用了Unity内置的光照模型,如BlinnPhong,它会使用如下语句计算高光反射的强度(在Lighting.cginc里):float spec = pow (nh, s.Specular*128.0) * s.Gloss;
  • Gloss(Smoothness):高光反射中的强度系数。和上面的Specular类似,一般在光照模型里使用。
  • Alpha:通常理解的透明通道。在Fragment Shader中会直接使用下列方式赋值(如果开启了透明通道的话):c.a = o.Alpha;
3. 获取模型空间坐标

可在vertex顶点函数中获取模型空间坐标。

#pragma surface surf Standard fullforwardshadows vertex:vert

struct Input
{
    
    
    float2 uv_MainTex;
    float3 worldPos;
    float2 uv_OtherTex;
    float4 modelPos;
};

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

// 获取模型空间坐标
void vert(inout appdata_base v, out Input o)
{
    
    
    UNITY_INITIALIZE_OUTPUT(Input, o);
    o.modelPos = v.vertex;
    // o.modelPos = mul(unity_ObjectToWorld, v.vertex);
}

使用函数也可以获取模型空间坐标。

float3 modelPos = mul(unity_WorldToObject, float4(IN.worldPos, 1));

3. 实例

1. 使用BlinnPhong老版光照模型
  1. 修改光照模型
  2. 修改输入inout为SurfaceOutput
  3. 更换使用对应的光照系数
CGPROGRAM
// 修改光照模型
#pragma surface surf BlinnPhong fullforwardshadows
#pragma target 3.0

sampler2D _MainTex;

struct Input
{
    
    
    float2 uv_MainTex;
};

// 老版使用的光照系数变量名为_Gloss、_Specular,因此需要统修改
half _Gloss;
half _Specular;
fixed4 _Color;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

// 老版使用的输入为SurfaceOutput,因此需要统修改
void surf (Input IN, inout SurfaceOutput o)
{
    
    
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Specular = _Specular;
    o.Gloss = _Gloss;
    o.Alpha = c.a;
}
ENDCG
2. 卡通渲染

我们也可以自定义函数,改写光照模式

// nolightmap: 禁用此着色器中的所有光照贴图(烘焙)支持。 
#pragma surface surf Toon fullforwardshadows nolightmap finalcolor:final

...

half4 LightingToon (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    
    
    float difLight = dot(lightDir, s.Normal) * 0.5 + 0.5;
    difLight = smoothstep(0, 1, difLight);
    float toon = floor(difLight * _Steps) / _Steps;
    difLight = lerp(difLight, toon, _ToonEffect);
    fixed3 diffuse = _LightColor0 * s.Albedo * difLight;
    return half4(diffuse, 1);
}
3. 纯纹理颜色

漫反射会受到环境光的影响,无论如何也无法显示出贴图原本的颜色,因此不使用漫反射,并将其设为0

half4 LightingToon (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    
    
    return half4(0, 0, 0, 0);
}

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutput o)
{
    
    
    fixed4 color = tex2D (_MainTex, IN.uv_MainTex) * _Color;

    o.Albedo = fixed4(0, 0, 0, 0);
    o.Emission = color;
    o.Alpha = color.a;
}
4. 法线贴图采样
  1. 在Input中获取法线贴图uv
  2. 获得法线贴图的值

(示例在下方)

5. 最终颜色控制
  1. 在Input结构体中加入viewDir获取视角坐标。

(示例在下方)

6. 边缘光
  1. 设置最终颜色控制函数 finalcolor:final
  2. 将计算后的结果赋值给Emission(自发光)
CGPROGRAM
#pragma surface surf Standard fullforwardshadows finalcolor:final

#pragma target 3.0

sampler2D _MainTex;
sampler2D _BumpMap;

struct Input
{
    
    
    float2 uv_MainTex;
    // 提取_BumpMap的uv值
    float2 uv_BumpMap;
    // 获取视角坐标
    float3 viewDir;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;
float _BumpScale;
fixed4 _ColorTint;
float4 _RimColor;
float _RimPower;

// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
    // 获得法线贴图的值
    fixed3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
    // xy轴上增大倾斜倍数。直接相乘只是增加了长度,对法线方向并没有影响。
    normal.xy *= _BumpScale;
    o.Normal = normal;
    // 计算边缘光
    half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
    o.Emission = _RimColor.rgb * pow(rim, _RimPower);
}

// 最终颜色控制函数
void final(Input IN, SurfaceOutputStandard o, inout fixed4 color)
{
    
    
    color *= _ColorTint;
}
ENDCG
7. 添加描边
  1. 新建一个Pass通道,通道内写法与顶点着色器实现添加描边的方式相同即可。
8. X-ray
  1. 新建一个Pass通道,通道内写法与顶点着色器实现X-ray的方式相同即可。
9. 流动效果
  1. 根据时间_Time决定uv位置,从而实现流动效果。
  2. 用自发光作为流动效果载体。
CGPROGRAM
#pragma surface surf StandardSpecular fullforwardshadows
#pragma target 3.0

sampler2D _MainTex;
sampler2D _Normal;
sampler2D _Mask;
sampler2D _Specular;
sampler2D _Fire;

struct Input
{
    
    
    float2 uv_MainTex;
};

half _Smoothness;
half _FireIntensity;
half2 _FireSpeed;
fixed4 _Color;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
    
    
    // 漫反射
    o.Albedo = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    // 法线
    o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_MainTex));
    float2 uv = IN.uv_MainTex + _Time.x * _FireSpeed;
    // 使用自发光作为流动效果载体。_Mask作为蒙版遮罩,_Fire作为流动体
    o.Emission = (tex2D(_Mask, IN.uv_MainTex) * tex2D(_Fire, uv) * (_FireIntensity + _SinTime.w)).rgb;
    // 将图片颜色的作为高光颜色(非必要)
    o.Specular = tex2D(_Specular, IN.uv_MainTex).rgb;
    // 高光反射中的强度系数(非必要)
    o.Smoothness = _Smoothness;
    o.Alpha = 1;
}
ENDCG
10. UV扭曲
  1. 根据时间_Time决定uv位置,从而实现流动效果。
  2. 将法线贴图的值作为偏移量附加给纹理坐标的获取上,导致纹理坐标与期待坐标相比发生偏移,从而实现UV扭曲效果。
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

sampler2D _MainTex;
sampler2D _DistortTexture;

struct Input
{
    
    
    float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
half _Speed;
half _UVDisIntensity;
fixed4 _Color;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    float2 uv1 = IN.uv_MainTex + _Time.y * _Speed * float2(1, 1);
    float2 uv2 = IN.uv_MainTex + _Time.y * _Speed * float2(-1, -1);
    // UnpackScaleNormal:对法线贴图使用正确的解码,并缩放法线。与下式作用相同。
    // float2 distortTexture = UnpackNormal(tex2D(_DistortTexture, IN.uv_MainTex));
    // distortTexture.xy *= _UVDisIntensity;
    float2 distortTexture = UnpackScaleNormal(tex2D(_DistortTexture, IN.uv_MainTex), _UVDisIntensity);
    // 获取偏移位置的颜色,形成类似UV扭曲的效果
    float4 mainTex1 = tex2D(_MainTex, (uv1 + distortTexture).xy);
    float4 mainTex2 = tex2D(_MainTex, (uv2 + distortTexture).xy);
    float4 color = _Color * mainTex1 * mainTex2;

    // 漫发射
    o.Albedo = color;
    // 自发光
    o.Emission = color;
    // 高光反射中的指数部分
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = 1;
}
ENDCG
11. Glow
  1. 使用两套不同的法线纹理,两套不同的图像。
  2. 将蒙版的R颜色通道作为蒙版值,其值(0~1)正对应着两张纹理、背景图和顶面图
    在这里插入图片描述
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

sampler2D _MainTex;
sampler2D _TopTex;
sampler2D _Nomral;
sampler2D _BurnNormal;
sampler2D _Mask;
half _BurnRange;
// 使用纯色时使用
// fixed4 _BurnColor;

struct Input
{
    
    
    float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;


UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    // 两套不同的法线纹理
    float3 normal1 = UnpackNormal(tex2D(_Nomral, IN.uv_MainTex));
    float3 normal2 = UnpackNormal(tex2D(_BurnNormal, IN.uv_MainTex));
    // 将蒙版的R颜色通道作为蒙版值,其值(0~1)正对应着两张纹理、背景图和顶面图
    fixed3 maskColor = tex2D(_Mask, IN.uv_MainTex);
    float maskR = saturate(_BurnRange + maskColor.r);

    // 拿到背景图像
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    fixed4 t = tex2D (_TopTex, IN.uv_MainTex) * _Color;
    // 使用纯色作为顶面时使用
    // fixed4 diffuse = lerp(c, _BurnColor, maskR);
    fixed4 diffuse = lerp(c, t, maskR);

    o.Normal = lerp(normal1, normal2, maskR);
    o.Albedo = diffuse.rgb;

    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = 1;
}
ENDCG
12. 法线外扩变化
  1. 编写顶点着色器代码,添加vertex:vertFun。目的是修改顶点的位置。
  2. 使用函数UNITY_INITIALIZE_OUTPUT(type,name)把所给结构体里的各个变量初始化为0 (数据类型,数据名),防止DX报错。
  3. 沿法线等比放大模型,根据时间和y轴位置决定不同放大比,形成波纹效果
    在这里插入图片描述
CGPROGRAM
#pragma surface surf Standard fullforwardshadows vertex:vertFun
#pragma target 3.0

sampler2D _MainTex;
half _ExtrusionFrency;
half _ExtrusionSwing;

struct Input
{
    
    
    float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

void vertFun (inout appdata_full v, out Input o)
{
    
    
    // 用于把所给结构体里的各个变量初始化为0 (数据类型,数据名)
    // 防止DX报错
    UNITY_INITIALIZE_OUTPUT(Input, o);
    float3 normal = v.normal.xyz;
    float3 vertexPos = v.vertex.xyz;
    // 沿法线等比放大模型,根据时间和y轴位置决定不同放大比,形成波纹效果
    v.vertex.xyz += normal * max(sin((vertexPos.y + _Time.x) * _ExtrusionFrency) * _ExtrusionSwing, 0);
}

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}
ENDCG
13. 消融效果
  1. 解析消融纹理的R通道作为消融依据。
  2. 使用clip函数,小于0的部分会被清除。
  3. 定义一个temp值,利用消融依据,确保越靠近消融边缘的部分temp越低。
  4. 通过temp值获取纹理颜色,越靠近近消融边缘的部分越靠近图片的某一侧。
  5. 为消融颜色应用一个插值过渡。越靠近边缘的部分,颜色越接近期待的消融颜色,并将其应用到发光材质中。
    在这里插入图片描述
CGPROGRAM
#pragma surface surf Standard fullforwardshadows noshadow addshadow
#pragma target 3.0

sampler2D _MainTex;
sampler2D _Normal;
half _NormalScale;
sampler2D _DisolveTex;
half _Threshold;
sampler2D _BurnTex;
half _EdgeLength;
half _BurnInstensity;

struct Input
{
    
    
	float2 uv_MainTex;
	float2 uv_Normal;
	float2 uv_DisolveTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
	fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
	// 法线
	o.Normal = UnpackScaleNormal(tex2D(_Normal, IN.uv_Normal) ,_NormalScale);
	// 漫反射
	o.Albedo = c.rgb;

	// 解析消融纹理的R通道作为消融依据
	float cutout = tex2D(_DisolveTex, IN.uv_DisolveTex).r;
	// 小于0的部分会被消融,也就意味着_Threshold越大消融程度越高
	clip(cutout - _Threshold);

	// 越靠近消融边缘的部分temp越低
	float temp = saturate((cutout - _Threshold) / _EdgeLength);
	// 通过temp值获取纹理颜色,越靠近近消融边缘的部分越靠近图片右侧
	fixed4 edgeColor = tex2D(_BurnTex, float2(1 - temp,1 - temp));
	// 为消融颜色应用一个过渡。越靠近边缘的部分,颜色越接近期待的消融颜色。
	fixed4 finalColor = _BurnInstensity * lerp(edgeColor, fixed4(0, 0, 0, 0), temp);
	
	o.Emission = finalColor.rgb;
	o.Metallic = _Metallic;
	o.Smoothness = _Glossiness;
	o.Alpha = c.a;
}
ENDCG
14. 区域过度
  1. 获取模型空间坐标
  2. 记录比起始过渡高度高出的单位量
  3. 起始过渡高度到高出的单位量为1的高度,这段区间即为过渡区间
    在这里插入图片描述
CGPROGRAM
#pragma surface surf Standard fullforwardshadows vertex:vert
#pragma target 3.0

sampler2D _MainTex;
half _StartPoint;
fixed4 _Tint;
half _Dis;
sampler2D _OtherTex;

struct Input
{
    
    
    float2 uv_MainTex;
    float3 worldPos;
    float2 uv_OtherTex;
    float4 modelPos;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

// 获取模型空间坐标
void vert(inout appdata_base v, out Input o)
{
    
    
    UNITY_INITIALIZE_OUTPUT(Input, o);
    o.modelPos = v.vertex;
    // o.modelPos = mul(unity_ObjectToWorld, v.vertex);
}

void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    // 记录比起始过渡高度高出的单位量(单位为_Dis)
    float temp =  saturate((IN.modelPos.y + _StartPoint) / _Dis);
    fixed4 c1 = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    fixed4 c2 = tex2D(_OtherTex, IN.uv_OtherTex) * _Color;
    // 起始过渡高度到高出的单位量为1的高度,这段区间即为过渡区间
    fixed4 color = lerp(c2, c1, temp);

    o.Albedo = color.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c1.a;
}
ENDCG
15. AlphaTest
  1. 使用alphatest来完成透明度测试
  2. 关闭原本阴影并添加并生成一个阴影投射的Pass(使用addshadow语句即可完成)
// 透明度测试通道
Tags {
    
     "RenderType"="Transparent" "Queue"="AlphaTest"}
LOD 200
// 双面渲染
Cull off

CGPROGRAM
// 根据_Cutoff来控制透明度混合和透明度测试。关闭原本阴影并添加并生成一个阴影投射的Pass
#pragma surface surf Standard alphatest:_Cutoff noshadow addshadow
16. AlphaBlend
  1. 使用alpha:blend来开启透明度混合
Tags {
    
     "RenderType"="Transparent" "Queue"= "Transparent"}
LOD 200
Cull off

CGPROGRAM
#pragma surface surf Standard alpha:blend noshadow
#pragma target 3.0
17. 雪效果

注:

  1. 默认的法线是垂直于面的常规法线方向,一旦在任意位置赋值就会转换为切线空间的法线方向。(即便是先获取值再改变方向,获取到的值也会变为切线空间的法线方向)
  2. WorldNormalVector可将已经被转换为切线空间的法线方向,重新变化为世界空间坐标系中法线的向量。
  1. 计算雪浓度(法线与雪方向的夹角)
  2. 以雪的浓度为依据,将法线转变为纹理法线与雪法线的中间某值,使得效果更加柔和
  3. 计算融合后的世界法线向量
  4. 融合后法线与雪方向二次运算,让雪纹理与贴图纹理融合更加柔和
CGPROGRAM
#pragma surface surf StandardSpecular fullforwardshadows
#pragma target 3.0

sampler2D _MainTex;
sampler2D _NormalTex;
sampler2D _SnowTex;
sampler2D _SnowNormal;

struct Input
{
    
    
    float2 uv_MainTex;
    float2 uv_NormalTex;
    float2 uv_SnowTex;
    float2 uv_SnowNormal;
    float3 worldNormal;
    // 该宏定义与WorldNormalVector联动
    INTERNAL_DATA
};

half _Glossiness;
half _Metallic;
fixed4 _Color;
float4 _SnowDir;
half _SnowAmount;

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
    
    
    float3 normalTex = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
    float3 snowNorTex = UnpackNormal(tex2D(_SnowNormal, IN.uv_SnowNormal));
    // 默认的法线是垂直于面的常规法线方向,一旦在任意位置赋值就会转换为切线空间的法线方向。(即便是先获取值再改变方向,获取到的值也会变为切线空间的法线方向)
    // 该转换可将已经被转换为切线空间的法线方向,重新变化为世界空间坐标系中法线的向量
    // 世界空间法线向量
    fixed3 wNormal = WorldNormalVector(IN, normalTex);
    // 计算雪浓度(法线与雪方向的夹角)
    float lerpVal = saturate(dot(wNormal, _SnowDir.xyz));
    // 以雪的浓度为依据,将法线转变为纹理法线与雪法线的中间某值,使得效果更加柔和
    fixed3 finalNormal = lerp(normalTex, snowNorTex, lerpVal * _SnowAmount);
    // 融合后的世界法线向量
    fixed3 fWNormal = WorldNormalVector(IN, finalNormal);
    // 融合后法线与雪方向二次运算,让雪纹理与贴图纹理融合更加柔和
    lerpVal = saturate(dot(fWNormal, _SnowDir.xyz));
    fixed4 c = lerp(tex2D(_MainTex, IN.uv_MainTex) * _Color, tex2D(_SnowTex, IN.uv_SnowTex), lerpVal * _SnowAmount);
    
    o.Normal = finalNormal;
    o.Albedo = c.rgb;
    // o.Metallic = _Metallic;
    // o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}
ENDCG

3. 其他内容

1. GI

1. 什么是GI

在这里插入图片描述

2. Cubemap(立方体贴图)

1. 代码创建立方体贴图
  1. 勾选检查器中Cubemap的Readable,使该Cubemap可读写
  2. 使用函数camera.RenderToCubema(Cubemap cubemap);,将对应位置的相机全景拍摄渲染为静态立方体贴图,赋值给入参Cubemap。
public class RenderCubeMap : ScriptableWizard
{
    
    
    public Transform renderPos;
    public Cubemap cubemap;

    [MenuItem("Tools/CreateCubemap")]
    static void CreatCubemap()
    {
    
    
        ScriptableWizard.DisplayWizard<RenderCubeMap>("Render Cube", "Create");
    }

    private void OnWizardCreate()
    {
    
    
        GameObject go = new GameObject("CubemapCam");
        Camera camera = go.AddComponent<Camera>();
        go.transform.position = renderPos.position;
        // 从该相机渲染为静态立方体贴图,赋值给入参Cubemap。
        camera.RenderToCubemap(cubemap);
        DestroyImmediate(go);
    }

    private void OnWizardUpdate()
    {
    
    
        helpString = "选择渲染位置并且确定需要设置的Cubemap";
        isValid = renderPos != null && cubemap != null;
    }
}
2. 烘焙生成立方体贴图

在这里插入图片描述

3. 反射

  1. 使用函数reflect(worldViewDir, worldNormal)获取到反射向量。
  2. 在shader中应用反射与立方体贴图,反射立方体贴图中的颜色。
    若使用法线仅可获得常规贴图一样的效果,而使用反射可得到如同鱼眼镜头的效果。
    在这里插入图片描述
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"

fixed4 _ReflectionColor;
samplerCUBE _CubeMap;
half _ReflectionAmount;
sampler2D _MainTex;
float4 _MainTex_ST;

struct v2f
{
    
    
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    float3 worldPos : TEXCOORD1;
    float3 worldNormal : TEXCOORD2;
    float3 worldViewDir : TEXCOORD3;
    float3 worldRefl : TEXCOORD4;
};

v2f vert (appdata_base v)
{
    
    
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
	o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
	o.worldNormal = UnityObjectToWorldNormal(v.normal);
	o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
	o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    
    
    fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    fixed3 diffuse = tex2D(_MainTex, i.uv).rgb *_LightColor0.rgb * (saturate(dot(worldLightDir, i.worldNormal)) * 0.5 + 0.5);
    // 常规查询立方体纹理
    // fixed3 refection = texCUBE(_CubeMap, i.worldNormal).rgb * _ReflectionColor;
    // 根据反射法向查询立方体纹理,点随着与摄像机角度的而增大反射角,相比于常规的查询立方体纹理,能获得更大的视野面积,形成类似鱼眼镜头的效果
    fixed3 refection = texCUBE(_CubeMap, i.worldRefl).rgb * _ReflectionColor;
    fixed3 col = ambient + lerp(diffuse, refection, _ReflectionAmount);
    return fixed4(col, 1);
}
ENDCG

效果如图
在这里插入图片描述

2. 菲涅尔反射

在这里插入图片描述
由菲涅耳公式推导出的光的反射规律,F0为系数,v为视角方向,n为法线向量。

fixed4 frag (v2f i) : SV_Target
{
    
    
    fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    fixed3 diffuse = tex2D(_MainTex, i.uv).rgb *_LightColor0.rgb * (saturate(dot(worldLightDir, i.worldNormal)) * 0.5 + 0.5);

    fixed3 refection = texCUBE(_CubeMap, i.worldRefl).rgb;
    // 菲涅尔反射
    fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(normalize(i.worldViewDir), i.worldNormal), 5);

    fixed3 col = ambient + lerp(diffuse, refection, saturate(fresnel));
    return fixed4(col, 1);
}

在这里插入图片描述

4. 折射

  1. 使用函数refract(-o.worldViewDir, o.worldNormal, _RefractRotio)获取到反射向量。具体使用方式同上。

  2. 折射系数为1时,效果类似于透镜。
    在这里插入图片描述

  3. 折射系数为0时,效果类似于放大镜
    在这里插入图片描述

8. RenderTexture镜面效果
  1. 创建自定义渲染纹理
  2. 在目标镜面上添加正交摄像机,设置摄像机目标纹理为刚才创建的自定义渲染纹理,正交尺寸比将转换为自定义渲染纹理的尺寸比
  3. 将自定义渲染纹理赋值给shader的纹理2D,或UGUI的原始图像即可使用。
v2f vert (appdata v)
{
    
    
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.uv.x = 1 - o.uv.x;
    return o;
}

在这里插入图片描述

5. GrabPass

1. 介绍
  1. 该通道用于截屏。
  2. 在shader中建立一个特殊的Pass通道GrabPass { "GrabPassTex" },其中的GrabPassTex为图像名称。
  3. 在其他Pass通道中便可命名sampler2D GrabPassTex来使用该图像。
    在这里插入图片描述
2. 玻璃效果
  1. 使用函数ComputeGrabScreenPos(o.vertex)计算用于采样GrabPass纹理的纹理坐标。输入为剪辑空间位置。
  2. 使用法线纹理贴图,获取纹理贴图的xy作为偏移量,以形成玻璃扭曲光线的效果。
  3. 将获得GrabPass的位置纹理坐标处于w值获得正确的通道位置。
    在这里插入图片描述
// 抓屏通道。抓屏时间取决于Queue,如果在其他shader中使用抓屏应直接调用该pass通道而不要再抓屏。抓屏通道需放到最上方,以免被该对象本身遮蔽
GrabPass {
    
     "GrabPassTex" }

Pass
{
    
    
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"


    struct v2f
    {
    
    
        float4 vertex : SV_POSITION;
        float4 uv : TEXCOORD0;
        float3 tanToWorld0: TEXCOORD1;
        float3 tanToWorld1: TEXCOORD2;
        float3 tanToWorld2: TEXCOORD3;
        float3 worldPos: TEXCOORD4;
        float4 scrPos: TEXCOORD5;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    sampler2D _BumpMap;
    float4 _BumpMap_ST;
    float _BumpScale;
    float4 _Diffuse;
    // 获取抓屏通道的贴图。
    sampler2D GrabPassTex;
    float4 GrabPassTex_ST;
    float _Distortion;
    samplerCUBE _Cubemap;
    float _RefractAmount;

    v2f vert (appdata_tan v)
    {
    
    
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
        o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
        fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex);
        fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
        fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
        fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
        o.tanToWorld0 = float3(worldTangent.x, worldBinormal.x, worldNormal.x);
        o.tanToWorld1 = float3(worldTangent.y, worldBinormal.y, worldNormal.y);
        o.tanToWorld2 = float3(worldTangent.z, worldBinormal.z, worldNormal.z);
        o.worldPos = worldPos;

        // 计算用于采样GrabPass纹理的纹理坐标。输入为剪辑空间位置。
        o.scrPos = ComputeGrabScreenPos(o.vertex);
        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    
    
        fixed4 albedo = tex2D(_MainTex, i.uv.xy);
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
        fixed3 tangentNormal = UnpackNormal(packedNormal);
        tangentNormal.xy *= _BumpScale;
        fixed3 worldNormal = normalize(mul(float3x3(i.tanToWorld0, i.tanToWorld1, i.tanToWorld2), tangentNormal));
        
        // 切获取纹理偏移以形成玻璃扭曲光线的效果,
        float2 offset = tangentNormal.xy * _Distortion;
        // 距离越远,i.scrPos.z越大,越容易看出来有法偏移。虽然从物理角度讲这并不合理,但从效果来看这样更棒。
        i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
        // 纹理坐标除以深度w以获得正确的空间图像,保证映出的是对应位置的图像
        fixed3 refrCol = tex2D(GrabPassTex, i.scrPos.xy / i.scrPos.w).rgb;

        // 反射天空的颜色
        fixed3 reflCol = texCUBE(_Cubemap, reflect(-viewDir, worldNormal)).rgb * albedo;

        fixed3 color = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
        return fixed4(color, 1);
    }
    ENDCG
}

6. 动画

1. 序列帧动画
  1. 确定序列帧内容宽高,计算播放顺序,操作uv来实现序列动画。
  2. 播放帧的确定:floor(_Time.y * _Speed)
  3. 水平位置的确定:floor(time / _HorAmount) 播放帧整除每行数量即为当前播放行
  4. 垂直位置的确定:time - row * _HorAmount 去掉当前行上面的每一行的内容,剩余的数量即当前列的序号,也是垂直位置。
  5. uv与行列位置均需被行列数量除,为了让uv扩大相应的尺寸。
fixed4 frag (v2f i) : SV_Target
{
    
    
    float time = floor(_Time.y * _Speed);
    float row = floor(time / _HorAmount);
    float column  = time - row * _HorAmount;

    // 一起除是为了让uv扩大相应的尺寸
    float2 uv = i.uv + float2(column, -row);
    uv.x /= _HorAmount;
    uv.y /= _VerAmount;

    fixed4 col = tex2D(_MainTex, uv);
    return col;
}
2. 滚动动画

沿着一个方向移动的动画。
利用_Time的自增长特性,将值附加到uv的某一个轴即可。

v2f vert (appdata_base v)
{
    
    
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex) + float2(_ScrollX, 0) * _Time.y;
    return o;
}
3. 顶点动画

在顶点转化到裁剪坐标前,也就是仍在模型空间下时对其进行动画控制。

v2f vert (appdata_base v)
{
    
    
    v2f o;
    v.vertex.y = v.vertex.y + _Arange * sin(_Time.y * _Speed + v.vertex.x * _Frequency);
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    return o;
}

7. 广告牌

1. 介绍

通过视口方向与垂直方向的叉乘,求出此时表面法线和向上方向的垂直的右向量,右向量与法线再次叉乘得到正确的上方向量。
在这里插入图片描述

2. 示例
v2f vert (appdata_base v)
{
    
    
    v2f o;
    
    float3 center = float3(0, 0, 0);
    // 模型空间的摄像头坐标即视图方向
    float3 normalDir = normalize(mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)));
    // _Verical将决定面片是否允许y轴旋转
    normalDir.y = normalDir.y * _Verical;
    // 上方方向
    float3 upDir = float3(0, 1, 0);
    float3 rightDir = normalize(cross(normalDir, upDir));
    // 重新计算后的上方
    upDir = normalize(cross(rightDir, normalDir));
    float3 localPos = center + rightDir * v.vertex.x + 
                                upDir * v.vertex.y + 
                                normalDir * v.vertex.z;

    o.vertex = UnityObjectToClipPos(localPos);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    return o;
}

8. 水效果

1. 深度贴图采样
  1. 定义变量sampler2D_float _CameraDepthTexture获得摄像机的深度图。

  2. 在顶点函数中计算出点对应的屏幕位置及深度。

    void vert(inout appdata_full v, out Input i)
    {
          
          
        UNITY_INITIALIZE_OUTPUT(Input, i);
            // 计算机屏幕位置
        i.proj = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
        // 记录其z值为视图坐标深度
        COMPUTE_EYEDEPTH(i.proj.z);
    }
    
  3. 获得屏幕空间下摄像机深度值。

    // tex2Dproj: 在tex2D的基础上,将输入的UV xy坐标除以其w坐标。这是将坐标从正交投影转换为透视投影。
    // LinearEyeDepth: Z缓冲区到线性深度
    // 默认获得的值是非线性的(将可视椎体转化为正方体),因此需要转换到线性空间
    // 总之,该操作可获得屏幕空间下该点的摄像机深度值
    half depth = LinearEyeDepth(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(IN.proj)).r);
    

    在这里插入图片描述
    在这里插入图片描述

  4. 用摄像机深度值减去当前对象的深度值,完成水深度的采样。

    // 将该点的摄像机深度值减去该对象纹理中该点的深度,获得的值即为该点到视图方向延伸到遮挡处的长度(深度)
    half deltaDepth = depth - IN.proj.z;
    

    在这里插入图片描述

注意:

  1. _CameraDepthTexture(摄像机深度纹理)采样的重要依据是 LightMode 为 ShadowCaster 的 Pass 通道,因此若想使用的shader被**_CameraDepthTexture**视作有效的遮挡,你必须为该shader编写 LightMode 为 ShadowCaster 的 Pass 通道(在上文中的投射阴影已经实现过,若懒得手动编写,你也可以直接UsePass "Standard/ShadowCaster"
  2. 或许需要为摄像机开启深度纹理: GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
2. 浪花
  1. 法线采样,移动法线图像。
  2. 将法线图 (x, y) 颠倒后再次采样,获得更好的法线效果。(注: (x, y) 颠倒将会原本作用于x轴的时间偏移改变为作用到y轴)
  3. 将一次采样的值作为偏移量进行二次采样,将打散法线图像,使得浪花变得更加细腻。
// 波浪法线(二次采样取偏移作为偏移,可将原本的法线打散,表现出更小的浪花)
float4 bumpOffset1 = tex2D(_NormalTex, IN.uv_NormalTex + float2(_WaterSpeed * _Time.y, 0));
float4 bumpOffset2 = tex2D(_NormalTex, float2(-IN.uv_NormalTex.y, -IN.uv_NormalTex.x) + float2(_WaterSpeed * _Time.y, 0));
float2 offset = UnpackNormal(((bumpOffset1 + bumpOffset2) / 2)).xy * _Refract;
float4 bumpColor1 = tex2D(_NormalTex, IN.uv_NormalTex + offset + float2(_WaterSpeed * _Time.y, 0));
float4 bumpColor2= tex2D(_NormalTex, float2(-IN.uv_NormalTex.y, -IN.uv_NormalTex.x) + offset + float2(_WaterSpeed * _Time.y, 0));

o.Normal = UnpackNormal((bumpColor1 + bumpColor2) / 2);
3. 自定义高光与漫发射
#pragma surface surf WaterLight vertex:vert alpha:blend noshadow
fixed4 LightingWaterLight(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    
    
    float diffuse = saturate(dot(normalize(lightDir), s.Normal));
    half3 halfDir = normalize(lightDir + viewDir);
    float nh = saturate(dot(halfDir, s.Normal));
    // 高光
    float spec = pow(nh, s.Specular * 128) * s.Gloss;
    fixed4 c;
    c.rgb = (s.Albedo * _LightColor0.rgb * diffuse + _SpecularColor.rgb * spec * _LightColor0.rgb) * atten;
    c.a = s.Alpha + spec * _SpecularColor.a;
    return c;
}
4. 海边的浪花
  1. 越浅的地方浪花偏移越大
  2. 采样噪声图像,用作发现海浪的纹理偏移
  3. 随时间在浪花图像上不断移动采样,浅的地方优先采样右侧,反之越靠左
  4. 利用公式1 - (sin() + 1) / 2)使得海浪获得淡入淡出的渐变
  5. 颜色叠加到原本的漫反射中
// 海边的浪花
// 越浅的地方浪花偏移越大
half waveOffset = 1 - saturate(deltaDepth / _WaveRange);
// 采样噪声图像,用作发现海浪的纹理偏移
fixed4 noiserColor = tex2D(_NoiseTex, IN.uv_NoiseTex);
// 随时间在浪花图像上不断移动采样,浅的地方优先采样右侧,反之越靠左
half waveOffsetAnim = waveOffset + _WaveAmplitude * (sin(_Time.x * _WaveSpeed + noiserColor.r));
// offset为波浪法线的二次采样偏移
fixed4 waveColor = tex2D(_WaveTex, float2(waveOffsetAnim, .5) + offset);
// 形成海浪的淡入淡出效果,* noiserColor.r 是为了让海浪的透明度并非处处相同
waveColor.rgb *= (1 - (sin(_Time.x * _WaveSpeed + noiserColor.r) + 1) / 2) * noiserColor.r;
// 双层效果
fixed4 waveColor2 = tex2D(_WaveTex, float2(waveOffsetAnim + _WaveDelta, 1) + offset);
waveColor2.rgb *= (1 - (sin(_Time.x * _WaveSpeed + _WaveDelta + noiserColor.r) + 1) / 2) * noiserColor.r;

// c 为水体本色
o.Albedo = c + (waveColor.rgb + waveColor2.rgb) * waveOffset;
5. 抓屏扰动
  1. 使用GrabPass{ "GrabPassTex" }抓屏
  2. 获取海浪的扰动法线作为顶点偏移
  3. 将计算的屏幕位置除以视图坐标深度,可得到抓屏图像在此处的uv值,将其与扰动法线形成的顶点偏移相加计算即可。
// 抓屏
float3 normal = UnpackNormal((bumpColor1 + bumpColor2) / 2);
float2 handoffset = normal.xy * _Distortion * GrabPassTex_TexelSize.xy;
fixed3 refrCol = tex2D(GrabPassTex, (handoffset * IN.proj.z + IN.proj.xy) / IN.proj.w).rgb;

o.Albedo = (c + (waveColor.rgb + waveColor2.rgb) * waveOffset) * refrCol;
6. 菲涅尔反射
  1. 获得反射与折射向量
  2. 计算值
fixed3 reflaction = texCUBE(_Cubemap, WorldReflectionVector(IN, normal)).rgb;
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(normalize(IN.viewDir), WorldNormalVector(IN, normal)
), 5);
fixed3 refrAndRefl = lerp(reflaction, refrCol, saturate(fresnel));

o.Albedo = (c + (waveColor.rgb + waveColor2.rgb) * waveOffset) * refrAndRefl;

9. 湿地效果

1. 原理
  1. 原始地面与水面的法线不同,金属性、平滑度,颜色均不同。
    在这里插入图片描述
void surf (Input IN, inout SurfaceOutputStandard o)
{
    
    
    // 遮罩纹理决定哪里是水,哪里是地面
    fixed wetness = tex2D(_WetMap, IN.uv_WetMap).r * _Wetness;

    // 颜色取正常颜色与水颜色的过渡
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * lerp(_Color, _WetColor, wetness);
    o.Albedo = c.rgb;
    // 法线取正常颜色与水颜色的过渡
    o.Normal = lerp(UnpackScaleNormal(tex2D(_Normal, IN.uv_Normal), _NormalScale), half3(0, 0, 1), wetness);
    // 金属质感过渡
    o.Metallic = lerp(_Metallic, _WetMetallic, wetness);
    // 平滑度过渡
    o.Smoothness = lerp(_Glossiness, _WetGlossiness, wetness);
    o.Alpha = c.a;
}

猜你喜欢

转载自blog.csdn.net/qq_50682713/article/details/125758881