Doris232 发表于 2021-12-26 13:15

《Unity Shader入门精要》笔记(六)

本文为《Unity Shader入门精要》第六章内容的笔记。本文相关代码,详见:
原书代码,详见原作者github:
1. 光照原来

光照环境生成图像的3个物理现象:

[*]光线从光源中被发射出来;
[*]光线与场景中物体相交,一部分被物体吸收,另一部分被物体散射到其他方向;
[*]摄像机吸收了一些光产生了图像;
1.1 光源

实时渲染中,光源通常被当成一个没有体积的点,用I来来表示方向。
光学中用辐照度来量化光。对于平行光,辐照度通过计算垂直于I的单位面积上单位时间内穿过的能量得到。对于不垂直于物体表面的光,求辐照度可使用光源方向I和表面法线n之间的余弦值得到。



因为辐照度是和照在物体表面时光线之间的距离d/cosθ成反比,因此辐照度和cosθ成正比。cosθ可用光源方向I和表面法线n的点积得到,这就是使用点积来计算辐照度的由来。
1.2 吸收和散射

光线和物体相交,会产生散射和吸收。
散射只改变光线的方向,不改变密度和颜色;
吸收只改变光线的密度和颜色,不改变方向;
光线在物体表面散射后,一部分散射到物体内部,称为折射或投射;另一部分散射到物体外部,称为反射。
对于不透明物体,折射进入物体内部后的光线会继续和内部的颗粒进行相交,其中一部分光线重新发射出物体,另一部分则被物体吸收。那些从物体表面重新发射出的光线具有和入射光线不同的方向分布和颜色。



高光反射:光线直接从物体表面反射;
漫反射:光线被折射、吸收和散射出表面;
出射度:根据入射光线的数量和方向,计算出射光线的数量和方向;
本书中,漫反射被当做没有方向,即:光线向所有方向均匀分布;高光反射则只考虑特定方向。
1.3 着色

着色:根据材质属性(如漫反射光线属性等)、光源信息(如光源方向、辐照度等),使用一个公式去计算沿某个观察方向的出射度的过程。这个公式称为光照模型。
有时为了得到不同光照效果,会使用不同的光照模型。
1.4 BRDF光照模型

BRDF光照模型:给定入射光线的方向和辐照度,可以用它得到某个出射方向上的光照能量分布。
在图形学中,BRDF大多使用一个数学公式来表示。本文涉及的BRDF是对真实场景进行理想化或简化后的模型。也就是说,他们不能真实地反映问题和光线之间的交互,这些模型被称为经验模型。
在实时渲染中,考虑到性能消耗,一般还是会使用经验模型。虽然他们不能很真实地反映光照情况,但是他们“看起来是对的”。

计算机图形学的第一定律:如果它看起来是对的,那么它就是对的。如果希望更真实地模拟光和物体的交互,一般会使用基于物理的BRDF模型(后面章节会学到)。
2. 标准光照模型

标准光照模型只关心直接光照,即:直接从光源发射出来照到物体表面后,经过表面的一次反射进入摄像机的光线。(真实环境下,光照往往经过多次反射、折射后进入人眼)
标准光照模型将进入摄像机(人眼)的光线分为4部分:自发光、高光反射、漫反射、环境光。每部分使用一种方法计算其贡献度。
2.1 环境光

使用 https://www.zhihu.com/equation?tex=c_%7Bambient%7D 表示,用来描述除自发光、高光反射、漫反射以外的其他所有间接光照。这些间接光照,指的是:光线经过多个物体间多次反射后进入摄像机。
标准光照模型中,使用环境光近似模拟间接光照,简化计算。通常环境光是一个全局变量,场景里所有物体都使用它。

https://www.zhihu.com/equation?tex=c_%7Bambient%7D+%3D+g_%7Bambient%7D
2.2 自发光

使用 https://www.zhihu.com/equation?tex=c_%7Bemissive%7D 表示,描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。
标准光照模型使用自发光来计算光线直接由光源发射进入相机的部分。Unity中直接使用物体材质的自发光颜色: https://www.zhihu.com/equation?tex=c_%7Bemissive%7D+%3D+m_%7Bemissive%7D 。
通常在实时渲染中,自发光的表面往往不会照亮周围的表面,即:这个物体不会被当成一个光源。Unity中会引入全局光照系统来模拟这类自发光物体对周围物体的影响。
2.3 漫反射

