Unity Shader入门精要中级

中级光照

可以在摄像机组件下设置渲染路径

image-20250316094010967

在之前我们场景之中只有一个平行光,在实际游戏中我们会需要处理数量更多,类型更加复杂的光源,更加重要的是我们需要得到阴影。

Unity如何处理这些光源的:
Unity里,渲染路径决定了光照是如何被应用到Unity Shader当中的,因此,如果我们需要和光源打交道,我们需要为每一个Pass指定它使用的渲染路径。
Unity这里有3种渲染路径:
前向渲染路径
延迟渲染路径
顶点照明渲染路径

大多数情况一个项目只会使用一个渲染路径。

可以在设置种选择项目渲染路径(但是我找不到书上的位置有这个选项)
image-20250316094952359

如果当前的显卡并不支持所选择的渲染路径,Unity就会自动使用更低一级的渲染路径,比如如果一个GPU不支持延迟渲染,那么Unity就会使用前向渲染。

Pass
{
	Tags {"LightMode" = "ForwardBase"}
}

钱箱渲染路径还有一种路径叫做ForwardAdd
image-20250316095251313

如果我们没有指定任何渲染路径(在Unity5x版本如果使用了前向渲染但又没有为Pass指定任何前向渲染适合的标签,就会被当成一个和顶点照明渲染路径等同的Pass) 那么一些光照变量很可能就不会被正确的赋值,我们计算出的效果也很有可能是错误的。

前向渲染路径

原理:
每进行一次完整的前向渲染,我们就需要渲染这个对象的渲染图元,并且计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲区来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值

image-20250316100159027

对于每一个逐像素光源,我们都会需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域之内,那么这个物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果。然后把这些光照结果混合起来得到最终颜色。

渲染引擎通常会限制每一个物体的逐像素光照的数目。

Unity中的前向渲染

image-20250316104619743

Pass不止可以用来计算逐像素光照,他也可以用来计算逐顶点等其他光照。这取决于光照计算所处流水线阶段以及计算时候使用的数学模型。当我们渲染一个物体的时候,Unity会计算哪一些光源照亮了它。以及这些光源照亮这个物体的方式。

Unity中前向渲染有三种照亮物体的方式:逐顶点处理,逐像素处理,球谐函数处理。而决定一个光源使用哪一种处理模式取决于它的类型和渲染模式。

光源类型:这个光是平行光还是其他类型的光源。
光源的渲染模式:这个光源是否是重要的。

如果我们把一个光照的模式设置成Important,意味着我们告诉Unity要将这个光源当成一个逐像素光源来进行处理。

前向渲染中,当我们渲染一个物体Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度对这些光源进行一个重要度排序。
其中一定数目的光源会按照逐像素的方式进行处理。

然后最多有4个光源按照逐顶点的方式进行处理。
剩下的光源可以按照SH(Spherical Harmonics,球谐光照)方式进行处理。

判断规则如下:
1 场景中最亮的平行光总是是按照逐像素进行处理的。
2 渲染模式被设置成Not Important的光源会按照逐顶点或者SH处理。
3 渲染模式被设置成Important的光源,会按照逐像素的方式进行处理。
4 如果根据以上规则得到的逐像素光源数量小于Quality Setting当中的逐像素光源数量(Pixel Light Count),就会有更多的光源以逐像素的方式进行渲染。

image-20250316102419234

光照计算将会在Pass当中进行,但是Pass被分成Base Path 和 Additional Path。

这两种Pass进行的标签和渲染设置以及常规光照计算如图
image-20250316102552489

在渲染设置中,我们需要使用
#pragma multi_compile_fwdbase 这样的编译指令。
这些编译指令保证Unity可以为相应类型的Pass生成所有需要的Shader变种。这些变种会处理不同条件下的渲染逻辑,例如是否使用lightmap,当前使用哪种光源类型等等。

在AddPass中使用命令替换#pragma multi_compile_fwdadd 编译指令开启阴影效果,但是这需要Unity在内部使用更多的Shader变种。

环境光和自发光一般都是在BasePass当中进行计算的,因为这种光一般只需要计算一次。而我们在Additional Pass来计算这两种光照,就会造成叠加多次环境光和自发光
image-20250316104553452

在AddPass中开启了混合,因为我们需要进行光照的叠加

这只是在通常情况下,渲染路径告诉Unity这个Pass在前向渲染当中的位置,然后底层的渲染引擎就会进行相关计算并且填充一些内部变量(比如_LightColor0)如何使用这些内置变量进行计算完全取决于开发者的选择。

前向渲染可以使用的内置光照变量
image-20250316103909893

image-20250316105030208

顶点照明渲染路径

顶点照明渲染路径通常在一个Pass当中就可以完成对于物体的渲染,在这个Pass当中我们会计算我们关心的所有光源对于这个物体的照明。并且这个计算是按照逐顶点的方式进行处理的。

对硬件的配置要求最少,运算性能最高,但是同时也是得到效果最差的一种类

image-20250316105807890

见书上P185

延迟渲染路径

当场景包含有大量实时光源前向渲染的性能就会急速下降,例如如果我们在场景当中的某一块区域放置了多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,我们就需要我这个区域的每一个物体执行多个Pass来计算不同光源对这个物体的光照结果。然而每执行一个Pass我们都会需要重新渲染一遍物体。但是很多计算实际上是重复的。

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

image-20250316110120397

延迟渲染的效率不依赖于场景的复杂度,而是和我们的屏幕空间的大小有关。

两种:
Unity5之前遗留的延迟渲染路径
Unity5.x当中使用的延迟渲染路径

如果游戏当中使用了大量的实时光照,那么我们可能希望选择延迟渲染路径,但是这种路径需要一定的硬件支持。

image-20250316111000469

对于延迟渲染路径来说它适合在场景当中光源数量很多的时候。
缺点在于:
不能支持真正的抗锯齿anti-aliasing功能
不能处理半透明物体
对显卡有一定的要求,如果要使用延迟渲染的话,显卡必须支持MRT,Shader Mode3.0以及以上,深度渲染纹理以及双面的模版缓冲。

当使用延迟渲染的时候Unity要求我们提供两个Pass
第一个Pass用于渲染G缓冲,在这个Pass当中我们要把物体的漫反射颜色,高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区当中,对于每一个物体来说这个Pass只会执行一次。

第二个Pass用于计算真正的光照模型,这个Pass会使用上一个Pass当中渲染的数据来计算最终的光照颜色,再存储到帧缓冲当中。
image-20250316111705264

延迟渲染路径可访问的内置变量和函数
这些变量都可以在UnityDeferredLibrary.cginc文件当中找到它们的声明。
image-20250316111936553

Unity官方文档中对渲染路径的比较

image-20250316112026495

光源

光源的属性有 位置、方向、颜色、强度、以及衰减等

平行光
最简单的光,没有一个唯一的位置,通过调整rotation来改变它的光源方向。
而且平行光导场景中所有点的方向都是一样的,这也是平行光名字的由来。

点光源
照亮空间有限,一个球体定义,向所有方向延伸
image-20250316112426547

聚光灯
照亮空间通用是有限的,但是是由空间中的一个椎体来决定
image-20250316112557633

在前向渲染当中处理不同的光源类型

// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter91_ForwardRendering"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _Specularity ("Specular", Color) = (1, 1, 1, 1)
        _Shininess ("Shininess", Range(8., 256.)) = 120
    }
    SubShader
    {
        Tags {"Queue" = "Geometry"}//设置渲染队列为Geometry,这样可以保证物体的前后顺序正确
        Pass
        {// Pass for ambient light & first pixel light (directional light)
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            
            #pragma multi_compile_fwdbase

            //这个定义可以保证我们在Shader当中使用的光照衰减等光照变量可以被正确的赋值

            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Color;
            fixed4 _Specularity;
            float _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                //o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {//每一个光源有五个属性:颜色,方向,位置,强度,衰减。
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 viewDir = normalize(UnityWorldToViewPos(i.worldPos).xyz);
                fixed3 halfDir = normalize(lightDir + viewDir);

                //fixed4 albedo = tex2D(_MainTex , i.uv);

                // 首先计算了环境的环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 
                // 计算了平行光的漫反射光
                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, lightDir));
                // 计算了平行光的镜面光
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);
                // 衰减
                fixed atten = 1.0;//平行光可以认为没有衰减
                
                fixed3 finalColor = ambient + (dffuse + specular) * atten;
                return fixed4(ambient + (dffuse + specular) * atten, 1);
            }
            
            ENDCG
        }

        Pass
        {
            Tags {"LightMode" = "ForwardAdd"}//注意要更改tag为ForwardAdd,否则不会进行第二次光照计算

            Blend One One //选择混合系数为1,即完全不透明
            //将计算得到的光照结果与原本的颜色进行相加
            //如果没有blend命令,additional pass会覆盖掉原本的颜色
            //常见的还有Blend SrcAlpha One

            //一般来说Add Pass的光照处理和Base Pass的处理方式和Base Pass的处理方式是一样的
            //只需要稍微做一些修改
            CGPROGRAM
            
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "AutoLight.cginc"//添加这个文件才可以进行光照衰减

            fixed4 _Color;
            fixed4 _Specularity;
            fixed _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                //o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {//每一个光源有五个属性:颜色,方向,位置,强度,衰减。
                fixed3 worldNormal = normalize(i.worldNormal);

                //fixed4 albedo = tex2D(_MainTex , i.uv);

                //————不用再计算环境光了,因为已经在Base Pass中计算过了
                //fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 

                //首先判断了当前处理的逐像素光源类型,这事通过使用#ifdef指令判断是否定义了
                //USING_DIRECTIONAL_LIGHT 这个宏来得到的
                //如果是平行光,那么unity的底层渲染引擎就会定义usingDirectionalLight这个宏
                //如果是点光源,那么就不会定义这个宏
                //得知是平行光之后,光源方向可以直接由_WorldSpaceLightPos0.xyz得到。
                //如果是点光源或者聚光灯,那么_WorldSpaceLightPos0.xyz表示的是世界坐标下的光源位置
                //而想要得到光源的方向的话,那么我们就需要用这个位置减去世界空间下的顶点位置。
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif
                fixed3 viewDir = normalize(UnityWorldToViewPos(i.worldPos).xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);

                // 计算了漫反射光
                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, worldLightDir));
                // 计算了镜面光
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);
                // 衰减


                //尽管我们可以使用数学表达式来计算点光源和聚光灯的衰减,但是这些计算往往涉及到开根号、除法等计算量较大的操作
                //Unity选择使用一张纹理作为查找表(Lookup Table,LUT)来存储衰减值
                //我们首先得到光源空间下的坐标,然后用这个坐标对衰减纹理进行采样得到衰减值。
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else 
                    //获取物体在光源坐标下的位置
                    float3 lightCoord = mul(unity_WorldToLight , float4(i.worldPos , 1)).xyz;
                    //光源可能是一个贴图,所以需要使用tex2D来获取衰减值,不过一般都是一个颜色
                    fixed atten = tex2D(_LightTexture0 , dot(lightCoord , lightCoord).rr).UNITY_ATTEN_CHANNEL;
                #endif
                
                fixed3 finalColor = (dffuse + specular) * atten;
                return fixed4((dffuse + specular) * atten, 1);
            }
            
            ENDCG
        }
    }
    //FallBack "Specular"
}

这段代码暂时还无法正常表现,我们在场景当中新建一个场景,放置一个球体,附着这个材质,然后给场景中添加3个不同的点光源,得到这样的效果

image-20250316143440764

这种效果怎么来的?
当我们创建一个光源的时候,默认情况下它的rendermode是auto。这意味着,unity会在后面为我们判断哪些光源会按照逐像素处理,而哪些按逐顶点或者SH的方式进行处理。由于我们没有更改Edit -> Project Settings -> Quality -> Pixel Light Count 当中的数值,因此默认情况下一个物体可以接受除最亮的平行光之外的4个逐像素光照。
在这个例子当中场景中包含了4个光源,其中一个是平行光,它会在Base Pass当中按照逐像素的方式被处理,其余3个都是点光源,由于它们的Render Mode为Auto而且数目正好又等于3 < 4,因此都会在Add Pass当中按照逐像素的方式被处理,每一个光源都会调用一次Add Pass。

我们还可以使用帧调试器(Frame Debugger)工具来查看场景的绘制过程。
image-20250316132308373

在这里插入图片描述

image-20250316143705161

Frame Debugger进行逐帧调试。渲染顺序是根据光源的重要程度排序的。

如果逐像素光源的数目很多的话,这个物体的additional pass就会被调用很多次,影响性能。

我们可以把光源的render mode设置成NotImportant来告诉Unity,我们不希望将这个光源当成逐像素处理。比如我们把3个点光源的render mode都设置成not important 那么我们就可以得到下面的结果

image-20250316144258292

由于我们在shader当中没有在base Pass当中计算逐顶点和SH光源,因此场景当中的四个点光源实际上不会对物体造成影响,如果把平行光的render mode也设置成not important,那么物体就仅仅会显示环境光的光照效果。

Unity当中的光照衰减

用于光照衰减的纹理
Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减

如果我们对光源使用了cookie,那么衰减查找纹理是_LightTextureB0

我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源不同位置的衰减值。其中0 0代表了与光源位置重合的时候的衰减值,而1,1点表面了在光源空间当中所关心的距离最远的点的衰减。

我们要先得到该点在光源空间中的位置,这是通过_LightMatrix0变换得到的,我们需要将 _LightMatrix0和世界空间当中的顶点坐标相乘即可得到光源空间当中的相应位置。

之后我们可以使用这个坐标的模的平方来对衰减纹理进行采样,得到衰减值。
(之所以没有使用距离值采样是因为这样避免了开方操作)

然后我们使用UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量。得到了最终的衰减值
image-20250316145258378

使用数学公式进行衰减

纹理采样的方法可以减少衰减时的复杂度
但是仍然可以使用数学来进行衰减计算
比如
image-20250316145417995

我们无法再shader当中通过内置变量得到光源的方位聚光灯的朝鲜张开角度等信息,得到的效果常常不尽人意。

Unity当中的阴影

阴影是如何实现的。

在实时渲染的时候我们最常使用的是一种叫做shadowMap的技术,这种技术首先会把摄像机的位置放在与光源位置重合的位置上,那么场景当中这个光源的阴影区域就是哪些摄像机看不到的地方,unity使用这种技术。

在前向渲染路径当中,如果场景当中最重要的平行光开启了阴影,Unity就会为这个光源计算它的阴影映射纹理(shadowMap) ,这张阴影映射纹理本质上是一张深度图,它记录了这个光源的位置出发,看到的场景当中距离它最近的表面位置(深度信息)。

