21.标准高光反射

  1. 21.标准光照着色器-标准高光反射
    1. 21.1 知识点
      1. 什么是标准高光反射Shader
      2. 制作常用高光反射Shader
        1. 主要步骤
        2. 新建一个Shader,取名 Lesson21_BumpedSpecular
        3. 复制 Lesson30_Bump_Texture_World_High_Performance.shader 的内容复制并粘贴到新建的Shader文件中
        4. 加入渲染标签Tags { “RenderType”=”Opaque” “Queue”=”Geometry”},渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)
        5. 添加 multi_compile_fwdbase 指令和 AutoLight 内置文件
        6. v2f 结构体添加阴影坐标宏 SHADOW_COORDS
        7. 顶点函数添加坐标转换宏 TRANSFER_SHADOW
        8. 片元函数加上计算光照衰减和阴影衰减的宏UNITY_LIGHT_ATTENUATION,最后计算时乘上衰减值
        9. 加入附加渲染通道,复制基础渲染通道。把LightMode改成ForwardAdd。添加Blend One One让线性减淡的效果 进行 光照颜色混合。把预编译指令改成multi_compile_fwdadd_fullshadows让Unity生成多个包括支持和不支持阴影的Shader变体,从而为额外的逐像素光源计算阴影,并传递给Shader。之前需要我们手动考虑不同光源的情况,现在学了UNITY_LIGHT_ATTENUATION宏会在内部帮我们计算
        10. 注意附加渲染通道的参数名要改成v,否则可能出现Shader error in ‘Unlit/Lesson21_BumpedSpecular’: undeclared identifier ‘v’ at line 288 (on d3d11)这样的报错
        11. 加入FallBack “Specular”,这Unity自带的高光反射Shader,里面实现了对应投射阴影的Pass
        12. 创建材质,可以看到可以接收和投放多光源阴影。和标准漫反射相比。可以看到有高光效果,成功实现了常见的带法线、高光反射、有阴影且能收到多光源影响的常用shader。
    2. 21.2 知识点代码
      1. Lesson21_标准光照着色器_标准高光反射.cs
      2. Lesson21_BumpedSpecular.shader

21.标准光照着色器-标准高光反射


21.1 知识点

什么是标准高光反射Shader

所谓的标准高光反射Shader,其实就是一个常用的高光反射Shader而已。该Shader使用的是带有法线(世界空间中计算-全局效果的表现更准确)的基于BlinnPhong光照模型,支持多光源和阴影效果。

制作常用高光反射Shader

主要步骤

  1. 新建一个Shader,命名为 Lesson21_BumpedSpecular(凹凸镜面反射)。
  2. 复制 Shader开发入门 中的 Lesson30_Bump_Texture_World_High_Performance.shader,并粘贴到新建的文件中。
  3. 为Shader添加渲染标签,设置渲染类型为不透明,渲染队列为几何队列。
  4. 添加阴影和衰减相关的代码。
  5. 添加额外的渲染通道。
  6. 添加 FallBack "Specular" 以确保Shader兼容其他情况。

新建一个Shader,取名 Lesson21_BumpedSpecular

Shader "Unlit/Lesson21_BumpedSpecular"
{
    
}

复制 Lesson30_Bump_Texture_World_High_Performance.shader 的内容复制并粘贴到新建的Shader文件中

