17.不透明物体光照衰减和阴影

  1. 17.阴影-不透明物体阴影-光照衰减和阴影
    1. 17.1 知识点
      1. 知识回顾
      2. 光照衰减和阴影
      3. 光照衰减和阴影的综合实现
        1. 主要步骤
        2. 创建一个新的Shader Lesson17_LightDecayAndShadow.shader,并将Lesson16_OpaqueObjectReceiveShadows中的代码赋值过来
        3. AdditionalPass 要包括上 AutoLight.cginc,没有的话要添加上
        4. AdditionalPass 的 v2f 结构体要添加上 SHADOW_COORDS 阴影采样坐标宏
        5. AdditionalPass 的顶点函数要添加上 TRANSFER_SHADOW 阴影坐标转换宏
        6. BasePass 片元函数中,注释掉上节课计算阴影衰减值和衰减值的代码,使用 UNITY_LIGHT_ATTENUATION 传入对应参数进行计算衰减值,计算完后的衰减值和最后的颜色相乘
        7. AdditionalPass片元函数中,注释掉我们使用预编译指令判断各种光源计算衰减值的代码,使用UNITY_LIGHT_ATTENUATION传入对应参数进行计算衰减值,计算完后的衰减值和最后的颜色相乘
        8. AdditionalPass中为了能够添加阴影效果,将编译指令进行修改,将原本的 #pragma multi_compile_fwdadd,修改为 #pragma multi_compile_fwdadd_fullshadows。这样Unity会生成多个包括支持和不支持阴影的Shader变体,从而为额外的逐像素光源计算阴影,并传递给Shader了。
        9. 创建一个红色点光源,可以看到点光源也会影响阴影。但是我们其实包AdditionalPass中的计算衰减值注释了,这是因为我们使用了UNITY_LIGHT_ATTENUATION计算衰减孩子。
        10. 把点光源改成软阴影模式,发现可能会出现Shader error in ‘Unlit/Lesson17_LightDecayAndShadow’: undeclared identifier ‘v’ at line 250 (on d3d11)的报错,这是因为我们改了顶点着色器参数appdata_base的命名,把顶点着色器appdata_base类型参数名修改回v即可看见点光源正常产生的阴影
    2. 17.2 知识点代码
      1. Lesson17_阴影_不透明物体阴影_光照衰减和阴影.cs
      2. Lesson17_LightDecayAndShadow.shader

17.阴影-不透明物体阴影-光照衰减和阴影


17.1 知识点

知识回顾

  • 多种光源的效果实现
    Additional Pass(附加渲染通道)中,根据光源类型计算出不同的光源衰减值,参与到最终的颜色计算中。

  • 计算阴影时需要使用的三剑客

    • SHADOW_COORDS(阴影坐标宏)
      v2f 结构体中声明,本质是声明了一个表示阴影映射纹理坐标的变量。

    • TRANSFER_SHADOW(转移阴影宏)
      在顶点着色器中调用,本质是将顶点进行坐标转换并存储到 _ShadowCoord 阴影纹理坐标变量中。

    • SHADOW_ATTENUATION(阴影衰减宏)
      本质是利用 _ShadowCoord 对阴影映射纹理进行采样,将采样得到的深度值进行比较,以计算出一个 fixed3 的阴影衰减值。
      最终之所以能够接受阴影,主要是因为利用了最终得到的阴影衰减值参与到了最终的颜色计算中。

光照衰减和阴影

通过之前的学习,我们发现光照衰减和接受阴影相关的计算是类似的。
关键点都是通过计算出一个衰减值,参与到颜色计算中,都是用 (漫反射 + 高光反射) 的结果乘以对应的衰减值。

由于它们对最终颜色影响的计算非常类似,都是通过乘法进行运算,因此 Unity 中专门提供了对应的宏来综合处理光照衰减和阴影衰减的计算。

AutoLight.cginc 内置文件中的 UNITY_LIGHT_ATTENUATION(Unity光照衰减宏),
该宏中会统一的对光照衰减进行计算,并且也会计算出阴影衰减值,最后将两者相乘得到综合衰减值。
我们只需要利用该宏来处理光照和阴影的衰减即可。

我们可以在 Unity 安装目录的 Editor->Data->CGIncludes 中找到该内置文件,查看该宏的逻辑。


光照衰减和阴影的综合实现

主要步骤