在计算阴影映射纹理的时候如何判断距离它最近的表面距离?

1,把摄像机放到光源的位置上,然后按照正常的渲染流程,即调用base pass 和additional pass来更新深度信息,得到阴影映射纹理,但是这种方法会对性能造成一定的浪费,因为我们只需要知道深度信息

Unity选择使用一个额外的pass来专门更新光源的阴影映射纹理,这个pass就是lightmode标签被设置为shadowcaster的pass

这个pass的渲染目标不是帧缓存,而是阴影映射纹理(或者深度纹理)。

Unity先把摄像机放到光源的位置上,然后调用这个Pass,通过对顶点变换后得到光源空间下的位置,并且据此来输出深度信息到阴影映射纹理当中

因此当开启了光源的阴影效果之后底层渲染引擎会先在当前渲染物体的UnityShader当中找到LightMode为ShadowCaster的Pass,如果没有他就会在Fallback指定的UnityShader当中寻找,如果仍然没有找到,该物体就无法向其它的物体投射投影。(但是它仍然可以接受来自其他物体的阴影)

当找到了一个LightMode为ShadowCaster的Pass之后,Unity就会使用这个Pass更新光源的阴影映射纹理

在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass当中把顶点位置变换到光源空间下,得到他在光源空间中的三维位置信息。

然后我们使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理的深度信息,如果该深度值小于该点的深度值(通常由z分量得到)那么说明该点位于阴影当中。

image-20250316154827600

2,在Unity当中Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术

屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法,但是需要注意的是,不是所有平台Unity都会采用这种技术,因为屏幕空间的阴影映射需要显卡支持MRT,一些移动平台往往不支持

Unity会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图如果摄像机的深度图当中记录的表面深度大于转换到阴影映射纹理当中的深度值,就说明该表面虽然是可见的,但是却处于光源的阴影当中。

通过这种方式,阴影图就包含了屏幕空间中所有有阴影的区域,如果我们想要一个物体接收来自其他物体的阴影,只需要在shader中对阴影图进行采样。由于阴影图是在屏幕空间下的,因此我们首先要把表面坐标从模型空间转换到屏幕空间当中。

然后使用这个坐标对阴影图进行采样即可。

一个物体接收来自其他物体的阴影,以及它乡其他物体投射阴影是两个过程:

如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader当中对阴影映射纹理(包括屏幕空间当中的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。

如果我们想要一个物体向其它物体投射阴影,就必须把物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样的时候可以得到这个物体的相关信息。在unity当中,这个过程是通过为这个物体执行LightMode为ShadowCaster的Pass来实现的,如果使用了屏幕空间的投影映射技术,Unity还会使用这个Pass产生一张摄像机的深度纹理。

不透明物体的阴影

image-20250316161229118

Cast Shadows可以被设置成On或者Off关闭,如果开启了CastShadows属性
那么Unity就会把物体加入到光源的阴影映射纹理的计算当中。让其他物体在对阴影映射纹理进行采样的时候得到该物体的相关信息。

Receive Shadows则可以选择是否让物体接收来自其他物体的阴影,如果没有开启receive shadows,那么当我们调用unity的内置宏和变量计算阴影的时候这些宏通过判断物体有没有开启接收阴影接收的功能,就不会在内部为我们计算阴影。

我直接创建的正方体是没有阴影的
image-20250316162013993
这是因为我把Fallback’注释掉了
image-20250316162032367
如果取消注释
image-20250316162050746

image-20250316162112475

在specular通过不断回调中寻找到了lightmode为shadow caster的pass

image-20250316162252101

由于面剔除,平面背面没有渲染阴影
image-20250316162546279
我们需要把它Cast Shadows 设置成two sides
image-20250316162714783

image-20250316162705515

但是我们看到正方体并没有接收到来自平面的阴影

SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION是计算阴影时的“三剑客”,这些内置宏帮助我们在必要的时候计算光源的阴影。

image-20250316163902004

image-20250316163912791

上面代码实际上是unity为了处理不同的光源类型,不同平台而定义了多个版本的宏,在前向渲染当中
SHADOW_COORDS实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量

而TRANSFER_SHADOW的实现会根据不同平台而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过片段是否定义了UNITY_NO_SCREENSPACE_SHADOWS)来得到。TRANSFER_SHADOW会调用内置的ComputeScreenPos函数来计算 _ShadowCoord。如果这个平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术。这个宏会把顶点坐标从模型空间变换到光源空间后存储到 _ShadowCoord中

然后SHADOW_ATTENUATION负责使用_ShadowCoord对相关的纹理进行采样,得到阴影信息

注意到上面的内代码在最后定义了在关闭阴影时的处理代码。当关闭阴影之火SHADOW_COORDS、TRANSFER_SHADOW实际没有发生作用,SHADOW_ATTENUATION会等于1

image-20250316164808839

完成操作之后我们只要把阴影值shadow鹅漫反射以及高光反射颜色相乘就行。

Shader "Custom/Chapter92_ShadowRendering"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _Specularity ("Specular", Color) = (1, 1, 1, 1)
        _Shininess ("Shininess", Range(8., 256.)) = 120
    }
    SubShader
    {
        Tags {"Queue" = "Geometry"}
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            //#include "AutoLight.cginc"
            #include "AutoLight.cginc"//添加一个新的内置文件计算阴影

            fixed4 _Color;
            fixed4 _Specularity;
            float _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;

                SHADOW_COORDS(2) //添加一个新的宏,用于声明阴影贴图的UV坐标
                //它的参数需要下一个可用的插值寄存器的索引值。
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                //计算阴影贴图的UV坐标
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                TRANSFER_SHADOW(o); //调用宏,将阴影贴图的UV坐标传给寄存器2

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {

                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 viewDir = normalize(UnityWorldToViewPos(i.worldPos).xyz);
                fixed3 halfDir = normalize(lightDir + viewDir);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 
                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, lightDir));
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);

                fixed atten = 1.0;
                fixed shadow = SHADOW_ATTENUATION(i); //调用宏,计算阴影贴图的衰减值
                
                fixed3 finalColor = ambient + (dffuse + specular) * atten * shadow;
                return fixed4(ambient + (dffuse + specular) * atten * shadow, 1);
            }
            
            ENDCG
        }

        Pass
        {
            Tags {"LightMode" = "ForwardAdd"}

            Blend One One 

            CGPROGRAM
            
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Color;
            fixed4 _Specularity;
            fixed _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {//每一个光源有五个属性:颜色,方向,位置,强度,衰减。
                fixed3 worldNormal = normalize(i.worldNormal);

                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif
                fixed3 viewDir = normalize(UnityWorldToViewPos(i.worldPos).xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);

                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, worldLightDir));
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);
                
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else 
                    float3 lightCoord = mul(unity_WorldToLight , float4(i.worldPos , 1)).xyz;
                    fixed atten = tex2D(_LightTexture0 , dot(lightCoord , lightCoord).rr).UNITY_ATTEN_CHANNEL;
                #endif
                
                fixed3 finalColor = (dffuse + specular) * atten;
                return fixed4((dffuse + specular) * atten, 1);
            }
            
            ENDCG
        }
    }
    FallBack "Specular"
}

image-20250316180721012

使用帧调试器查看阴影的绘制过程

统一管理光照衰减和阴影