Shader "Lesson21_BumpedSpecular"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)//  漫反射颜色 
        _MainTex("MainTex", 2D) = ""{}//  单张纹理
        _BumpMap("BumpMap", 2D) = ""{}//  法线纹理
        _BumpScale("BumpScale", Range(0,1)) = 1//  凹凸程度
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)//  高光反射颜色
        _SpecularNum("SpecularNum", Range(0,20)) = 18//  光泽度
    }
    SubShader
    {
        Pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            float4 _MainColor; //漫反射颜色
            sampler2D _MainTex; //颜色纹理
            float4 _MainTex_ST; //颜色纹理的缩放和平移
            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移
            float _BumpScale; //凹凸程度
            float4 _SpecularColor; //高光颜色
            fixed _SpecularNum; //光泽度

            struct v2f
            {
                //裁剪空间下坐标
                float4 pos:SV_POSITION;

                //纹理uv

                //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
                //float2 uvTex:TEXCOORD0;//颜色纹理
                //float2 uvBump:TEXCOORD1;//法线纹理

                //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
                float4 uv:TEXCOORD0; //纹理变量


                //顶点相对于世界坐标的位置 主要用于 之后的 视角方向的计算
                //float3 worldPos:TEXCOORD1;

                //切线 到 世界空间的 变换矩阵
                //float3x3 rotation:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD1;
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;
            };

            v2f vert(appdata_full appdata_full)
            {
                v2f v2f;

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

                //计算纹理的缩放偏移
                v2f.uv.xy = appdata_full.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                v2f.uv.zw = appdata_full.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
                //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
                float3 worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);

                //把模型空间下的法线、切线转换到世界空间下
                float3 worldNormal = UnityObjectToWorldNormal(appdata_full.normal);
                float3 worldTangent = UnityObjectToWorldDir(appdata_full.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * appdata_full.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //世界空间下光的方向
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                //纹理采样函数tex2D
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

                //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);

                //乘以凹凸程度的系数
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


                //把计算完毕后的切线空间下的法线转换到世界空间下
                // float3 worldNormal = mul(v2f.rotation, tangentNormal);

                //以下两种写法等价 但是可以一步到位
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, tangentNormal);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
            dot(v2f.TtoW2.xyz, tangentNormal));


                //接下来就来处理 带颜色纹理的 布林方光照模型计算

                //颜色纹理和漫反射颜色的 叠加
                fixed3 albedo = tex2D(_MainTex, v2f.uv.xy) * _MainColor.rgb;

                //兰伯特漫反射颜色 = 光的颜色 * 漫反射材质的颜色 * max(0, dot(世界坐标系下的法线, 光的方向))
                fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, normalize(lightDir)));

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

                //高光反射的颜色 = 光的颜色 * 高光反射材质的颜色 * pow(max(0, dot(世界坐标系下的法线, 半角向量)), 光泽度)
                fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
                    max(0, dot(worldNormal, halfA)), _SpecularNum);

                //布林方光照颜色 = 环境光颜色 + 兰伯特漫反射颜色 + 高光反射的颜色
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

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

加入渲染标签Tags { “RenderType”=”Opaque” “Queue”=”Geometry”},渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)

Shader "Unlit/Lesson21_BumpedSpecular"
{
    Properties
    {
        //...
    }
    SubShader
    {
        //  加入渲染标签Tags { "RenderType"="Opaque" "Queue"="Geometry"}
        //  渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)
        Tags
        {
            "RenderType"="Opaque" "Queue"="Geometry"
        }
        Pass
        {
            //...
        }
    }
}

添加 multi_compile_fwdbase 指令和 AutoLight 内置文件

#pragma vertex vert
#pragma fragment frag

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

#include "UnityCG.cginc"
#include "Lighting.cginc"

//使用阴影宏内置文件
#include "AutoLight.cginc"

v2f 结构体添加阴影坐标宏 SHADOW_COORDS

struct v2f
{
    //裁剪空间下坐标
    float4 pos:SV_POSITION;

    //纹理uv

    //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
    //float2 uvTex:TEXCOORD0;//颜色纹理
    //float2 uvBump:TEXCOORD1;//法线纹理

    //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
    float4 uv:TEXCOORD0; //纹理变量


    //顶点相对于世界坐标的位置 主要用于 之后的 视角方向的计算
    //float3 worldPos:TEXCOORD1;

    //切线 到 世界空间的 变换矩阵
    //float3x3 rotation:TEXCOORD2;

    //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
    float4 TtoW0:TEXCOORD1;
    float4 TtoW1:TEXCOORD2;
    float4 TtoW2:TEXCOORD3;


    //阴影坐标宏
    //n为下一个可用的插值寄存器的索引值(结构体前面有几个TEXCOORD就填几)
    //比如这里前面有四个
    SHADOW_COORDS(4)
};

顶点函数添加坐标转换宏 TRANSFER_SHADOW

v2f vert(appdata_full appdata_full)
{
    v2f v2f;

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

    //计算纹理的缩放偏移
    v2f.uv.xy = appdata_full.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    v2f.uv.zw = appdata_full.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

    //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
    //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
    float3 worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);

    //把模型空间下的法线、切线转换到世界空间下
    float3 worldNormal = UnityObjectToWorldNormal(appdata_full.normal);
    float3 worldTangent = UnityObjectToWorldDir(appdata_full.tangent);

    //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
    //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
    float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * appdata_full.tangent.w;

    //这个就是我们 切线空间到世界空间的 转换矩阵
    //       |    |     |
    //      切线 副切线 法线
    //       |    |     |
    //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
    //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
    //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
    v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

    //坐标转换宏
    TRANSFER_SHADOW(v2f)

    return v2f;
}