我们将利用 AutoLight.cginc 内置文件中的 UNITY_LIGHT_ATTENUATION(Unity光照衰减宏)来综合处理光照衰减和阴影相关的逻辑。

  1. 创建一个新的 Shader Lesson17_LightDecayAndShadow.shader 并将 Lesson16_OpaqueObjectReceiveShadows 中的代码赋值过来。

  2. 将三剑客中的前两剑客,在 Additional Pass 附加渲染通道中也添加上。
    注意:需要在附加渲染通道中包含内置文件 AutoLight.cginc(因为不仅三剑客来自于它,UNITY_LIGHT_ATTENUATION 也来自于它)。

  3. 修改两个 Pass 的片元着色器中衰减计算相关的代码,使用 UNITY_LIGHT_ATTENUATION 宏替代原有逻辑。
    该宏需要传入 3 个参数:

    • 第一个参数是用来存储最终衰减值的变量名(不用声明,内部会声明)。
    • 第二个参数是片元着色器中传入的 v2f 结构体对象。
    • 第三个参数是顶点相对于世界坐标系的位置。
      最终将得到的衰减结果和 (漫反射 + 高光反射) 的结果相乘即可。
  4. 为了让 Additional Pass 附加渲染通道能够添加阴影效果,需要将编译指令进行修改,将原本的 #pragma multi_compile_fwdadd 修改为 #pragma multi_compile_fwdadd_fullshadows,这样 Unity 会生成多个包括支持和不支持阴影的 Shader 变体,从而为额外的逐像素光源计算阴影,并传递给 Shader。

创建一个新的Shader Lesson17_LightDecayAndShadow.shader,并将Lesson16_OpaqueObjectReceiveShadows中的代码赋值过来