光照衰减因子 和 阴影值及光照结果相乘得到最终渲染结果
Unity提供了内置宏UNITY_LIGHT_ATTENUATION可以同时计算两个信息。

Shader "Custom/Chapter93_ShadowAttenuation"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _Specularity ("Specular", Color) = (1, 1, 1, 1)
        _Shininess ("Shininess", Range(8., 256.)) = 120
    }
    SubShader
    {
        Tags {"Queue" = "Geometry"}
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            #include "AutoLight.cginc"//添加一个新的内置文件计算阴影

            fixed4 _Color;
            fixed4 _Specularity;
            float _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;

                SHADOW_COORDS(2) //添加一个新的宏,用于声明阴影贴图的UV坐标
                //它的参数需要下一个可用的插值寄存器的索引值。
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                //计算阴影贴图的UV坐标
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                TRANSFER_SHADOW(o) //调用宏,将阴影贴图的UV坐标传给寄存器2

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed shadow = SHADOW_ATTENUATION(i); //调用宏,计算阴影贴图的衰减值

                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 viewDir = normalize(UnityWorldToViewPos(i.worldPos).xyz);
                fixed3 halfDir = normalize(lightDir + viewDir);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 
                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, lightDir));
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);

                fixed atten = 1.0;
                
                fixed3 finalColor = ambient + (dffuse + specular) * atten * shadow;
                return fixed4(ambient + (dffuse + specular) * atten * shadow, 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 "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Color;
            fixed4 _Specularity;
            fixed _Shininess;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                SHADOW_COORDS(2)
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //fixed shadow = SHADOW_ATTENUATION(i); //调用宏,计算阴影贴图的衰减值
                fixed3 worldNormal = normalize(i.worldNormal);

                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);

                fixed3 dffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal, worldLightDir));
                fixed3 specular = _LightColor0.rgb * _Specularity.rgb * pow(max(0 , dot(worldNormal, halfDir)) , _Shininess);
                
                // #ifdef USING_DIRECTIONAL_LIGHT
                //     fixed atten = 1.0;
                // #else 
                //     float3 lightCoord = mul(unity_WorldToLight , float4(i.worldPos , 1)).xyz;
                //     fixed atten = tex2D(_LightTexture0 , dot(lightCoord , lightCoord).rr).UNITY_ATTEN_CHANNEL;
                // #endif
                
                UNITY_LIGHT_ATTENUATION(atten , i , i.worldPos);
                //这次我们使用了内置宏来计算光照衰减和阴影,
                //他接受三个参数,他会将光照衰减和阴影值相乘之后的结果存储到第一个参数当中。
                //我们并没有声明第一个参数1atten,这是因为这个宏会帮我们声明这个变量。
                //第二个参数是结构体v2f,它包含了顶点位置、法线、世界坐标、阴影贴图的UV坐标。
                //这个参数会传递给SHADOW_ATTENUATION宏,它会计算阴影贴图的衰减值。
                //第三个参数是世界空间的坐标,这个参数会用于计算光源空间下的坐标,
                //再对光照衰减纹理采样来得到光照衰减。
                fixed3 finalColor = (dffuse + specular) * atten;
                return fixed4((dffuse + specular) * atten, 1);
                //如果我们要给additional pass当中添加阴影效果,就需要使用
                //#pragma multi_compile_fwdadd_fullshadow指令。这样一来,unity也会为这些额外的逐像素光源计算阴影。
            }
            
            ENDCG
        }
    }
    FallBack "Specular"
}

对比效果前者为之前的代码,后者为之后的代码
image-20250316190908662

image-20250316190758348

透明度物体的投影

透明物体的实现通常会用到透明度测试或者透明度混合,我们需要小心设置这些物体的fallback
透明度测试的处理要在片元着色器舍弃某些片元

image-20250316192048380

代码如下

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter94_TransparentShadowTest"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5 
    }
    SubShader
    {
        Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}

        Pass
        {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "AutoLight.cginc"//++

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Cutoff;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float3 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
                SHADOW_COORDS(3)//++
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.texcoord , _MainTex);
                TRANSFER_SHADOW(o) //++
                return o;
            }

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

                fixed4 texColor = tex2D(_MainTex , i.uv);

                clip(texColor.a - _Cutoff);
                if(texColor.a - _Cutoff < 0.0f)
                {
                    discard;
                }
                fixed3 albedo = texColor.rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

                UNITY_LIGHT_ATTENUATION(atten, i, i.pos); //++

                return fixed4(ambient + diffuse * atten, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Transparent/Cutout/VertexLit" //++
}

image-20250316192442163

为使用透明度混合的物体添加阴影是一件比较困难的事情,Unity所有内置的的透明度混合的UnityShader都没有包含阴影投射的Pass这意味着这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其它物体投射投影,头痒它们也不会接受来自其他物体的投影。

由于透明度混合需要关闭深度写入由此带来的问题也影响了阴影的生成,总体来说,要想为这些半透明物体产生正确的阴影,需要在每一个光源空间下仍然严格按照从后往前的顺序进行渲染。这会让阴影处理变得非常复杂,而且也会影响性能。因此在unity当中所有内置的半透明shader是不会产生任何的阴影效果的。

当然我们也可以使用一些技巧强制为半透明物体生成阴影,通过把fallback设置成vertexlit,diffuse这些不透明物体使用的UnityShader,这样unity就会在他的fallback当中找到一个阴影投射的pass,然后我们可以通过物体的mesh renderer组件上的cast shadows和receive shadows选项来控制是否需要向其他物体投射或者接受阴影。

image-20250316193323694

中级纹理

立方体纹理

在图形学当中,立方体纹理是环境映射的一种实现方式。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境

立方体纹理一共包含了6张图像,这些图像对应了一个立方体的六个面,立方体纹理的名称也由此而来。立方体的每个面表示沿着世界空间下的轴向

对立方体纹理采用我们需要提供一个三维的纹理坐标。

这个三维纹理坐标表示了我们在世界空间下的一个3D方向。这个方向矢量从立方体的中心出发,当它向外部延伸的时候,就会和立方体的六个纹理之一发生相交,而采样得到的结果就是由该交点计算而来。

使用立方体纹理的好处在于它的实现简单快速,而且得到的效果也比较好。

但是它也有一些缺点

1 例如当场景当中引入了新的物体、光源、或者物体发生移动的时候我们就需要重新生成立方体纹理。

2 除此之外,立方体纹理也仅可以反射环境,但是不能反射使用了该立方体纹理的物体本身。

因为立方体纹理不可以模拟多次反射的结果,例如两个金属球互相反射的情况。

由于这样的原因想要得到令人信服的渲染结果,我们应该尽量对凸面体而不要对凹面体使用立方体纹理、

立方体纹理的应用:skybox,环境映射

天空盒

天空盒式游戏中用于模拟背景的一种方法
Unity当中使用天空盒,我们需要创建一个skybox材质,再把它献给该场景的相关设置即可。
image-20250316201055290

image-20250316203027286

image-20250316203100597

最后得到的效果如下
image-20250316203304749

创建用于环境映射的立方体纹理

除了天空盒子,立方体纹理当中最常见的用处是用于环境映射。通过这种方法,我们模拟出金属的质感
(如图在blender,有环境映射和没有环境映射的区别)
image-20250316203906053

image-20250316203918889