片元函数加上计算光照衰减和阴影衰减的宏UNITY_LIGHT_ATTENUATION,最后计算时乘上衰减值

fixed4 frag(v2f v2f) : SV_Target
{
    //世界空间下光的方向
    fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

    //世界空间下视角方向
    // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
    float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

    //纹理采样函数tex2D
    //通过纹理采样函数 取出法线纹理贴图当中的数据
    float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

    //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
    //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
    float3 tangentNormal = UnpackNormal(packedNormal);

    //乘以凹凸程度的系数
    tangentNormal.xy *= _BumpScale;
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


    //把计算完毕后的切线空间下的法线转换到世界空间下
    // float3 worldNormal = mul(v2f.rotation, tangentNormal);

    //以下两种写法等价 但是可以一步到位
    //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
    //float3 worldNormal = mul(rotation, tangentNormal);
    //本质 就是在进行矩阵运算
    float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                                dot(v2f.TtoW2.xyz, tangentNormal));


    //接下来就来处理 带颜色纹理的 布林方光照模型计算

    //颜色纹理和漫反射颜色的 叠加
    fixed3 albedo = tex2D(_MainTex, v2f.uv.xy) * _MainColor.rgb;

    //兰伯特漫反射颜色 = 光的颜色 * 漫反射材质的颜色 * max(0, dot(世界坐标系下的法线, 光的方向))
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, normalize(lightDir)));

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

    //高光反射的颜色 = 光的颜色 * 高光反射材质的颜色 * pow(max(0, dot(世界坐标系下的法线, 半角向量)), 光泽度)
    fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
        max(0, dot(worldNormal, halfA)), _SpecularNum);

    //计算光照衰减和阴影衰减的宏
    UNITY_LIGHT_ATTENUATION(atten, v2f, worldPos);

    // //布林方光照颜色 = 环境光颜色 + 兰伯特漫反射颜色 + 高光反射的颜色
    // fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
    
    //布林方光照颜色 = 环境光颜色 + (兰伯特漫反射颜色 + 高光反射的颜色)*衰减值
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor)*atten;

    return fixed4(color.rgb, 1);
}

加入附加渲染通道,复制基础渲染通道。把LightMode改成ForwardAdd。添加Blend One One让线性减淡的效果 进行 光照颜色混合。把预编译指令改成multi_compile_fwdadd_fullshadows让Unity生成多个包括支持和不支持阴影的Shader变体,从而为额外的逐像素光源计算阴影,并传递给Shader。之前需要我们手动考虑不同光源的情况,现在学了UNITY_LIGHT_ATTENUATION宏会在内部帮我们计算