Shader "Unlit/Lesson17_LightDecayAndShadow"
{
    Properties
    {
        //材质的漫反射光照颜色
        _MainColor("MainColor", Color) = (1,1,1,1)
        //高光反射颜色
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)
        //光泽度
        _SpecularNum("SpecularNum", Range(0, 20)) = 0.5
    }
    SubShader
    {
        //Bass Pass 基础渲染通道
        Pass
        {
            //设置我们的光照模式 ForwardBase这种向前渲染模式 主要是用来处理 不透明物体的 光照渲染的
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
            #pragma multi_compile_fwdbase

            //引用对应的内置文件 
            //主要是为了之后 的 比如内置结构体使用,内置变量使用
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            //引用包含该内置文件 其中有计算阴影会用到的三个宏(阴影三剑客)
            #include "AutoLight.cginc"

            //材质的漫反射颜色
            fixed4 _MainColor;
            //对应属性当中的颜色和光泽度
            fixed4 _SpecularColor;
            float _SpecularNum;

            //顶点着色器传递给片元着色器的内容
            struct v2f
            {
                //裁剪空间的位置
                float4 pos:SV_POSITION;
                //基于世界坐标系下的顶点位置
                float3 wPos:TEXCOORD0;
                //基于世界坐标系下的法线
                float3 wNormal:NORMAL;

                //      该宏在v2f结构体(顶点着色器返回值)中使用
                //      本质上就是声明了一个用于对阴影纹理进行采样的坐标
                //      在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
                //      需要注意的是:
                //      在使用时 SHADOW_COORDS(2) 传入参数2
                //      表示需要时下一个可用的插值寄存器的索引值   
                //阴影坐标宏 主要用于存储阴影纹理坐标
                SHADOW_COORDS(2)
            };


            //得到兰伯特光照模型计算的颜色 (逐片元)
            fixed3 getLambertColor(in float3 wNormal)
            {
                //_WorldSpaceLightPos0.xyz 世界坐标系下光源方向 _WorldSpaceLightPos0是四维向量 只需要xyz即可
                //normalize 把向量归一化
                //lightDir 得到光源单位向量
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //_LightColor0光 照颜色
                //.rgb 代表只使用颜色进行计算 透明度不考虑 
                //color 计算出了兰伯特光照的漫反射颜色
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));

                return color;
            }

            //得到Blinn Phong式高光反射模型计算的颜色(逐片元)
            fixed3 getSpecularColor(in float3 wPos, in float3 wNormal)
            {
                //1.视角单位向量
                //用观察者的位置(摄像机的位置)减去世界空间下顶点坐标 得到的就是视角方向 并且进行单位化
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos);

                //2.光的反射单位向量
                //光的方向
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //半角方向向量
                float3 halfA = normalize(viewDir + lightDir);

                //color = 光源颜色 * 材质高光反射颜色 * pow( max(0, dot(视角单位向量, 光的反射单位向量)), 光泽度 )
                fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);

                return color;
            }

            v2f vert(appdata_base appdata_base)
            {
                v2f v2f;

                //转换模型空间下的顶点到裁剪空间中
                v2f.pos = UnityObjectToClipPos(appdata_base.vertex);

                //转换模型空间下的法线到世界空间下
                v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);

                //转换模型空间下的顶点到世界空间
                v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;

                //      该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
                //      该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
                //      最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
                //      需要注意的是:
                //      1.该宏会在内部使用顶点着色器中传入的结构体
                //        该结构体中顶点的命名必须是vertex
                //      2.该宏会在内部使用顶点着色器的返回结构体
                //        其中的顶点位置命名必须是pos
                //计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
                TRANSFER_SHADOW(v2f);

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //计算兰伯特光照颜色
                fixed3 lambertColor = getLambertColor(v2f.wNormal);

                //计算BlinnPhong式高光反射颜色
                fixed3 specularColor = getSpecularColor(v2f.wPos, v2f.wNormal);

                //得到阴影衰减值
                //      该宏在片元着色器中调用,传入对应的v2f结构体对象
                //      该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
                //      将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
                //      我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
                fixed3 shadow = SHADOW_ATTENUATION(v2f);
                
                //衰减值
                //因为我们这是基础Pass 一般是处理场景中最亮的平行光 所以可以认为不存在衰减值的 衰减值默认是1
                //但是之后附加Pass的衰减值就会变化了
                fixed atten = 1;

                //物体表面光照颜色 = 环境光颜色 + 兰伯特光照模型所得颜色 + 布林Phong式高光反射光照模型所得颜色
                //衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算   
                fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten*shadow;

                return fixed4(blinnPhongColor.rgb, 1);
            }
            ENDCG
        }


        //Additional Pass 附加渲染通道
        Pass
        {
            //设置我们的光照模式 ForwardAdd这种向前渲染模式 主要是用来处理 附加光照渲染的
            Tags
            {
                "LightMode"="ForwardAdd"
            }

            //线性减淡的效果 进行 光照颜色混合
            Blend One One


            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
            #pragma multi_compile_fwdadd

            //引用对应的内置文件 
            //主要是为了之后 的 比如内置结构体使用,内置变量使用
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"


            //材质的漫反射颜色
            fixed4 _MainColor;
            //对应属性当中的颜色和光泽度
            fixed4 _SpecularColor;
            float _SpecularNum;

            //顶点着色器传递给片元着色器的内容
            struct v2f
            {
                //裁剪空间的位置
                float4 pos:SV_POSITION;
                //基于世界坐标系下的顶点位置
                float3 wPos:TEXCOORD0;
                //基于世界坐标系下的法线
                float3 wNormal:NORMAL;
            };


            v2f vert(appdata_base appdata_base)
            {
                v2f v2f;

                //转换模型空间下的顶点到裁剪空间中
                v2f.pos = UnityObjectToClipPos(appdata_base.vertex);

                //转换模型空间下的法线到世界空间下
                v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);

                //转换模型空间下的顶点到世界空间
                v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;

                return v2f;
            }

            fixed4 frag (v2f v2f) : SV_Target
            {
                //兰伯特漫反射
                //公式:漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
                
                //世界标准法线
                fixed3 worldNormal = normalize(v2f.wNormal);
                
                #if defined(_DIRECTIONAL_LIGHT)
                    //平行光 光的方向 其实就是它的位置
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                    //点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - v2f.wPos);
                #endif
                
                // 漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
                fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

                
                //BlinnPhong高光反射
                //公式:高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
                
                //视角方向 摄像机位置 - 顶点位置
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - v2f.wPos.xyz);
                
                //半角方向向量 光方向 + 视角方向 平行四边形法则
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                
                // 高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

                
                //衰减值
                #ifdef USING_DIRECTIONAL_LIGHT
                    //平行光
                    fixed atten = 1;
                #else
                    #if defined(POINT)
                        //点光源
                        //将世界坐标系下顶点转到光源空间下
                        float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
                        //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
                        fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                    #elif defined(SPOT)
                        //聚光灯   
                        //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
                        float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
                        fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
                                      tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
                                      tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
                    #else
                        fixed atten = 1;
                    #endif
                #endif

                // 衰减值 不准确的判断
                // #if defined(_DIRECTIONAL_LIGHT)
                //     //平行光
                //     fixed atten = 1;
                // #elif defined(_POINT_LIGHT)
                //     //点光源
                //     //将世界坐标系下顶点转到光源空间下
                //     float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
                //     //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
                //     fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                // #elif defined(_SPOT_LIGHT)
                //     //聚光灯   
                //     //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
                //     float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
                //     fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
                //                   tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
                //                   tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
                // #else
                //     fixed atten = 1;
                // #endif

                //在附加渲染通道中不需要在加上环境光UNITY_LIGHTMODEL_AMBIENT.rgb颜色了
                //因为它只需要计算一次 在基础渲染通道中已经计算了
                return fixed4((diffuse + specular)*atten, 1);
            }
            ENDCG
        }
    }

    //找不到LightMode为ShaderCaster的Pass 使用Specular的shader
    Fallback "Specular"
}