Unity当中创建用于环境映射的立方体纹理的方法有三种:
第一种方法是直接由一些特殊布局的纹理创建
第二种方法是手动创建一个cubemap资源,再把六张图赋值给他
第三种方法是脚本生成

如果使用第一种方法,我们需要提供一张具有特殊布局的纹理,例如类似正方体展开图的交叉布局、全景布局等等。然后我们只需要把这个纹理的Texture Type设置成CubeMap就可以了,Unity会为我们做好剩下的事情,在基于物理的渲染当中,我们通常会使用一张HDR图像来生成高质量的Cubemap。

第二种方法是Unity5之前的版本中使用的方法。我们首先需要在项目资源中创建一个cubemap,然后把六张纹理拖拽

第一种方法可以对纹理数据进行压缩,并且支持边缘修正,光滑反射和HDR等功能。

我们希望根据物体在场景中的位置不同,生成它们各自不同的立方体纹理。这时我们就可以在Unity中使用脚本来创建。这是通过利用Unity提供的Camera.RenderToCubemap函数来实现的。Camera.RenderToCubemap函数可以把从任意位置观察到的场景图像存储到6张图像当中,从而创建出这个位置上对应的立方体纹理。

using UnityEngine;
using UnityEditor;
using System.Collections;

public class RenderCubemapWizard : ScriptableWizard {
	
	public Transform renderFromPosition;
	public Cubemap cubemap;
	
	void OnWizardUpdate () {
		helpString = "Select transform to render from and cubemap to render into";
		isValid = (renderFromPosition != null) && (cubemap != null);
	}
	
	void OnWizardCreate () {
		// create temporary camera for rendering
		GameObject go = new GameObject( "CubemapCamera");
		go.AddComponent<Camera>();
		// place it on the object
		go.transform.position = renderFromPosition.position;
		// render into cubemap		
		go.GetComponent<Camera>().RenderToCubemap(cubemap);
		
		// destroy temporary camera
		DestroyImmediate( go );
	}
	
	[MenuItem("GameObject/Render into Cubemap")]
	static void RenderCubemap () {
		ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
			"Render cubemap", "Render!");
	}
}

这段代码在指定位置生成了一个带有camera组件的游戏对象,并且把它所观察到的地方渲染到指定的cubemap里面

关于具体用这个脚本生成cubemap的操作,见书,这是最终效果

image-20250316210422714

image-20250316210607488

反射
使用了反射效果的物体通常看起来都像镀一层金属

我们需要通过入射光线的方向和表面法线方向来计算法向方向。再利用反射方向对立方体纹理采样即可。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter10_0_Reflection"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _ReflectColor ("Reflection Color", Color) = (1,1,1,1)
        _ReflectAmount ("Reflect Amount", Range(0,1)) = 1
        _Cubemap ("Reflection Cubemap" , Cube) = "_Skybox" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry" }
        Pass
        {
            Tags { "LightMode"="ForwardBase" } //+

            CGPROGRAM
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag

			#include "Lighting.cginc"
			#include "AutoLight.cginc"
            
            fixed4 _Color;
            fixed4 _ReflectColor;
            float _ReflectAmount;
            samplerCUBE _Cubemap; //这是一个新的类型,表示一个立方体贴图

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldRefl : TEXCOORD0;//将顶点上的反射向量传递到片段着色器
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;

                SHADOW_COORDS(4) //声明4个shadowmap坐标
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);

                //在世界空间中计算视线方向的反射向量

                o.worldRefl = reflect(-o.worldViewDir , o.worldNormal);

                TRANSFER_SHADOW(o);//计算阴影

                return o;
            }

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

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
                fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb * _ReflectAmount;
                //在世界空间中使用反射方向来得到cubemap
                UNITY_LIGHT_ATTENUATION(atten , i , i.worldPos);//将外界颜色与阴影进行混合并且进行衰减
                
                //将漫反射颜色与反射颜色进行混合
                fixed3 color = ambient + lerp(diffuse , reflection , _ReflectAmount) * atten;

                return fixed4(color , 1.0);
            }
            ENDCG
        }
    }
    FallBack "Reflective/VertexLit"
}

image-20250316215830330

折射

image-20250316220049723

一般来说当得到了折射方向我们就会直接使用它来对立方体纹理进行采样,但是这是不符合物理规律的。对于一个透明物体来说,一种更加准确的模拟方法需要计算两次折射

一次是光线进入到物体内部的时候。另一次则是光线从物体内部射出的时候,一般来说我们模拟一次就可以达到我们想要的效果。

image-20250316230543714

image-20250316230753183

image-20250316232750424

image-20250316232953088
image-20250316233014084
image-20250316233054013
Unity提供给了我们非常方便的函数来处理折射

image-20250317002302965

Shader "Custom/Chapter10_2_Refraction"
{
    Properties
    {
        _Color ("Color Tint" , Color) = (1,1,1,1)
        _RefractColor ("Refraction Color", Color) = (1,1,1,1)
        _RefractAmount ("Refraction Amount", Range(0 , 1)) = 1
        _RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5//只需要折射一次,玻璃的折射率一般是1.5,这里是相对折射率,则填0.5
        _Cubemap ("Refraction Cubemap", Cube) = "_Skybox" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry" }
        
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            #include "AutoLight.cginc"


            fixed4 _Color;
            fixed4 _RefractColor;
            float _RefractAmount;
            float _RefractRatio;
            samplerCUBE _Cubemap;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldRefra : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;
                SHADOW_COORDS(4)
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.worldViewDir = normalize(UnityWorldSpaceViewDir(o.worldPos));

                //计算反射
                //o.worldRefl = reflect(-normalize(o.worldPos) , o.worldNormal); //语法为reflect(入射方向direction, 顶点上的法线normal)

                //已知空气折射率1,玻璃折射率为_RefractRatio,则相对折射率是1/_RefractRatio
                //再计算折射
                o.worldRefra = refract(-normalize(o.worldViewDir) , normalize(o.worldNormal) , _RefractRatio); 
                //语法为refract(入射方向direction, 法线normal, 相对折射率ratio)

                TRANSFER_SHADOW(o);
                return o;
            }

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

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 refraction = texCUBE(_Cubemap, i.worldRefra).rgb * _RefractColor.rgb;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

                fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
                //我们没有对i.worldRefr进行归一化操作,因为对于立方体反射

                return fixed4(color, 1);
            }
            ENDCG
        }
    }
    FallBack "Reflective/VertexLit"
}

菲涅尔反射

image-20250317110759631

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter10_3_Fersnel"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _FresnelScale("Fresnel Scale" , Range(0 , 1)) = 0.5
        _Cubemap("Reflection Cubemap" , Cube) = "_Skybox" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }//中间不需要添加逗号

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Color;
            float _FresnelScale;
            samplerCUBE _Cubemap;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldViewDir : TEXCOORD2;
                float3 worldRefl : TEXCOORD3;
                SHADOW_COORDS(4)//这个参数实际上就是shadowmap的uv坐标,4代表声明的是TEXCOORD4,随意赋值会发生报错
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos( v.vertex);
                o.worldNormal = mul(v.normal , (float3x3)unity_WorldToObject);
                o.worldPos = mul(unity_ObjectToWorld , v.vertex).xyz;
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
                o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldViewDir = normalize(i.worldViewDir);
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

                fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb; // 反射光照

                // 利用公式来计算菲涅耳反射,并且使用结果值混合漫反射光照和反射光照

                fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldNormal , worldLightDir) , 5);

                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0 , dot(worldNormal , worldLightDir));

                //在漫反射和镜面反射之间使用fresnel进行插值混合,注意fresnel可能大于1需要进行限制
                fixed3 color = ambient + lerp(diffuse , reflection , saturate(fresnel)) * atten;

                return fixed4(color , 1);
            }
            ENDCG
        }
    }
    FallBack "Reflective/VertexLit"
}