Shader "Unlit/Lesson21_BumpedSpecular"
{
    Properties
    {
        //...
    }
    SubShader
    {
        //  加入渲染标签Tags { "RenderType"="Opaque" "Queue"="Geometry"}
        //  渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)
        Tags
        {
            "RenderType"="Opaque" "Queue"="Geometry"
        }

        //Base Pass
        Pass
        {
            //...
        }

        //Additional Pass
        Pass
        {
            Tags
            {
                "LightMode"="ForwardAdd"
            }
            
            //线性减淡的效果 进行 光照颜色混合
            Blend One One
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            //使用阴影宏内置文件
            #include "AutoLight.cginc"

            float4 _MainColor; //漫反射颜色
            sampler2D _MainTex; //颜色纹理
            float4 _MainTex_ST; //颜色纹理的缩放和平移
            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移
            float _BumpScale; //凹凸程度
            float4 _SpecularColor; //高光颜色
            fixed _SpecularNum; //光泽度

            struct v2f
            {
                //裁剪空间下坐标
                float4 pos:SV_POSITION;

                //纹理uv

                //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
                //float2 uvTex:TEXCOORD0;//颜色纹理
                //float2 uvBump:TEXCOORD1;//法线纹理

                //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
                float4 uv:TEXCOORD0; //纹理变量


                //顶点相对于世界坐标的位置 主要用于 之后的 视角方向的计算
                //float3 worldPos:TEXCOORD1;

                //切线 到 世界空间的 变换矩阵
                //float3x3 rotation:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD1;
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;


                //阴影坐标宏
                //n为下一个可用的插值寄存器的索引值(结构体前面有几个TEXCOORD就填几)
                //比如这里前面有四个
                SHADOW_COORDS(4)
            };

            v2f vert(appdata_full appdata_full)
            {
                v2f v2f;

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

                //计算纹理的缩放偏移
                v2f.uv.xy = appdata_full.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                v2f.uv.zw = appdata_full.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
                //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
                float3 worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);

                //把模型空间下的法线、切线转换到世界空间下
                float3 worldNormal = UnityObjectToWorldNormal(appdata_full.normal);
                float3 worldTangent = UnityObjectToWorldDir(appdata_full.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * appdata_full.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                //坐标转换宏
                TRANSFER_SHADOW(v2f)

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //世界空间下光的方向
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                //纹理采样函数tex2D
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

                //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);

                //乘以凹凸程度的系数
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


                //把计算完毕后的切线空间下的法线转换到世界空间下
                // float3 worldNormal = mul(v2f.rotation, tangentNormal);

                //以下两种写法等价 但是可以一步到位
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, tangentNormal);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                                            dot(v2f.TtoW2.xyz, tangentNormal));


                //接下来就来处理 带颜色纹理的 布林方光照模型计算

                //颜色纹理和漫反射颜色的 叠加
                fixed3 albedo = tex2D(_MainTex, v2f.uv.xy) * _MainColor.rgb;

                //兰伯特漫反射颜色 = 光的颜色 * 漫反射材质的颜色 * max(0, dot(世界坐标系下的法线, 光的方向))
                fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, normalize(lightDir)));

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

                //高光反射的颜色 = 光的颜色 * 高光反射材质的颜色 * pow(max(0, dot(世界坐标系下的法线, 半角向量)), 光泽度)
                fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
                    max(0, dot(worldNormal, halfA)), _SpecularNum);

                //计算光照衰减和阴影衰减的宏
                UNITY_LIGHT_ATTENUATION(atten, v2f, worldPos);

                // //布林方光照颜色 = 环境光颜色 + 兰伯特漫反射颜色 + 高光反射的颜色
                // fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

                //布林方光照颜色 = 环境光颜色 + (兰伯特漫反射颜色 + 高光反射的颜色)*衰减值
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

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

注意附加渲染通道的参数名要改成v,否则可能出现Shader error in ‘Unlit/Lesson21_BumpedSpecular’: undeclared identifier ‘v’ at line 288 (on d3d11)这样的报错

v2f vert(appdata_full v)
{
    v2f v2f;

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

    //计算纹理的缩放偏移
    v2f.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    v2f.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

    //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
    //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
    float3 worldPos = mul(unity_ObjectToWorld, v.vertex);

    //把模型空间下的法线、切线转换到世界空间下
    float3 worldNormal = UnityObjectToWorldNormal(v.normal);
    float3 worldTangent = UnityObjectToWorldDir(v.tangent);

    //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
    //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
    float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;

    //这个就是我们 切线空间到世界空间的 转换矩阵
    //       |    |     |
    //      切线 副切线 法线
    //       |    |     |
    //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
    //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
    //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
    v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

    //坐标转换宏
    TRANSFER_SHADOW(v2f)

    return v2f;
}

加入FallBack “Specular”,这Unity自带的高光反射Shader,里面实现了对应投射阴影的Pass

//Unity自带的高光反射Shader,里面实现了对应投射阴影的Pass
Fallback "Specular"

创建材质,可以看到可以接收和投放多光源阴影。和标准漫反射相比。可以看到有高光效果,成功实现了常见的带法线、高光反射、有阴影且能收到多光源影响的常用shader。


21.2 知识点代码

Lesson21_标准光照着色器_标准高光反射.cs

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

public class Lesson21_标准光照着色器_标准高光反射 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 什么是标准高光反射Shader?

        //和上节课一样,所谓的标准高光反射Shader
        //其实就是一个常用的高光反射Shader而已

        //该Shader其实就是一个
        //带有法线(世界空间中计算-全局效果的表现更准确)的基于BlinnPhong光照模型的
        //支持多光源和阴影的Shader

        #endregion

        #region 知识点二 制作常用高光反射Shader

        //1.新建一个Shader,取名叫Lesson21_BumpedSpecular(凹凸镜面反射)
        //2.复制 Shader开发入门中 Lesson30_Bump_Texture_World_High_Performance.shader,粘贴到新建文件中
        //3.加入渲染标签Tags { "RenderType"="Opaque" "Queue"="Geometry"}
        //  渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)
        //4.加入阴影、衰减相关代码
        //5.加入附加渲染通道
        //6.加入FallBack "Specular"

        #endregion
    }
}

Lesson21_BumpedSpecular.shader