AdditionalPass 要包括上 AutoLight.cginc,没有的话要添加上

// 引用包含该内置文件,其中有计算阴影会用到的三个宏(阴影三剑客)
#include "AutoLight.cginc"

AdditionalPassv2f 结构体要添加上 SHADOW_COORDS 阴影采样坐标宏

//顶点着色器传递给片元着色器的内容
struct v2f
{
    //裁剪空间的位置
    float4 pos:SV_POSITION;
    //基于世界坐标系下的顶点位置
    float3 wPos:TEXCOORD0;
    //基于世界坐标系下的法线
    float3 wNormal:NORMAL;

    //      该宏在v2f结构体(顶点着色器返回值)中使用
    //      本质上就是声明了一个用于对阴影纹理进行采样的坐标
    //      在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
    //      需要注意的是:
    //      在使用时 SHADOW_COORDS(2) 传入参数2
    //      表示需要时下一个可用的插值寄存器的索引值   
    //阴影坐标宏 主要用于存储阴影纹理坐标
    SHADOW_COORDS(2)
};

AdditionalPass 的顶点函数要添加上 TRANSFER_SHADOW 阴影坐标转换宏

v2f vert(appdata_base appdata_base)
{
    v2f v2f;

    //转换模型空间下的顶点到裁剪空间中
    v2f.pos = UnityObjectToClipPos(appdata_base.vertex);

    //转换模型空间下的法线到世界空间下
    v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);

    //转换模型空间下的顶点到世界空间
    v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;

    //      该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
    //      该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
    //      最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
    //      需要注意的是:
    //      1.该宏会在内部使用顶点着色器中传入的结构体
    //        该结构体中顶点的命名必须是vertex
    //      2.该宏会在内部使用顶点着色器的返回结构体
    //        其中的顶点位置命名必须是pos
    //计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
    TRANSFER_SHADOW(v2f);
    
    return v2f;
}

BasePass 片元函数中,注释掉上节课计算阴影衰减值和衰减值的代码,使用 UNITY_LIGHT_ATTENUATION 传入对应参数进行计算衰减值,计算完后的衰减值和最后的颜色相乘

fixed4 frag(v2f v2f) : SV_Target
{
    //计算兰伯特光照颜色
    fixed3 lambertColor = getLambertColor(v2f.wNormal);

    //计算BlinnPhong式高光反射颜色
    fixed3 specularColor = getSpecularColor(v2f.wPos, v2f.wNormal);

    //得到阴影衰减值
    //      该宏在片元着色器中调用,传入对应的v2f结构体对象
    //      该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
    //      将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
    //      我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
    //fixed3 shadow = SHADOW_ATTENUATION(v2f);
    
    //衰减值
    //因为我们这是基础Pass 一般是处理场景中最亮的平行光 所以可以认为不存在衰减值的 衰减值默认是1
    //但是之后附加Pass的衰减值就会变化了
    //fixed atten = 1;

    //利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算
    //要传三个参数
    //第一个参数 衰减值变量名 会在内部声明并赋值
    //第二个参数 片元函数结构体对象
    //第三个参数 世界坐标的位置
    UNITY_LIGHT_ATTENUATION(atten, v2f, v2f.wPos)
    
    //调用玩后atten会是计算好后的衰减值 乘上漫反射颜色 + 高光反射颜色即可

    //物体表面光照颜色 = 环境光颜色 + 兰伯特光照模型所得颜色 + 布林Phong式高光反射光照模型所得颜色
    //衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算   
    // fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten*shadow;
    fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

    return fixed4(blinnPhongColor.rgb, 1);
}