image-20250317113338392

通过滑动条调整物体的漫反射强度

渲染纹理

渲染纹理一般用来模拟镜子效果
创建一个新场景
在project视图里面创建shader,material,以及一个Render Texture
image-20250317122901192

为了得到镜子内的图像我们需要新创建一个摄像机调整它的视角让其显示我们的镜子想要的内容。这个摄像机不需要直接显示在屏幕上,而是用于渲染到纹理

我们需要把创建的Render Texture拖拽到摄像机的Target Texture上

之后注意要把render texture拖到material里面才有效果

image-20250317123308060

image-20250317124309070
但是这仅仅是渲染出摄像机照片然后贴到墙上而已,并不是真正的镜子
image-20250317124345135

Shader "Custom/Chapter10_4_Mirror"
{
    Properties
    {
        _MainTex("Main Texture" , 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.uv.x = 1 - o.uv.x;
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                return tex2D(_MainTex , i.uv);
            }

            ENDCG
        }

    }
    FallBack "Specular"
}

在unity当中,我们可以使用一种特殊的pass来完成获取屏幕图像的目的,这就是GrabPass。当我们在Shader当中定义了一个GrabPass之后,Unity会把当前屏幕的图像绘制在一个纹理当中,以便我们在后续的Pass当中访问它,我们通常会使用GrabPass来实现诸如玻璃等透明材质的模拟

与简单的透明混合不同,使用GrabPass可以让我们对物体后面的图像进行更加复杂的处理。例如使用法线来模拟折射的效果,而不再是简单的和原屏幕颜色进行混合

需要注意在使用GrabPass的时候,我们需要额外小心物体的渲染队列设置,正如之前所说的一样,GrabPass通常用于渲染透明物体,尽管代码里面不包含混合指令,但是我们往往需要把物体的渲染对了,这样才可以保证当渲染这个物体的时候,所有的不透明物体都已经被绘制在了屏幕上,从而获取正确的屏幕图像

image-20250317125648078

image-20250317125659399

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter10_4_Glass"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {} //是该玻璃的材质纹理
        _BumpMap ("Normal Map", 2D) = "bump" {} //玻璃的法线纹理
        _CubeMap ("Environment Map", Cube) = "_Skybox" {} //模拟反射的环境纹理
        _Distortion ("Distortion", Range(0, 100)) = 10 //控制模拟折射时候的图像模拟程度
        _RefractAmount ("Refract Amount", Range(0, 1)) = 1.0 //用于控制折射程度
        //当RefractAmount=1时,玻璃只包含折射效果,当RefractAmount=0时,玻璃只包含反射效果
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }
        //看似矛盾,实际上服务于不同的需求
        //把Queue设置成Transparent,可以确保该物体被渲染时其他不透明物体已经渲染完成,否则难以正确得到"透过玻璃看到的图像"
        //而设置RenderType则是为了在使用着色器替换的时候,这个物体可以在需要的时候被正确渲染。
        //这通常发生在我们需要得到摄像机的深度和法线纹理时
        
        //定义了一个可以抓取屏幕图像的Pass,这个Pass中我们定义了一个字符串
        //这个字符串内部的名称决定了抓得到的屏幕图像将会被存入哪个纹理当中
        //实际上我们可以省略字符串,但是直接声明纹理名称的方法往往可以得到更高的性能。
        GrabPass {"_RefractionTex"}

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #include "Lighting.cginc"

            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            samplerCUBE _CubeMap;
            float _Distortion;
            float _RefractAmount;

            //这两个变量对应了在使用GrabPass时候指定的纹理名称,_RefractionTex_TexelSize可以让我们得到该纹理的纹素(纹理像素?大小
            //例如一个大小为256x512的纹理,它的纹素大小就是1/256,1/512。我们需要在对屏幕图像的采样坐标偏移的时候使用这个变量
            sampler2D _RefractionTex;
            float4 _RefractionTex_TexelSize;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 scrPos : TEXCOORD0;
                float4 uv : TEXCOORD1;
                float4 TtoW0 : TEXCOORD2;
                float4 TtoW1 : TEXCOORD3;
                float4 TtoW2 : TEXCOORD4;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                //调用内置的ComputeGrabScreenPos函数来得到对应被抓取的屏幕图像的采样坐标
                //相比ComputeScreenPos的不同在于针对平台差异造成的采样坐标问题进行了处理
                o.scrPos = ComputeGrabScreenPos(o.pos);

                o.uv.xy = TRANSFORM_TEX(v.texcoord , _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);

                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                o.TtoW0 = float4(worldTangent.x , worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y , worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z , worldBinormal.z, worldNormal.z, worldPos.z);

                return o;
            }
            
            float4 frag(v2f i) : SV_Target
            {
                //采样主纹理
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));//对法线纹理进行采样,得到切线空间下的法线方向

                //利用这个法线方向 计算在切线空间当中的法线偏移量,模拟折射的效果
                //_Distortion越大,偏移量越大,玻璃背后的物体看起来变形程度越大。
                float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                i.scrPos.xy = offset + i.scrPos.xy;
                //通过scrPos透视除法得到真正的屏幕坐标,再用这个坐标对屏幕图像_Refractiontex进行采样,得到模拟的反射颜色
                fixed3 refrCol = tex2D(_RefractionTex , i.scrPos.xy / i.scrPos.w).rgb;

                //将法线转换成世界坐标,并且计算反射结果
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
                fixed3 reflDir = reflect(-worldViewDir, bump);
                fixed4 texColor = tex2D(_MainTex, i.uv.xy);
                fixed3 reflCol = texCUBE(_CubeMap, reflDir).rgb * texColor.rgb;

                fixed3 finalColor = reflCol * _RefractAmount + refrCol * (1 - _RefractAmount);
                return fixed4(finalColor, 1);
            }
            ENDCG
        }
    }
    FallBack "Reflective/VertexLit"
}

image-20250317134203991

image-20250317134729622

得到一个这样的结果
image-20250317134836101

image-20250317135400438

渲染纹理vsGrabPass
Grab好处:实现简单。只需要写几行代码

渲染纹理好处:效率更好,在移动设备上使用渲染纹理可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再次渲染一遍,但是我们可以通过调整摄像机的渲染层来减少第二次渲染时候的场景大小,或者控制摄像机是否开启,而GrabPass虽然不会重新渲染场景,但是它往往需要CPU直接读取后备缓冲中的数据,破坏了CPU和GPU之间的并行性,这是比较好使的

Unity引入了命令缓冲,这允许我们扩展渲染流水线,使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理当中,然后在那里进行一些额外的操作,例如模糊等等,最后把图像传递给需要使用它的物体进行处理和显示。

程序纹理

程序纹理指的是使用计算机生成的图像

好处是可以使用各种参数来控制纹理的外观,这些属性不仅仅是哪些颜色的属性,甚至可以是完全不同类型的图案属性,这使得我们可以得到更加丰富的动画和视觉效果

在Unity当中实现简单的程序纹理

image-20250317140144613