Shader "Unlit/Lesson21_BumpedSpecular"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)//  漫反射颜色 
        _MainTex("MainTex", 2D) = ""{}//  单张纹理
        _BumpMap("BumpMap", 2D) = ""{}//  法线纹理
        _BumpScale("BumpScale", Range(0,1)) = 1//  凹凸程度
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)//  高光反射颜色
        _SpecularNum("SpecularNum", Range(0,20)) = 18//  光泽度
    }
    SubShader
    {
        //  加入渲染标签Tags { "RenderType"="Opaque" "Queue"="Geometry"}
        //  渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)
        Tags
        {
            "RenderType"="Opaque" "Queue"="Geometry"
        }

        //Base Pass
        Pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            //使用阴影宏内置文件
            #include "AutoLight.cginc"

            float4 _MainColor; //漫反射颜色
            sampler2D _MainTex; //颜色纹理
            float4 _MainTex_ST; //颜色纹理的缩放和平移
            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移
            float _BumpScale; //凹凸程度
            float4 _SpecularColor; //高光颜色
            fixed _SpecularNum; //光泽度

            struct v2f
            {
                //裁剪空间下坐标
                float4 pos:SV_POSITION;

                //纹理uv

                //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
                //float2 uvTex:TEXCOORD0;//颜色纹理
                //float2 uvBump:TEXCOORD1;//法线纹理

                //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
                float4 uv:TEXCOORD0; //纹理变量


                //顶点相对于世界坐标的位置 主要用于 之后的 视角方向的计算
                //float3 worldPos:TEXCOORD1;

                //切线 到 世界空间的 变换矩阵
                //float3x3 rotation:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD1;
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;


                //阴影坐标宏
                //n为下一个可用的插值寄存器的索引值(结构体前面有几个TEXCOORD就填几)
                //比如这里前面有四个
                SHADOW_COORDS(4)
            };

            v2f vert(appdata_full v)
            {
                v2f v2f;

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

                //计算纹理的缩放偏移
                v2f.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                v2f.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
                //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex);

                //把模型空间下的法线、切线转换到世界空间下
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldTangent = UnityObjectToWorldDir(v.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                //坐标转换宏
                TRANSFER_SHADOW(v2f)

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //世界空间下光的方向
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                //纹理采样函数tex2D
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

                //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);

                //乘以凹凸程度的系数
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


                //把计算完毕后的切线空间下的法线转换到世界空间下
                // float3 worldNormal = mul(v2f.rotation, tangentNormal);

                //以下两种写法等价 但是可以一步到位
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, tangentNormal);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                                            dot(v2f.TtoW2.xyz, tangentNormal));


                //接下来就来处理 带颜色纹理的 布林方光照模型计算

                //颜色纹理和漫反射颜色的 叠加
                fixed3 albedo = tex2D(_MainTex, v2f.uv.xy) * _MainColor.rgb;

                //兰伯特漫反射颜色 = 光的颜色 * 漫反射材质的颜色 * max(0, dot(世界坐标系下的法线, 光的方向))
                fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, normalize(lightDir)));

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

                //高光反射的颜色 = 光的颜色 * 高光反射材质的颜色 * pow(max(0, dot(世界坐标系下的法线, 半角向量)), 光泽度)
                fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
                    max(0, dot(worldNormal, halfA)), _SpecularNum);

                //计算光照衰减和阴影衰减的宏
                UNITY_LIGHT_ATTENUATION(atten, v2f, worldPos);

                // //布林方光照颜色 = 环境光颜色 + 兰伯特漫反射颜色 + 高光反射的颜色
                // fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

                //布林方光照颜色 = 环境光颜色 + (兰伯特漫反射颜色 + 高光反射的颜色)*衰减值
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

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

        //Additional Pass
        Pass
        {
            Tags
            {
                "LightMode"="ForwardAdd"
            }
            
            //线性减淡的效果 进行 光照颜色混合
            Blend One One
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            //使用阴影宏内置文件
            #include "AutoLight.cginc"

            float4 _MainColor; //漫反射颜色
            sampler2D _MainTex; //颜色纹理
            float4 _MainTex_ST; //颜色纹理的缩放和平移
            sampler2D _BumpMap; //法线纹理
            float4 _BumpMap_ST; //法线纹理的缩放和平移
            float _BumpScale; //凹凸程度
            float4 _SpecularColor; //高光颜色
            fixed _SpecularNum; //光泽度

            struct v2f
            {
                //裁剪空间下坐标
                float4 pos:SV_POSITION;

                //纹理uv

                //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
                //float2 uvTex:TEXCOORD0;//颜色纹理
                //float2 uvBump:TEXCOORD1;//法线纹理

                //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
                float4 uv:TEXCOORD0; //纹理变量


                //顶点相对于世界坐标的位置 主要用于 之后的 视角方向的计算
                //float3 worldPos:TEXCOORD1;

                //切线 到 世界空间的 变换矩阵
                //float3x3 rotation:TEXCOORD2;

                //代表我们切线空间到世界空间的 变换矩阵的3行 三个变量的w存世界坐标
                float4 TtoW0:TEXCOORD1;
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;


                //阴影坐标宏
                //n为下一个可用的插值寄存器的索引值(结构体前面有几个TEXCOORD就填几)
                //比如这里前面有四个
                SHADOW_COORDS(4)
            };

            v2f vert(appdata_full v)
            {
                v2f v2f;

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

                //计算纹理的缩放偏移
                v2f.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                v2f.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //得到世界空间下的 顶点位置 用于之后在片元函数中计算视角方向(世界空间下的)
                //v2f.worldPos = mul(unity_ObjectToWorld, appdata_full.vertex);
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex);

                //把模型空间下的法线、切线转换到世界空间下
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldTangent = UnityObjectToWorldDir(v.tangent);

                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                //cross是叉乘 appdata_full.tangent.w代表叉乘结果方向
                float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;

                //这个就是我们 切线空间到世界空间的 转换矩阵
                //       |    |     |
                //      切线 副切线 法线
                //       |    |     |
                //v2f.rotation = float3x3( worldTangent.x, worldBinormal.x,  worldNormal.x,
                //                          worldTangent.y, worldBinormal.y,  worldNormal.y,
                //                          worldTangent.z, worldBinormal.z,  worldNormal.z);
                v2f.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                v2f.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                v2f.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                //坐标转换宏
                TRANSFER_SHADOW(v2f)

                return v2f;
            }

            fixed4 frag(v2f v2f) : SV_Target
            {
                //世界空间下光的方向
                fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //世界空间下视角方向
                // fixed3 viewDir = normalize(UnityWorldSpaceViewDir(v2f.worldPos));
                float3 worldPos = float3(v2f.TtoW0.w, v2f.TtoW1.w, v2f.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                //纹理采样函数tex2D
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, v2f.uv.zw);

                //利用内置的UnpackNormal函数对法线信息进行逆运算以及可能的解压
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);

                //乘以凹凸程度的系数
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));


                //把计算完毕后的切线空间下的法线转换到世界空间下
                // float3 worldNormal = mul(v2f.rotation, tangentNormal);

                //以下两种写法等价 但是可以一步到位
                //float3x3 rotation = float3x3(v2f.TtoW0.xyz, v2f.TtoW1.xyz, v2f.TtoW2.xyz );
                //float3 worldNormal = mul(rotation, tangentNormal);
                //本质 就是在进行矩阵运算
                float3 worldNormal = float3(dot(v2f.TtoW0.xyz, tangentNormal), dot(v2f.TtoW1.xyz, tangentNormal),
                                            dot(v2f.TtoW2.xyz, tangentNormal));


                //接下来就来处理 带颜色纹理的 布林方光照模型计算

                //颜色纹理和漫反射颜色的 叠加
                fixed3 albedo = tex2D(_MainTex, v2f.uv.xy) * _MainColor.rgb;

                //兰伯特漫反射颜色 = 光的颜色 * 漫反射材质的颜色 * max(0, dot(世界坐标系下的法线, 光的方向))
                fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, normalize(lightDir)));

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

                //高光反射的颜色 = 光的颜色 * 高光反射材质的颜色 * pow(max(0, dot(世界坐标系下的法线, 半角向量)), 光泽度)
                fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
                    max(0, dot(worldNormal, halfA)), _SpecularNum);

                //计算光照衰减和阴影衰减的宏
                UNITY_LIGHT_ATTENUATION(atten, v2f, worldPos);

                // //布林方光照颜色 = 环境光颜色 + 兰伯特漫反射颜色 + 高光反射的颜色
                // fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

                //布林方光照颜色 = 环境光颜色 + (兰伯特漫反射颜色 + 高光反射的颜色)*衰减值
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

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

    //Unity自带的高光反射Shader,里面实现了对应投射阴影的Pass
    Fallback "Specular"
}


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

×

喜欢就点赞,疼爱就打赏