AdditionalPass片元函数中,注释掉我们使用预编译指令判断各种光源计算衰减值的代码,使用UNITY_LIGHT_ATTENUATION传入对应参数进行计算衰减值,计算完后的衰减值和最后的颜色相乘

fixed4 frag (v2f v2f) : SV_Target
{
    //兰伯特漫反射
    //公式:漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
    
    //世界标准法线
    fixed3 worldNormal = normalize(v2f.wNormal);
    
    #if defined(_DIRECTIONAL_LIGHT)
        //平行光 光的方向 其实就是它的位置
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    #else
        //点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - v2f.wPos);
    #endif
    
    // 漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
    fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

    
    //BlinnPhong高光反射
    //公式:高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
    
    //视角方向 摄像机位置 - 顶点位置
    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - v2f.wPos.xyz);
    
    //半角方向向量 光方向 + 视角方向 平行四边形法则
    fixed3 halfDir = normalize(worldLightDir + viewDir);
    
    // 高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
    fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

    
    //衰减值
    // #ifdef USING_DIRECTIONAL_LIGHT
    //     //平行光
    //     fixed atten = 1;
    // #else
    //     #if defined(POINT)
    //         //点光源
    //         //将世界坐标系下顶点转到光源空间下
    //         float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
    //         //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
    //         fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
    //     #elif defined(SPOT)
    //         //聚光灯   
    //         //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
    //         float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
    //         fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
    //                       tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
    //                       tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
    //     #else
    //         fixed atten = 1;
    //     #endif
    // #endif

    // 衰减值 不准确的判断
    // #if defined(_DIRECTIONAL_LIGHT)
    //     //平行光
    //     fixed atten = 1;
    // #elif defined(_POINT_LIGHT)
    //     //点光源
    //     //将世界坐标系下顶点转到光源空间下
    //     float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
    //     //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
    //     fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
    // #elif defined(_SPOT_LIGHT)
    //     //聚光灯   
    //     //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
    //     float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
    //     fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
    //                   tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
    //                   tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
    // #else
    //     fixed atten = 1;
    // #endif

    //利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算
    //要传三个参数
    //第一个参数 衰减值变量名 会在内部声明并赋值
    //第二个参数 片元函数结构体对象
    //第三个参数 世界坐标的位置
    UNITY_LIGHT_ATTENUATION(atten, v2f, v2f.wPos)

    //在附加渲染通道中不需要在加上环境光UNITY_LIGHTMODEL_AMBIENT.rgb颜色了
    //因为它只需要计算一次 在基础渲染通道中已经计算了
    return fixed4((diffuse + specular)*atten, 1);
}

AdditionalPass中为了能够添加阴影效果,将编译指令进行修改,将原本的 #pragma multi_compile_fwdadd,修改为 #pragma multi_compile_fwdadd_fullshadows。这样Unity会生成多个包括支持和不支持阴影的Shader变体,从而为额外的逐像素光源计算阴影,并传递给Shader了。

//用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
// #pragma multi_compile_fwdadd

//这个编译指令让Unity生成多个包括支持和不支持阴影的Shader变体
//从而为额外的逐像素光源计算阴影,并传递给Shader了
#pragma multi_compile_fwdadd_fullshadows

创建一个红色点光源,可以看到点光源也会影响阴影。但是我们其实包AdditionalPass中的计算衰减值注释了,这是因为我们使用了UNITY_LIGHT_ATTENUATION计算衰减孩子。

把点光源改成软阴影模式,发现可能会出现Shader error in ‘Unlit/Lesson17_LightDecayAndShadow’: undeclared identifier ‘v’ at line 250 (on d3d11)的报错,这是因为我们改了顶点着色器参数appdata_base的命名,把顶点着色器appdata_base类型参数名修改回v即可看见点光源正常产生的阴影



17.2 知识点代码

Lesson17_阴影_不透明物体阴影_光照衰减和阴影.cs

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