image-20250317140528163

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode] //在编辑器模式下执行
public class ProceduralTextureGeneration : MonoBehaviour
{
    public Material material = null; //材质球

    #region 材质球属性

    [SerializeField, SetProperty("textureWidth")]
    //使用了插件https://github.com/LMNRY/SetProperty
    private int m_textureWidth = 512; //纹理宽度
    public int textureWidth
    {
        get
        {
            return m_textureWidth;
        }
        set
        {
            m_textureWidth = value;
            _UpdateMaterial();
        }
    }
    [SerializeField, SetProperty("backGroundColor")]
    private Color m_backGroundColor = Color.white; //背景颜色
    public Color backGroundColor
    {
        get
        {
            return m_backGroundColor;
        }
        set
        {
            m_backGroundColor = value;
            _UpdateMaterial();
        }
    }
    [SerializeField, SetProperty("circleColor")]
    private Color m_circleColor = Color.yellow; //图案颜色
    public Color circleColor
    {
        get
        {
            return m_circleColor;
        }
        set
        {
            m_circleColor = value;
            _UpdateMaterial();
        }
    }

    [SerializeField , SetProperty("blurFactor") ]
    private float m_blurFactor = 2.0f; //模糊程度
    public float blurFactor
    {
        get
        {
            return m_blurFactor;
        }
        set
        {
            m_blurFactor = value;
            _UpdateMaterial();
        }
    }
    #endregion
    
    private Texture2D m_generatedTexture; //生成的纹理

    void Start()
    {
        if(material == null)
        {
            Renderer renderer = gameObject.GetComponent<Renderer>();
            if(renderer == null)
            {
                Debug.LogWarning("不能找到一个网格来渲染");
                return;
            }
            material = renderer.sharedMaterial;
            // sharedMaterial:
            // 作用:获取或设置该 Renderer 共享的材质(Material)。
            // 影响范围:对 sharedMaterial 进行修改会影响所有使用该材质的对象,因为它是指向原始材质的引用。
            // 性能:更高效,因为它不会自动创建新的材质实例。
        }
        _UpdateMaterial();
    }
    private void _UpdateMaterial()
    {
        if(material != null)
        {
            m_generatedTexture = GenerateTexture();
            material.SetTexture("_MainTex" , m_generatedTexture);
        }
    }
    private Color _MixColor(Color color0, Color color1, float mix)
    {
        Color mixColor = Color.white;
		mixColor.r = Mathf.Lerp(color0.r, color1.r, mix);
		mixColor.g = Mathf.Lerp(color0.g, color1.g, mix);
		mixColor.b = Mathf.Lerp(color0.b, color1.b, mix);
		mixColor.a = Mathf.Lerp(color0.a, color1.a, mix);
        return mixColor;
    }
    private Texture2D GenerateTexture()
    {
        Texture2D proceduralTexture = 
        new Texture2D(textureWidth, textureWidth);

        float circleInterval = textureWidth / 4.0f; //圆的间隔
        float radius = textureWidth / 8.0f; //半径
        float edgeBlur = 1.0f / blurFactor;

        for(int w = 0 ; w < textureWidth ; w++)
        {
            for(int h = 0 ; h < textureWidth ; h++)
            {
                //使用背景颜色进行初始化
                Color pixelColor = backGroundColor;

                //依次画9个圆
                for(int i = 0 ; i < 3 ; i++)
                {
                    for(int j = 0 ; j < 3 ; j++)
                    {
                        //计算圆心坐标
                        Vector2 circleCenter = new Vector2(circleInterval * (i + 1) , circleInterval * (j + 1));
                        //计算当前像素与圆的距离
                        float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;
                        //模糊圆的边界
                        Color color = _MixColor
                        (circleColor , new Color(pixelColor.r, pixelColor.g, pixelColor.b, 0.0f)
                        , Mathf.SmoothStep(0f , 1.0f , dist * edgeBlur));
                        //在 Mathf.SmoothStep(0f, 1.0f, x) 中,如果 x(即 dist * edgeBlur)小于 0f,
                        // 函数将返回 0f,这意味着当前像素的颜色会完全混合成 circleColor。
                        //与之前得到的颜色进行混合
                        pixelColor = _MixColor(pixelColor , color , color.a);
                    }
                }
                proceduralTexture.SetPixel(w  , h , pixelColor);
            }
        }

        proceduralTexture.Apply();

        return proceduralTexture;
    }
}

生成的纹理如下

image-20250317150038217

Unity还有一种专门使用程序纹理的材质叫做程序材质,这类材质和我们之前使用的材质在本质上是一样的,不同的是,他们使用都是程序纹理,程序材质以及它使用的程序材质主要是在Substance Designer当中创建的。

让画面动起来

image-20250317150412727

纹理动画

序列帧动画

这种动画需要提供一张包含了关键帧图像的图像,例如这样

image-20250317150523876

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter11_1_ImageSequenceAnimation"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Image Sequence", 2D) = "white" {}
        _HorizontalAmount ("Horizontal Amount" , float) = 4
        _VerticalAmount ("Vertical Amount" , float) = 4
        //分别代表该图像在水平方向和竖直方向包含的关键帧个数,而_Speed代表动画播放速度 
        _Speed ("Speed" , Range(1 , 100)) = 30
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        //序列帧图像通常包含了透明管道,可以被当成是一个半透明对象来渲染
        Pass
        {
            Tags {"LightMode"="ForwardBase"}

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _HorizontalAmount;
            float _VerticalAmount;
            float _Speed;

            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord , _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float time= floor(_Time.y * _Speed); //即场景加载过后经历的时间,需要得到整数时间
                float row = floor(time / _HorizontalAmount);//找到当前的行索引
                float column = time - row * _HorizontalAmount;//找到当前的列索引

                half2 uv = i.uv + half2(column, -row);//在原来的uv上加上行列索引值,得到真正的采样坐标
                //如果row是正数,那么就会倒着播放

                uv.x /= _HorizontalAmount;//于此同时,还需要将uv的x坐标除以水平方向的关键帧个数,以便得到正确的采样位置
                uv.y /= _VerticalAmount;



                fixed4 c = tex2D(_MainTex , uv);
                c.rgb *= _Color.rgb;

                return c;
            }
            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

image-20250317155846336

image-20250317164855605

image-20250317164716505

滚动背景

注意摄像机要设置成正交

image-20250317170839398
添加前景和后景的图片得到这种效果

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter11_2_ScrollingBackground"
{
    Properties
    {
        _MainTex ("Base Layer (RGB) ", 2D) = "white" {}
        _DetailTex ("2nd Layer (RGB) ", 2D) = "white" {}
        _ScrollX ("Base layer Scroll Speed", Float) = 1.0
        _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
        _Multiplier ("Multiplier", Float) = 1.0
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            sampler2D _DetailTex;
            float4 _MainTex_ST;
            float4 _DetailTex_ST;
            float _ScrollX;
            float _Scroll2X;
            float _Multiplier;


            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); + frac(float2 (_ScrollX , 0.0) * _Time.y);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2 (_Scroll2X, 0.0) * _Time.y);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 firstLayer = tex2D(_MainTex , i.uv.xy);
                fixed4 secondLayer = tex2D(_DetailTex , i.uv.zw);

                fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
                c.rgb *= _Multiplier;

                return c;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

顶点动画