使用 https://www.zhihu.com/equation?tex=c_%7Bdiffuse%7D 表示,描述光源射出光线照到物体表面时,该表面向每个方向散射的辐射量。
在漫反射中,观察视角的位置是不重要的,因为反射是完全随机的,可以认为在各个反射方向上,辐射量都是均匀的;而入射光线的角度很重要,它决定反射光线的强度。
漫反射光照符合兰伯特定律:反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。



(矢量的上方有一个向上的箭头表示这个矢量是单位矢量)

表示法线方向;
表示光源方向;
表示材质的漫反射颜色;
表示光源颜色;
为防止法线和光源方向点积结果为负值,这里使用max函数。几何意思:防止物体被后面来的光源照亮。
2.4 高光反射

使用 https://www.zhihu.com/equation?tex=c_%7Bspecular%7D 表示,描述当光线从光源照到物体表面时,该表面在完全镜面反射方向散射的辐射量。
高光反射可以让物体看起来有光泽,比如:金属质感、巧克力豆质感。
2.4.1 Phong模型

裴祥风(1942~1975),美国电脑CG研究学者,创立Phone光照模型。他提出了标准光照模型背后的基本理念。以Phong模型为例,计算高光反射需要知道:表面法线(n)、视角方向(v)、光源方向(I)、反射方向(r),这里的矢量都需要是单位矢量。



注意:
的方向是从表面指向光源,后面案例代码中会涉及。
这四个矢量中,反射方向可以由法线和光源方向计算得到:



Phong模型计算高光反射的公式:




表示材质的光泽度,也称为反光度,用于控制高光区域的“亮点”有多宽,这个值越大,亮点越小;
表示材质的高光反射颜色,用于控制该材质对高光反射的强度和颜色;
表示光源的颜色和强度;
公式里用到了视角方向和反射方向的点积,为了防止结果为负值,用max函数保证点积结果非负。
2.4.2 Blinn模型

在Phong模型基础上,Blinn简化了计算:用新的矢量来代替反射方向,是通过对和取平均后再归一化得到的:



Blinn光照模型:



Blinn模型计算高光反射的公式:



需要注意的是:Blinn模型用到的点积是法线和新的矢量,而不是视角方向和反射方向。
在硬件实现时,如果摄像机和光源距离模型足够远,Blinn模型会快于Phong模型,因为此时可以认为单位矢量和单位矢量都是定值,因此是一个常量;但是当单位矢量和单位矢量不是定值时,Phong模型会更快。
这两种光照模型都是经验模型,不能认为Blinn模型是对“正确的”Phong模型的近似。在一些情况下Blinn模型更符合实验结果。
2.5 逐像素还是逐顶点

在片元着色器中计算,被称为逐像素光照;
在顶点着色器中计算,被称为逐顶点光照。
2.5.1 Phong着色

在逐像素光照中,以每个像素为基础,得到它的法线(可以由顶点法线插值得到,也可以从法线纹理中采样得到),然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术称为Phong着色(与前面的Phong光照模型不是一回事儿),也称Phong插值、法线插值着色技术。
2.5.2 高洛德着色

在逐顶点光照中,以每个顶点为基础计算光照,然后在渲染图元内部用顶点计算的结果进行线性插值,最后输出成像素颜色的技术,称为高洛德着色。
两者的区别在光照的计算,是放在片元着色器中,还是放在顶点着色器中。逐顶点光照的计算量往往要小于逐像素光照,但是逐顶点光照在片元着色器中输出的像素光照是依赖线性插值得到的,当光照模型中有非线性计算(比如:高光反射计算)时,逐顶点光照的效果会出现问题,会导致渲染片元内部的颜色总是暗于顶点处的最高颜色值,某些情况下会产生明显的棱角现象。
3. Unity中的环境光和自发光

3.1 环境光

在Unity中,场景中的环境光可以在Window - Lighting的设置面板中的Ambient Source/Ambient Color/Ambient Intensity中控制。



在Shader中,我们可以调用Unity的内置变量UNITY_LIGHTMODEL_AMBIENT得到环境光的颜色和强度信息。
3.2 自发光

大多数物体没有自发光特性,本书大多数Shader中没有计算自发光的部分,若需要计算自发光,只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。
4. 在Unity Shader中实现漫反射光照模型