public class Lesson17_阴影_不透明物体阴影_光照衰减和阴影 : MonoBehaviour
{
    void Start()
    {
        #region 知识回顾

        //1.多种光源的效果实现
        //  在Additional Pass(附加渲染通道)中根据光源类型
        //  计算出不同的光源衰减值,参与到最终的颜色计算中

        //2.计算阴影时需要使用的三剑客
        //  2-1.SHADOW_COORDS      (阴影坐标宏)
        //      在v2f结构体中声明,本质是声明了一个表示阴影映射纹理坐标的变量

        //  2-2.TRANSFER_SHADOW    (转移阴影宏)
        //      在顶点着色器中调用,本质是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中

        //  2-3.SHADOW_ATTENUATION (阴影衰减宏)
        //      本质是利用_ShadowCoord对阴影映射纹理进行采用
        //      将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值

        //  最终之所以能够接受阴影
        //  主要就是因为利用了最终得到的阴影衰减值参与到了最终的颜色计算中

        #endregion

        #region 知识点一 光照衰减和阴影

        //通过之前的学习
        //我们发现光照衰减和接受阴影相关的计算是类似的
        //关键点都是通过计算出一个衰减值,参与到颜色计算中
        //都是用 (漫反射+高光反射) 的结果乘以对应的衰减值

        //由于它们对最终颜色影响的计算非常类似,都是通过乘法进行运算
        //因此Unity中专门提供了对应的宏
        //来综合处理光照衰减和阴影衰减的计算

        //AutoLight.cginc 内置文件中的
        //UNITY_LIGHT_ATTENUATION(Unity光照衰减宏)
        //该宏中会统一的对 光照衰减进行计算 并且也会计算出 阴影衰减值
        //最后将两者相乘得到综合衰减值

        //我们只需要利用该宏来处理 光照和阴影的衰减即可

        //我们可以在 Unity安装目录的Editor->Data->CGIncludes中找到该内置文件
        //查看该宏的逻辑

        #endregion

        #region 知识点二 光照衰减和阴影的综合实现

        //我们将利用
        //AutoLight.cginc 内置文件中的
        //UNITY_LIGHT_ATTENUATION(Unity光照衰减宏)
        //来综合处理光照衰减和阴影相关的逻辑

        //1.创建一个新的Shader Lesson17_LightDecayAndShadow.shader
        //并将Lesson16_OpaqueObjectReceiveShadows中的代码赋值过来

        //2.将三剑客中的前两剑客,在Additional Pass 附加渲染通道中也添加上
        //  注意:需要在附加渲染通道中包含内置文件 AutoLight.cginc
        //      (因为不仅三剑客来自于它,UNITY_LIGHT_ATTENUATION-Unity光照衰减宏也来自于它)

        //3.修改两个Pass的片元着色器中衰减计算相关的代码
        //  使用 UNITY_LIGHT_ATTENUATION 宏替代原有逻辑
        //  该宏需要传入3个参数
        //  第一个参数是用来存储最终衰减值的变量名(不用声明,内部会声明)
        //  第二个参数是片元着色器中传入的v2f结构体对象
        //  第三个参数是顶点相对于世界坐标系的位置
        //  最终将得到的衰减结果和  (漫反射+高光反射) 的结果相乘即可

        //4.为了让Additional Pass 附加渲染通道能够添加阴影效果
        //需要将编译指令进行修改
        //将原本的 #pragma multi_compile_fwdadd
        //修改为 #pragma multi_compile_fwdadd_fullshadows
        //这样Unity会生成多个包括支持和不支持阴影的Shader变体
        //从而为额外的逐像素光源计算阴影,并传递给Shader了

        #endregion
    }
}

Lesson17_LightDecayAndShadow.shader