2D河流的模拟
它的原理通常就是使用正弦函数还模拟水流的波动效果

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter11_3_Water"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}//河流纹理
        _Color ("Color Tint", Color) = (1,1,1,1)//整体颜色
        _Magnitude ("Magnitude", Float) = 1//控制水流波动的幅度
        _Frequency ("Frequency", Float) = 1//波动频率
        _InvWaveLength ("Distortion Inverse Wave Length" , Float) = 10//控制波长的倒数
        _Speed ("Speed", Float) = 0.5//纹理移动速度
    }
    SubShader
    {
        //需要设置透明效果合适的SubShader标签:

        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"
        "DisableBatching" = "True" }
        //一些SubShader会在使用Unity的批处理功能时会出现问题,这个时候就可以使用这个标签直接指明
        //是否对这个SubShader使用批处理。而这些需要特殊处理的Shader通常就是包含了模型空间的
        //顶点动画的Shader,这是因为,批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失

        //而在本例中,我们需要在物体的模型空间下对定点位置进行偏移,因此在这里需要取消对这个Shader的批处理操作
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off
                //为了让水流的每一个面都能够正常显示

            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Magnitude;
            float _Frequency;
            float _InvWaveLength;
            float _Speed;

            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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



            v2f vert (a2v v)
            {
                v2f o;

                float4 offset;
                offset.yzw = float3(0,0,0);
                offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + 
                    v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
                    //计算顶点在x方向上的偏移值
                //Magnitute控制幅度
                //_Time.y为正弦函数的参数,随时间增加
                //偏移量由v.vertex.x、v.vertex.y、v.vertex.z三个参数决定,分别对应x、y、z轴
                o.pos = UnityObjectToClipPos(v.vertex + offset);

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv += float2(0.0 , _Time.y * _Speed);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target//由于操作的是顶点,片段着色器的代码较为简单
            {
                fixed4 c = tex2D(_MainTex , i.uv);
                c.rgb *= _Color.rgb;

                return c;
            }
            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

image-20250317175334780

image-20250317175432249

广告牌效果
一种常见的顶点动画,常用于渲染烟雾、云朵、闪光效果等
广告牌技术的本质是构建旋转矩阵,而我们知道一个变换矩阵需要三个基向量,广告牌技术使用的基向量通常就是表面法线、指向上的方向以及指向右的方向,除此之外,我们还需要一个锚点这个锚点在旋转过程当中是固定不变的,以此来确定多边形在空间中的位置。

广告牌技术的难点是如何根据需求 构建三个互相正交的基向量

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter11_4_BillBoard"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
        _Color ("Color Tint", Color) = (1,1,1,1)
        _VerticalBillboarding ("Vertical Billboarding", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True" }
        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off
            
            //让广告牌的每一个面都可以显示

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _VerticalBillboarding;


            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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

            v2f vert (a2v v)
            {
                v2f o;
                
                //Suppose the center in object space is fixed
                float3 center = float3(0,0,0);
                float3 viewer = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)).xyz;
                //选择模型空间的原点作为广告牌的锚点,并且利用内置变量获取模型空间下的视角位置
                
                float3 normalDir = viewer - center;//用观察位置和锚点计算目标法线方向
                normalDir.y = normalDir.y * _VerticalBillboarding; //根据_VerticalBillboarding控制垂直方向上的约束度
                normalDir = normalize(normalDir);
                //当_VerticalBillboarding为1的时候,意味着法线方向固定为视角方向,当_VerticalBillboarding为0的时候
                //意味着向上方向固定为0,1,0,最后我们需要对计算得到的法线方向进行归一化操作来得到单位矢量

                float3 upDir = abs(normalDir.y) > 0.999? float3(0,0,1) : float3(0,1,0); //选择垂直于广告牌的向量作为up向量
                float3 rightDir = normalize(cross(upDir, normalDir)); //选择右向量
                upDir = normalize(cross(normalDir, rightDir)); //重新计算up向量

                float3 centerOffs = v.vertex.xyz - center; //计算目标位置到锚点的偏移
                float3 localPos = center + rightDir * centerOffs.x +
                upDir * centerOffs.y + normalDir * centerOffs.z; //将目标位置转换到本地坐标系下

                o.pos = UnityObjectToClipPos(float4(localPos , 1)); //将本地坐标系下的位置转换到屏幕空间
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); //将纹理坐标转换到UV0
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
                c.rgb *= _Color.rgb;
                return c;
            }
            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

image-20250317193432333

image-20250317202051219

image-20250317203705402

注意事项

1,批处理总是会破坏在模型空间下的顶点动画效果。这个时候我们可以通过SubShader的,DisableBatching标签来强制取消对该UnityShader的批处理,不过取消批处理会带来一定的性能下降,增加了DrawCall,因此我们应该尽量避免模型空间下的一些绝对位置和方向来计算。
在广告牌的例子当中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏当中非常常见。

其次,如果我们想要对顶点动画的物体添加阴影,那么往往得不到正确的阴影效果,这是因为Unity的阴影绘制需要调用一个ShaowCaster Pass,而如果直接使用这些内置的ShadowCaster Pass,这个Pass中并没有进行相关的顶点动画,因此Unity会仍然按照原来的顶点位置来计算阴影,这并不是我们所希望看到的。这个时候我们需要提供一个自定义的ShadowCasterPass,在这个Pass我们将进行同样的顶点变换过程,需要注意的是,如果Transparent/VertexLit中没有定义ShadowCaster Pass,因此也就不会产生阴影。

image-20250317204547938

阴影投射的重点在于我们需要按照正常Pass的处理来剔除片元或者进行顶点动画,以使得阴影可以和物体正常的渲染结果相互匹配

注意要把平面的two side勾选上

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter11_5_WaterShadow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color Tint", Color) = (1,1,1,1)
        _Magnitude ("Magnitude", Float) = 1
        _Frequency ("Frequency", Float) = 1
        _InvWaveLength ("Distortion Inverse Wave Length" , Float) = 10
        _Speed ("Speed", Float) = 0.5
    }
    SubShader
    {
        //需要设置透明效果合适的SubShader标签:

        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"
        "DisableBatching" = "True" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Magnitude;
            float _Frequency;
            float _InvWaveLength;
            float _Speed;

            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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

            v2f vert (a2v v)
            {
                v2f o;

                float4 offset;
                offset.yzw = float3(0,0,0);
                offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + 
                    v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;

                o.pos = UnityObjectToClipPos(v.vertex + offset);

                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv += float2(0.0 , _Time.y * _Speed);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex , i.uv);
                c.rgb *= _Color.rgb;

                return c;
            }
            ENDCG
        }

        Pass
        {
            Tags { "LightMode"="ShadowCaster" }

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

            float _Magnitude;
            float _Frequency;
            float _InvWaveLength;
            float _Speed;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                V2F_SHADOW_CASTER;
            };

            v2f vert(a2v v)
            {
                v2f o;
                float4 offset;
                offset.yzw = float3(0 , 0 , 0);
                offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength
                     + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
                v.vertex = v.vertex + offset;
                TRANSFER_SHADOW_CASTER(o)
                return o;
            }
            fixed4 frag(v2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
         
    }
    FallBack "Transparent/VertexLit"
}

image-20250317210657661

自定阴影投射Pass常用:V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET、
SHADOW_CASTER_FRAGMENT
计算阴影投射时候需要的各种变量,而我们可以只关注自定义计算的部分,

image-20250317204944449