回顾漫反射的计算公式:




:入射光线的颜色和强度;
:材质的漫反射系数;
:表面法线;
:光源方向;
max函数是为了防止点击结果为负值,CG语言中有另一个函数可以达到同样的目的,即saturate函数:
// 参数x:可以是标量或矢量,float、float2、float3等类型
// 如果是标量,将x截取在范围内;
// 如果是矢量,会对矢量的每一个分量进行范围的限制;
saturate(x)

4.1 实践:逐顶点光照

4.1.1 准备工作

完成如下准备工作:

[*]新建名为Scene_6_4的场景,去掉天空盒子;
[*]新建名为DiffuseVertexLevelMat的材质;
[*]新建名为Chapter6-DiffuseVertexLevel的Unity Shader,并赋给上一步创建的材质;
[*]在场景中创建一个胶囊体,并将之前创建的材质赋给它;
[*]保存场景;

详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。
4.1.2 编写Shader代码

打开Chapter6-DiffuseVertexLevel,删除里面的所有代码,从第一行开始跟着下面的代码一行一行往下写,注释中已详细地对关键代码作了说明:
// 给Shader命名
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level"
{
    Properties
    {
      // 漫反射颜色的属性,默认为白色
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
    }
   
    SubShader
    {
      Pass
      {
            // LightMode用于定义该Pass在Unity的光照流水线中的角色
            // 后续会详细地了解,这里有个概念即可
            // 只有定义了正确的LightMode,才能得到一些Unity的内置光照变量
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            // 声明顶点/片元着色器的方法名
            #pragma vertex vert
            #pragma fragment frag

            // 为了使用Unity内置的一些变量,比如:_LightColor0
            #include "Lighting.cginc"

            // 声明属性变量
            fixed4 _Diffuse;

            struct a2v
            {
                // 顶点坐标
                float4 vertex : POSITION;
                // 顶点法线矢量
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                // 顶点着色器输出的颜色值
                // 这里不是必须使用COLOR语义,也可以使用TEXCOORD0语义
                fixed3 color : COLOR;
            };

            v2f vert (a2v v)
            {
                v2f o;
                // 将顶点坐标由模型空间转到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);

                // 通过Unity内置变量得到环境光的颜色值
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // 将法线由模型空间转到世界空间,并进行归一化(法线的矩阵转换需要使用逆矩阵来实现)
                // 交换矢量和矩阵的相乘顺序来实现相同的效果
                // 因为法线是三维矢量,所以只需截取对应矩阵的前三行前三列即可
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
                // 通过内置变量_WorldSpaceLightPos0获取世界空间下的光源方向,并进行归一化
                // 因为本案例中光源是平行光,所以可以直接取该变量进行归一化,若是其他类型光源,计算方式会有不同
                fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
                // 使用漫反射公式得到漫反射的颜色值
                // 通过内置变量_LightColor0,拿到光源的颜色信息
                // saturate,为取值范围限定的函数
                // dot,为矢量点积的函数,只有两个矢量处于同一坐标空间,点积才有意义
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

                // 最后对环境光和漫反射光部分相加,得到最终的光照结果
                o.color = ambient + diffuse;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 将顶点输出的颜色值作为片元着色器的颜色输出,输出到屏幕上
                return fixed4(i.color, 1.0);
            }

            ENDCG
      }
    }

    // 使用内置的Diffuse作为保底着色器
    Fallback "Diffuse"
}关于为何法线的坐标空间转换需要用逆矩阵反向相乘,详见《Unity Shader入门精要》笔记四的法线变换这一节里的内容。保存代码回到Unity场景,调整材质面板的漫反射颜色,可以看到类似这样的漫反射效果:



4.2 实践:逐像素光照

4.2.1 准备工作

完成如下准备工作:

[*]新建名为DiffusePixelLevelMat的材质;
[*]新建名为Chapter6-DiffusePixelLevel的Unity Shader,并赋给上一步创建的材质;
[*]在原来的场景中创建一个新的胶囊体(注意移动一下位置,不要与前一个胶囊体重叠),将刚刚创建的材质赋给它;
[*]保存场景;

4.2.2 编写Shader代码