Shader "Unlit/Lesson17_LightDecayAndShadow"
{
    Properties
    {
        //材质的漫反射光照颜色
        _MainColor("MainColor", Color) = (1,1,1,1)
        //高光反射颜色
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)
        //光泽度
        _SpecularNum("SpecularNum", Range(0, 20)) = 0.5
    }
    SubShader
    {
        //Bass Pass 基础渲染通道
        Pass
        {
            //设置我们的光照模式 ForwardBase这种向前渲染模式 主要是用来处理 不透明物体的 光照渲染的
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
            #pragma multi_compile_fwdbase

            //引用对应的内置文件 
            //主要是为了之后 的 比如内置结构体使用,内置变量使用
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            //引用包含该内置文件 其中有计算阴影会用到的三个宏(阴影三剑客)
            #include "AutoLight.cginc"

            //材质的漫反射颜色
            fixed4 _MainColor;
            //对应属性当中的颜色和光泽度
            fixed4 _SpecularColor;
            float _SpecularNum;

            //顶点着色器传递给片元着色器的内容
            struct v2f
            {
                //裁剪空间的位置
                float4 pos:SV_POSITION;
                //基于世界坐标系下的顶点位置
                float3 wPos:TEXCOORD0;
                //基于世界坐标系下的法线
                float3 wNormal:NORMAL;

                //      该宏在v2f结构体(顶点着色器返回值)中使用
                //      本质上就是声明了一个用于对阴影纹理进行采样的坐标
                //      在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
                //      需要注意的是:
                //      在使用时 SHADOW_COORDS(2) 传入参数2
                //      表示需要时下一个可用的插值寄存器的索引值   
                //阴影坐标宏 主要用于存储阴影纹理坐标
                SHADOW_COORDS(2)
            };


            //得到兰伯特光照模型计算的颜色 (逐片元)
            fixed3 getLambertColor(in float3 wNormal)
            {
                //_WorldSpaceLightPos0.xyz 世界坐标系下光源方向 _WorldSpaceLightPos0是四维向量 只需要xyz即可
                //normalize 把向量归一化
                //lightDir 得到光源单位向量
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //_LightColor0光 照颜色
                //.rgb 代表只使用颜色进行计算 透明度不考虑 
                //color 计算出了兰伯特光照的漫反射颜色
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));

                return color;
            }

            //得到Blinn Phong式高光反射模型计算的颜色(逐片元)
            fixed3 getSpecularColor(in float3 wPos, in float3 wNormal)
            {
                //1.视角单位向量
                //用观察者的位置(摄像机的位置)减去世界空间下顶点坐标 得到的就是视角方向 并且进行单位化
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos);

                //2.光的反射单位向量
                //光的方向
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //半角方向向量
                float3 halfA = normalize(viewDir + lightDir);

                //color = 光源颜色 * 材质高光反射颜色 * pow( max(0, dot(视角单位向量, 光的反射单位向量)), 光泽度 )
                fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);

                return color;
            }

            v2f vert(appdata_base v)
            {
                v2f v2f;

                //转换模型空间下的顶点到裁剪空间中
                v2f.pos = UnityObjectToClipPos(v.vertex);

                //转换模型空间下的法线到世界空间下
                v2f.wNormal = UnityObjectToWorldNormal(v.normal);

                //转换模型空间下的顶点到世界空间
                v2f.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                //      该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
                //      该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
                //      最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
                //      需要注意的是:
                //      1.该宏会在内部使用顶点着色器中传入的结构体
                //        该结构体中顶点的命名必须是vertex
                //      2.该宏会在内部使用顶点着色器的返回结构体
                //        其中的顶点位置命名必须是pos
                //计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
                TRANSFER_SHADOW(v2f);

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //计算兰伯特光照颜色
                fixed3 lambertColor = getLambertColor(v2f.wNormal);

                //计算BlinnPhong式高光反射颜色
                fixed3 specularColor = getSpecularColor(v2f.wPos, v2f.wNormal);

                //得到阴影衰减值
                //      该宏在片元着色器中调用,传入对应的v2f结构体对象
                //      该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
                //      将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
                //      我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
                //fixed3 shadow = SHADOW_ATTENUATION(v2f);
                
                //衰减值
                //因为我们这是基础Pass 一般是处理场景中最亮的平行光 所以可以认为不存在衰减值的 衰减值默认是1
                //但是之后附加Pass的衰减值就会变化了
                //fixed atten = 1;

                //利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算
                //要传三个参数
                //第一个参数 衰减值变量名 会在内部声明并赋值
                //第二个参数 片元函数结构体对象
                //第三个参数 世界坐标的位置
                UNITY_LIGHT_ATTENUATION(atten, v2f, v2f.wPos)
                
                //调用玩后atten会是计算好后的衰减值 乘上漫反射颜色 + 高光反射颜色即可

                //物体表面光照颜色 = 环境光颜色 + 兰伯特光照模型所得颜色 + 布林Phong式高光反射光照模型所得颜色
                //衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算   
                // fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten*shadow;
                fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

                return fixed4(blinnPhongColor.rgb, 1);
            }
            ENDCG
        }


        //Additional Pass 附加渲染通道
        Pass
        {
            //设置我们的光照模式 ForwardAdd这种向前渲染模式 主要是用来处理 附加光照渲染的
            Tags
            {
                "LightMode"="ForwardAdd"
            }

            //线性减淡的效果 进行 光照颜色混合
            Blend One One


            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
            // #pragma multi_compile_fwdadd
            
            //这个编译指令让Unity生成多个包括支持和不支持阴影的Shader变体
            //从而为额外的逐像素光源计算阴影,并传递给Shader了
            #pragma multi_compile_fwdadd_fullshadows

            //引用对应的内置文件 
            //主要是为了之后 的 比如内置结构体使用,内置变量使用
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            //引用包含该内置文件 其中有计算阴影会用到的三个宏(阴影三剑客)
            #include "AutoLight.cginc"


            //材质的漫反射颜色
            fixed4 _MainColor;
            //对应属性当中的颜色和光泽度
            fixed4 _SpecularColor;
            float _SpecularNum;

            //顶点着色器传递给片元着色器的内容
            struct v2f
            {
                //裁剪空间的位置
                float4 pos:SV_POSITION;
                //基于世界坐标系下的顶点位置
                float3 wPos:TEXCOORD0;
                //基于世界坐标系下的法线
                float3 wNormal:NORMAL;

                //      该宏在v2f结构体(顶点着色器返回值)中使用
                //      本质上就是声明了一个用于对阴影纹理进行采样的坐标
                //      在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
                //      需要注意的是:
                //      在使用时 SHADOW_COORDS(2) 传入参数2
                //      表示需要时下一个可用的插值寄存器的索引值   
                //阴影坐标宏 主要用于存储阴影纹理坐标
                SHADOW_COORDS(2)
            };


            v2f vert(appdata_base v)
            {
                v2f v2f;

                //转换模型空间下的顶点到裁剪空间中
                v2f.pos = UnityObjectToClipPos(v.vertex);

                //转换模型空间下的法线到世界空间下
                v2f.wNormal = UnityObjectToWorldNormal(v.normal);

                //转换模型空间下的顶点到世界空间
                v2f.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                //      该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
                //      该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
                //      最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
                //      需要注意的是:
                //      1.该宏会在内部使用顶点着色器中传入的结构体
                //        该结构体中顶点的命名必须是vertex
                //      2.该宏会在内部使用顶点着色器的返回结构体
                //        其中的顶点位置命名必须是pos
                //计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
                TRANSFER_SHADOW(v2f);
                
                return v2f;
            }

            fixed4 frag (v2f v2f) : SV_Target
            {
                //兰伯特漫反射
                //公式:漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
                
                //世界标准法线
                fixed3 worldNormal = normalize(v2f.wNormal);
                
                #if defined(_DIRECTIONAL_LIGHT)
                    //平行光 光的方向 其实就是它的位置
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                    //点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - v2f.wPos);
                #endif
                
                // 漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
                fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

                
                //BlinnPhong高光反射
                //公式:高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
                
                //视角方向 摄像机位置 - 顶点位置
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - v2f.wPos.xyz);
                
                //半角方向向量 光方向 + 视角方向 平行四边形法则
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                
                // 高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

                
                //衰减值
                // #ifdef USING_DIRECTIONAL_LIGHT
                //     //平行光
                //     fixed atten = 1;
                // #else
                //     #if defined(POINT)
                //         //点光源
                //         //将世界坐标系下顶点转到光源空间下
                //         float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
                //         //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
                //         fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                //     #elif defined(SPOT)
                //         //聚光灯   
                //         //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
                //         float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
                //         fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
                //                       tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
                //                       tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
                //     #else
                //         fixed atten = 1;
                //     #endif
                // #endif

                // 衰减值 不准确的判断
                // #if defined(_DIRECTIONAL_LIGHT)
                //     //平行光
                //     fixed atten = 1;
                // #elif defined(_POINT_LIGHT)
                //     //点光源
                //     //将世界坐标系下顶点转到光源空间下
                //     float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
                //     //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
                //     fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                // #elif defined(_SPOT_LIGHT)
                //     //聚光灯   
                //     //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
                //     float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
                //     fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
                //                   tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
                //                   tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
                // #else
                //     fixed atten = 1;
                // #endif

                //利用灯光衰减和阴影衰减计算宏统一进行衰减值的计算
                //要传三个参数
                //第一个参数 衰减值变量名 会在内部声明并赋值
                //第二个参数 片元函数结构体对象
                //第三个参数 世界坐标的位置
                UNITY_LIGHT_ATTENUATION(atten, v2f, v2f.wPos)

                //在附加渲染通道中不需要在加上环境光UNITY_LIGHTMODEL_AMBIENT.rgb颜色了
                //因为它只需要计算一次 在基础渲染通道中已经计算了
                return fixed4((diffuse + specular)*atten, 1);
            }
            ENDCG
        }
    }

    //找不到LightMode为ShaderCaster的Pass 使用Specular的shader
    Fallback "Specular"
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