打开Chapter6-DiffusePixelLevel,删除里面的所有代码,并复制粘贴上一节编写的Shader代码,做部分修改。
下面是逐像素光照的Shader代码,修改部分已用注释说明:
// 修改着色器的名字
Shader "Unity Shaders Book/Chapter 6/Diffuse Pixel-Level"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
    }
   
    SubShader
    {
      Pass
      {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                // 顶点着色器输出的世界空间下的法线
                // 用于在片元着色器中编写光照计算逻辑
                fixed3 worldNormal : TEXCOORD0;
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                // 顶点着色器只需要计算世界空间下的法线矢量,并传递给片元着色器即可
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 获取环境光颜色
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
               
                // 将世界空间下的法线矢量进行归一化
                fixed3 worldNormal = normalize(i.worldNormal);
                // 通过内置变量_WorldSpaceLightPos0获取世界空间下的光照方向,并进行归一化
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // 根据漫反射同时计算漫反射颜色值
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                // 将环境光和漫反射光相加,输出到屏幕
                fixed3 color = ambient + diffuse;
                return fixed4(color, 1.0);
            }

            ENDCG
      }
    }

    Fallback "Diffuse"
}保存代码,回到Unity场景,调整漫反射的颜色,可以看到类似下面的效果:



逐像素光照可以得到更加平滑的光照效果。
4.3 半兰伯特模型

前面实现的效果有个很大的问题,就是:光照无法达到的区域,模型外观是全黑的。如果从完全背对光的一面看模型,模型几乎看不到立体的效果。



因为这种漫反射光照模型符合兰伯特定律,所以也被称为兰伯特模型。
兰伯特定律:在平面某点漫反射光的光强和该反射点的法线和入射角度的余弦值成正比。4.3.1 概念及原理

为解决这个问题,Valve公司(也就是我们口中的“V社”)在开发游戏《半条命》时提出了一种技术,因为是在原来兰伯特光照模型的基础上进行的简单修改,所以被称为半兰伯特光照模型。
广义的半兰伯特光照模型的公式如下:



兰伯特光照模型公式如下:



与原兰伯特模型相比,半兰伯特模型对两个矢量点积的结果进行α倍的缩放加上β大小的偏移来防止点积的结果变成负值。
大多数情况,α和β的值均为0.5:



这样可以将两个矢量点积的结果范围由[-1, 1]映射到,也就实现了:即使观察点在背光面,看到的模型也不再是全黑的了。
注意:半兰伯特是没有任何物理依据的,仅仅是一个视觉加强技术。
4.3.2 代码实践:准备工作

接下来,用代码实现一下半兰伯特漫反射光照效果,准备工作如下:

[*]新建名为HalfLambertMat的材质;
[*]新建名为Chapter6-HalfLambert的Unity Shader,并赋给上一步创建的材质;
[*]在原来场景中创建一个新的胶囊体,并将第一步创建的材质赋给它;
[*]保存场景;
4.3.3 代码实践:Shader编码

打开Chapter6-HalfLambert,删除里面的所有代码,并复制粘贴Chapter6-DiffusePixelLevel的代码,做部分修改。
以下是半兰伯特光照模型的代码,修改部分已用注释说明:
// 修改着色器的名字
Shader "Unity Shaders Book/Chapter 6/Half Lambert"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
    }
   
    SubShader
    {
      Pass
      {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed3 worldNormal : TEXCOORD0;
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                return o;
            }

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

                // 计算漫反射的半兰伯特值
                fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
                // 将公式修改为半兰伯特模型的公式
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;

                fixed3 color = ambient + diffuse;
                return fixed4(color, 1.0);
            }

            ENDCG
      }
    }

    Fallback "Diffuse"
}保存代码,回到Unity,调整漫反射颜色值,可得到如下效果:



可以看到,尽管从背光面看,整个物体也是有立体感的。
5. 在Unity Shader中实现高光反射光照模型

回顾高光反射的计算公式:




:入射光线的颜色和强度;
:材质的高光反射系数;
:材质光泽度;
:观察视角方向;
:反射方向;
反射方向可以由表面法线和光源方向计算得到:



不过Unity已经内置了求反射方向的函数:
// 参数i:入射方向
// 参数n:法线方向
// 两个参数可以是float、float2、float3等类型
refrect(i, n)当给定入射方向和法线方向时,reflect函数可以返回反射方向:



注意:入射方向是指向反射点的。
5.1 实践:逐顶点光照

5.1.1 准备工作

完成如下准备工作:

[*]新建名为Scene_6_5的场景,并去掉天空盒子;
[*]新建名为SpecularVertexLevelMat的材质;
[*]新建名为Chapter6-SpecularVertexLevel的Unity Shader,并赋给刚刚创建的材质;
[*]在场景中创建一个胶囊体,并将刚刚创建的材质赋给赋给它;
[*]保存场景;

5.1.2 编写Shader代码

打开Chapter6-SpecularVertexLevel,删除里面的所有代码,编写如下代码(之前编写过的类似代码不再重复添加注释):
// 为Shader命名
Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level"
{
    Properties
    {
      _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
      // 高光反射叠加的颜色,默认为白色
      _Specular ("Specular", Color) = (1, 1, 1, 1)
      // 光泽度,控制高光区域的大小
      _Gloss ("Gloss", Range(8.0, 256)) = 20
    }

    SubShader
    {
      Pass
      {
            // 为了正确地得到一些Unity的内置光照变量,如_LightColor0
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            // 为了使用Unity内置的一些变量,如:_LightColor0
            #include "Lighting.cginc"

            /* 声明属性变量 */
            // 颜色值的范围在0~1,故使用fixed精度
            fixed4 _Diffuse;
            fixed4 _Specular;
            // 光泽度数值范围较大,使用float精度
            float _Gloss;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                // 顶点着色器输出颜色值到片元着色器
                fixed3 color : COLOR;
            };

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

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // 使用兰伯特漫反射模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
                // 使用reflect函数求出入射光线关于表面法线的反射方向,并进行归一化
                // 因为reflect的入射方向要求由光源指向焦点处(worldLightDir是焦点处指向光源),所以需要取反
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                // 通过Unity内置变量_WorldSpaceCameraPos得到世界空间中的相机位置
                // 通过与世界空间中的顶点坐标进行相减,得到世界空间下的视角方向
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

                // 根据高光反射公式求出高光反射颜色
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
               
                // 环境光+漫反射+高光反射
                o.color = ambient + diffuse + specular;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(i.color, 1.0);
            }
            ENDCG
      }
    }

    // 使用Unity内置的Specular Shader兜底
    Fallback "Specular"
}保存代码,将漫反射叠加颜色设为绛蓝色,将高光反射叠加颜色设为红色,得到如下效果:



5.2 实践:逐像素光照

5.2.1 准备工作

完成如下准备工作:

[*]新建名为SpecularPixelLevelMat的材质;
[*]新建名为Chapter6-SpecularPixelLevel的Unity Shader,并赋给上一步的材质;
[*]在原来的场景新建一个胶囊体,并将刚刚创建的材质赋给它;
[*]保存场景;
5.2.2 编写Shader代码

打开Chapter6-SpecularPixelLevel,删除里面所有代码,并复制粘贴上一节编写Chapter6-SpecularVertexLevel代码,做部分修改。
修改后的代码如下,修改部分已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 6/Specular Pixel-Level"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
      _Specular("Specular", Color) = (1, 1, 1, 1)
      _Gloss("Gloss", Range(8.0, 256)) = 20
    }

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

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            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 = mul(v.normal, (float3x3)unity_WorldToObject);
                // 将顶点坐标由模型空间转到世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 获取环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 对法线进行归一化
                fixed3 worldNormal = normalize(i.worldNormal);
                // 对光照方向进行归一化
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // 根据兰伯特漫反射公式计算漫反射
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                // 使用内置reflect函数获得反射光线,并进行归一化
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                // 计算视角方向
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                // 根据高光反射公式计算高光反射
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
               
                // 环境光+漫反射+高光反射
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
      }
    }

    Fallback "Specular"
}保存代码,设置与逐顶点光照相同的材质参数,得到如下效果:



将它与逐顶点光照(左)结果进行比对,可以明显看出逐像素光照(右)得到的高光反射效果更柔和:



5.3 Blinn-Phong光照模型

回顾Phong光照模型:



回顾Blinn光照模型(提出使用新的矢量h):



Blinn模型的高光反射公式:



Blinn光照模型提出用新的矢量代替光照的反射方向,使用法线和新矢量的点积,而不是视角方向和反射方向的点积。
接下来基于这个公式,用代码实现高光反射。
5.3.1 准备工作

完成如下准备工作:

[*]新建名为BlinnPhongMat的材质;
[*]新建名为Chapter6-BlinnPhong的Unity Shader,并赋给上一步创建的材质;
[*]在原来的场景中新建一个胶囊体,并将第一步创建的材质赋给它;
[*]保存场景;
5.3.2 编写Shader代码

打开Chapter6-BlinnPhong,删除里面的所有代码,将上一节Chapter6-SpecularPixelLevel代码复制粘贴进去,做部分修改。
修改后的代码如下,修改部分已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 6/Blinn Phong"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
      _Specular("Specular", Color) = (1, 1, 1, 1)
      _Gloss("Gloss", Range(8.0, 256)) = 20
    }

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

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            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 = mul(v.normal, (float3x3)unity_WorldToObject);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

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

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                // 通过入射方向+视角方向,得到新的矢量
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // 修改为Blinn公式,使用法线方向和新矢量的方向
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
               
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
      }
    }

    Fallback "Specular"
}保存代码,将材质调整为和之前一样的参数,得到如下效果:



对比逐顶点、逐像素、Blinn-Phong的高光反射效果:



可以看出,Blinn-Phong的高光反射部分看起来更大更亮,实际渲染中绝大多数情况会选择Blinn-Phong光照模型。
6. 使用Unity内置的函数

6.1 概念

关于Unity内置的函数,上面的案例代码中,我们已经有所接触,比如:
// 将顶点坐标从模型空间转到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);

// 在原书中,这部分代码是这样写的
// 模型空间的顶点坐标,通过与模型-世界-裁剪空间转换矩阵进行点积实现的
o.pos = normalize(UNITY_MATRIX_MVP, v.vertex);
我们前面写的案例全是基于平行光的实现,如果遇到更复杂的光照类型(比如:点光源、聚光灯),使用上面的代码会得到错误的结果,Unity则提供了一些内置函数来帮助我们计算这些信息。
下面是UnityCG.cginc里常用的一些辅助函数,之前笔记中有提过,现在再回顾一下:



上面的9个函数中,有5个我们已经掌握了其内部实现,甚至在案例中已经写过,例如WorldSpaceViewDir函数实现如下:
inline float3 UnityWorldSpaceViewDir(float3 worldPos)
{
    return _WorldSpaceCameraPos.xyz - worldPos;
}
与计算光源方向相关的3个函数:

[*]WorldSpaceLightDir
[*]UnityWorldSpaceLightDir
[*]ObjSpaceLightDir
这些函数的内部逻辑会稍微复杂一些,因为要针对不同种类的光源做不同的逻辑。需要注意的是:这3个函数仅可用于前向渲染(也就是之前Pass中Tags写的"LightMode" = "ForwardBase",后续会详细了解),因为函数内使用的一些内置变量,如_WorldSpaceLightPos0等,只有在前向渲染中才会被正确赋值。
接下来基于前面写的Blinn-Phong案例代码,使用内置函数进行调整。
6.2 实践:使用内置函数

6.2.1 准备工作

完成如下准备工作:

[*]新建名为Scene_6_6的场景;
[*]新建名为BlinnPhongInnerFuncMat的材质;
[*]新建名为Chapter16-BlinnPhongInnerFunc的材质,并赋给上一步创建的材质;
[*]新建一个胶囊体,并将第二步创建的材质赋给它;
[*]保存场景;
6.2.2 编写Shader代码

打开Chapter16-BlinnPhongInnerFunc,复制粘贴上一节Blinn-Phong模型的Shader代码,将部分代码逻辑改用内置函数。
修改后的代码如下,修改部分已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 6/Blinn Phong Inner Func"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
      _Specular("Specular", Color) = (1, 1, 1, 1)
      _Gloss("Gloss", Range(8.0, 256)) = 20
    }

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

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            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 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal = normalize(i.worldNormal);
                // 使用内置函数计算世界空间下的光照方向(函数返回值未经过归一化)
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                // 使用内置函数计算世界空间下的视角方向(函数返回值未经过归一化)
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
               
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
      }
    }

    Fallback "Specular"
}注意:内置函数得到的矢量是没有归一化的,需要自己调用normalize函数对结果进行归一化。

写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(六)