TheLudGamer 发表于 2021-12-9 12:59

Unity Shader基础光照详解(含推导)

《Unity Shader入门精要》学习笔记(8)-Shader光照详解(正式篇第二篇)

这个系列将会分享我在学习《Unity Shader入门精要》这本书的时候学习到的重点内容和一些心得体会,在书的基础上如果有能力的话会进行一定程度上的创新,以下是本系列的第八篇.这一篇中,我们会对Unity Shader进行更为详细的介绍.
时间:2021.11.28
在阅读这篇文章之前,读者最好已经阅读了前置的关于渲染管线和Shader基本介绍,以及关于数学部分介绍的文章,详细可以点击专栏阅读:
技美学习 - 知乎 (zhihu.com)
另外,与光照模型有关的一些知识在下述文章中也有提及,如果读者对计算机图形学中的光线追踪感兴趣的话可以去阅读:
基于Whitted模型的光线追踪 - 知乎 (zhihu.com)
这篇文章对应的书的位置:

基础篇 第6章
注:本章着重讲述光照模型的原理,因此实现的Shader往往并不能直接应用到实际项目中(直接使用会缺少阴影、光照衰减等效果).,比较完整的Shader光照介绍将会放在后面完成.
1.我们如何看到世界

通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象。

[*]首先,光线从光源 (light source) 中被发射出来。
[*]然后,光线和场景中的一些物体相交:一些光线被物体吸收了,而另一些光线被散射到其他方向。
[*]最后,摄像机吸收了一些光,产生了一张图像。
以下将详细介绍.
(1)光源

       实时渲染中,通常把光源当成一个没有体积的点,用l 来表示它的方向.光的量化则通常使用辐照度(irradiance)来进行.对于平行光来说,它的辐照度可通过计算在垂直于l 的单位面积上单位时间内穿过的能量来得到。
       很多时候,物体表面与光的方向并不是垂直的,此时的辐照度可以使用光源方向l 和表面法线n 之间的夹角的余弦值来得到.(这是因为倾斜照入的时候一定的损失).
       请注意,在后续大量的光照模型推算中,我们均认为向量是归一化的,也就是模为1,这也会大大减少光照模型推导的工作量.
       下图展示了这种计算方式的意义:


用点积来计算辐照度

       因为辐照度是和照射到物体表面时光线之间的距离d /cos成反比的,因此辐照度就和cos 成正比。cos 可以使用光源方向l 和表面法线n 的点积(参考之前文章:点积的意义)来得到。这就是使用点积来计算辐照度的由来。
(2)吸收和散射

       光线从光源发射出来后,会与一些物体相交.这里面相交的两种结果分别就是吸收与散射.   
散射与吸收的区别与联系

       散射只改变光线的方向,但不改变光线的密度和颜色。而吸收只改变光线的密度和颜色,但不改变光线的方向。如果发生了散射,则散射的方向可能在物体内部(这种叫做折射或透射),也可能会在物体外部(这种称为反射).
       注意,这里我们以普遍理性而论,即光源是在物体外部的,这样会比较好考虑问题.
       参考图如下:


       在本篇中,我们会首先介绍散射中的高光反射(镜面反射)与漫反射模型.根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们通常使用出射度 (exitance) 来描述它。辐照度和出射度之间的关系,是我们所比较感兴趣的问题.
       注:在本节中的漫反射符合Lambert模型(也就是说,反射的光线在各个方向分布均匀),后续还会介绍.
(3)着色

       着色 (shading) 指的是,根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型(Lighting Model) 。在下一小节中我们将会推导各种常见的光照模型.
(4)BRDF光照模型

       当已知光源位置和方向、视角方向时,我们就需要知道一个表面是如何和光照进行交互的。BRDF会为我们提供包含了对该点外观的完整的描述。在图形学中,BRDF大多使用一个数学公式来表示,并且提供了一些参数来调整材质属性。通俗来讲,当给定入射光线的方向和辐照度后,BRDF可以给出在某个出射方向上的光照能量分布。(可以简单理解成,BRDF更像是一张映射表,给定一个入射光线的基本信息后,可以计算出不同出射方向的能量分布(后续还会单独总结)).
       在书中这一章的BRDF并不能真实的反映物体和光线的交互,而是经验模型,关于更为真实的物理渲染会在后续进行整理总结.
牢记一句"定理"

计算机图形学的第一定律:如果它看起来是对的,那么它就是对的。(的确如此)
2.标准光照模型

       在BRDF理论被提出之前,比较广泛使用的是标准光照模型,也即Phong模型.
       标准光照模型只关心直接光照(direct light),也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。具体地,可以将进入摄像机的光线分为四个部分,所以光线就相当于是四个部分复合而成,每个部分提供一定的贡献度.
这四个部分如下:

[*]自发光 (emissive) 部分,本书使用 c emissive 来表示。这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。需要注意的是,如果没有使用全局光照(global illumination)技术**,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而已。
[*]高光反射 (specular) 部分,本书使用 c specular 来表示。这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
[*]漫反射 (diffuse) 部分,本书使用 c diffuse 来表示。这个部分用于描述,当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。
[*]环境光 (ambient) 部分,本书使用 c ambient 来表示。它用于描述其他所有的间接光照。
(1)环境光

       在真实的光照情况下,一个物体也可能会被间接光照所照亮.直观可以理解成物体同样也可能会吸收或散射从其他物体散射出来的光.我们可以用一个光线追踪的模型来表现这一点:


       可以看到,光线通常会在多个物体之间反射,而这种间接光照是比较复杂的,所以在标准光照模型中,对于这种间接光照做了简化处理,直接使用一种被称为环境光的部分来近似模拟间接光照。也就说,认为有一个全局的环境光,用来代表间接光照综合起来的效果,而场景中的所有物体也都使用这个相同的环境光,公式如下:



(2)自发光

       光线也可能由光源出发直接到达照相机,不经过任何反射.这样的分量用自发光来表示,直接采用材质的自发光颜色属性,公式如下:


       对于物体的普通的实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当成一个光源。自发光物体对周围造成影响的情况将在后续进行表述.
(3)漫反射

       漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模的。在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。
       可以用下面的图片来表示这种漫反射表面的特点:


       注:该模型用于反映粗糙表面的效果.
Lambert定理

       漫反射的光照模型符合Lambert定理,这个定理描述如下:
       反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。
       推导过程如下:


       本书给出的公式与这个推导过程一致,只是变量的符号不同:



       其中,n是表面法线,l是指向光源的单位矢量(请注意,l是指向光源的), m diffuse 是材质的漫反射颜色, c light 是光源颜色。需要注意的是,我们需要防止法线和光源方向点乘的结果为负值,为此,我们使用取最大值的函数来将其截取到0,这可以防止物体被从后面来的光源照亮。
(4)高光反射

       这里的高光反射是一种经验模型,也就是说,它并不完全符合真实世界中的高光反射现象。这种模型可以用如下的示意图来表示:
(a)Phong模型



       对于高光反射(镜面反射模型)而言,最理想的反射方向应该是和入射方向相教法线的夹角相等(对应上图的理想反射方向r),Phong模型是对理想的反射方向与实际的反射方向求了一个夹角(当然,要归一化),来模拟衰减的效果.
       那么,接下来如果我们能够在已知n,l,v的前提下求出r,则可以求出在不同方向的反射光强,r的求解推导如下:



所以,r的表达式如下:


此时的Phong模型表达式如下:



m specular 是材质的高光反射颜色,它用于控制该材质对于高光反射的强度和颜色。 c light 则是光源的颜色和强度。同样,这里也需要防止(v·r)的结果为负数。
mgloss是什么?

       这个指数指的是高光系数,它用于控制高光区域的“亮点”有多宽, m gloss 越大,亮点就越小。这个系数可以用来确定光强度衰减的快慢**,随着高光系数的增加,光在某一区域内的衰减速度会越来越快,从而产生"高光"效果.
       比如,当高光系数->∞时,对应镜子,其范围在100-200之间时,对应金属材料,而在5-10之间时则对应类似塑料的材质
       随着高光系数的变化,这种衰减如下(注:这里面的α指的就是高光系数):


(b)改进后的Blinn-Phong模型

       改进后的模型采用了中值向量的思想,简化了运算.
       中值向量h是l和v的中值单位向量, 即 h = ( l + v ) / | l + v |;
       n和h的夹角ψ称为中值角(halfway angle) ,当v位于l、 n和r所在平面时,可以证明 2ψ = (所以实际上Blinn-Phong与Phong模型在一定程度上是类似的),示意图如下:


也就是说,在改善后的Blinn-Phong模型中,我们用中值单位向量与法线的夹角来代替原来Phong模型中的理想反射方向与实际方向的夹角.这样可以大大简化运算.
关于 2ψ = 的证明





此时,Blinn-Phong模型如下:



两种模型的对比

       这部分可以参考书上的描述,如下:


(5)逐像素/逐顶点

       刚才我们介绍了基本的光照模型,那么这种计算要放在Shader的哪一个步骤,就是接下来要考虑的问题.
       通常来讲,我们有两种选择:在片元着色器中计算,也被称为逐像素光照 (per-pixel lighting) ;在顶点着色器中计算,也被称为逐顶点光照 (per-vertex lighting) 。
(a)逐像素光照

       在逐像素光照中,会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算.这种在面片之间对顶点法线进行插值的技术被称为Phong 着色 (Phong shading) ,也被称为Phong插值或法线插值着色技术。注意,这不同于刚才所说的Phong光照模型。
(b)逐顶点光照

       也被称为高洛德着色 (Gouraud shading).在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。
       不过,由于这种计算基于线性插值,所以当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。
       而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。
       后续我们会对这些问题以及两者的比较进行测试.
(6)标准光照模型的局限性

       有很多重要的物理现象无法用Blinn-Phong模型表现出来,例如菲涅耳反射 (Fresnel reflection)(有关于折射的问题).
       还有一点,Blinn-Phong模型是各项同性 (isotropic) 的,也就是说,当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。但有些表面是具有各向异性 (anisotropic) 反射性质的,例如拉丝金属、毛发等。后续在更为复杂的光照模型中将会对这些情况进行讨论.
3.Unity中的环境光与自发光

       打开Window->Rendering->Lighting,如下图所示:


       然后做出以下选择,其中②的部分即为环境光的部分,可以对来源和强度进行调节.



       对于大多数物体来说,其一般是没有自发光属性的,因此如果我们想要实现自发光的效果,可以采用直接在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上,即可实现简易的自发光的效果.
4.Unity Shader中实现漫反射光照模型

(1)公式回顾




我们一共需要四个参数: 入射光的颜色和强度clight,材质的漫反射系数mdiffuse,表面法线n以及光源方向l
我们需要用max函数来处理可能会出现的负数情况.在这里使用CG的函数saturate.
saturate函数

可以参考如下文章:
Unity shader saturate - jiahuafu - 博客园 (cnblogs.com)
简而言之,如果参数<0,则会变为0.大于1则会变为1,否则按原来输出.由于当n,l归一化之后其点积结果必定小于1,因此用这个函数处理是比较合适的.
(2)实践

为了避免干扰,我们首先关掉Unity的天空盒SkyBox.
(a)新建一个Shader和Material

       这里面的Shader类型为Unlit Shader,取名叫做DiffuseTestShader,材质取名叫DiffuseTest01,将shader赋予材质,并新建一个capsule,将材质赋予capsule.
此时的情况应该如我们之前所描述过的,是一个白色的看不出立体感的效果,如下图所示:


(b)编写Shader实现逐顶点漫反射效果

(i)首先,可以给Shader改一个名字,在这里更改:
Shader "MyShader/Lighting/DiffuseTestShader"//我更改的名字
(ii)为了得到并控制漫反射的颜色,需要一个Properties语义块,定义如下:
Properties
{
    _Diffuse("Diffuse",Color)=(1,1,1,1) //注意这里没有分号.
}
(iii)接下来,我们定义一个SubShader中的Pass模块,并指定该Pass的光照模式:
SubShader{
    Pass{
      Tags {"LightMode"="ForwardBase"}
    }
}
关于LightMode标签的用处和写法在后续会有说明,在这里我们只需要知道其用于定义该Pass在Unity的光照流水线中的角色,并且只有定义正确了,才能得到一些Unity的内置光照变量.
(iv)用CGPROGRAM和ENDCG包围我们写的代码块,使用#pragma指令定义顶点着色器和片元着色器的名字:
同时为了引入一些Unity Shader中的变量,我们使用#include指令包含Lighting.cginc
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
ENDCG
(v)为了在Shader中使用属性块定义的变量,我们需要再定义一个与之相匹配的变量:
fixed4 _Diffuse;
此时我们就可以得到漫反射公式中重要的参数之一——材质的漫反射属性.由于颜色范围在之间,因此用fixed精度存储是没有问题的.
(vi)定义顶点着色器与片元着色器的结构体
思考一下,顶点着色器需要知道法线信息,同时要把计算出的光照颜色传递给片元着色器,所以定义应该如下:
struct a2v
{
    float4 vertex:POSITION;
    float3 normal:NORMAL;
};//有分号
struct v2f
{
    float4 pos:SV_POSITION;
    fixed3 color:COLOR;//注意,未必一定使用COLOR语义,比如TEXCOORD0也是可以的
};
(vii)顶点着色器的编写
在这里我们实现逐顶点的漫反射光照,所以漫反射部分的计算也都在顶点着色器中:
v2 vert(a2v v)
{
    v2f o;
    //先转到裁剪空间去,顶点着色器的基本要求
    o.pos=UnityObjectToClipPos(v.vertex);
    //通过Unity内置变量获得环境光
    fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
    /*计算漫反射光照,刚才我们已经知道了法线方向v.normal以及材质的漫反射颜色_Diffuse
      还需要知道光源颜色和强度(Unity提供_LightColor0,但需要定义合适的LightModel标签)
      光源方向由_WorldSpaceLightPos0得到(假设场景中只有一个光源且为平行光,否则不能直接这么运算)

      计算光源方向与法线的点积是,务必确保两者在同一坐标系下,在这里我们选择世界坐标系
      a2v结构体中的法线是模型坐标,所以要转到世界坐标(使用顶点变换矩阵的逆转置矩阵,之前有说)

      因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。
      最后,我们只需要归一化并点乘,同时使用saturate函数即可.利用公式求解蛮烦色项,再与环境光部分相加即可.
    */
    fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));//注意,这个矩阵名字在高版本是这个
    fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
    //开始计算
    fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
    o.color=diffuse+ambient;
    return o;
}
(viii)片元着色器
只需要利用顶点着色器计算出的颜色即可.
fixed4 frag(v2f i):SV_Target
{
    return fixed4(i.color,1.0);
}
最后,设置一个Fallback即可
Fallback "Diffuse"
至此,逐顶点的漫反射模型就书写完毕了.
一些可能的报错信息

a)请注意struct结构体后面是否有分号;
b)请注意float4x4这个x是字母x,不是输入法打出乘号×;
c)注意一些接口方面的升级;
得到的漫反射效果如下图所示:



实际上,对于一些细分程度较低的模型,逐顶点光照会出现一些视觉问题,比如锯齿等,这可以通过逐像素的漫反射光照来改善.
(c)编写Shader实现逐像素光照

此时我们将光照模型的计算放置到片元着色器当中去进行,代码如下:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "MyShader/Lighting/DiffuseTestFrag"
{
    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;
                float3 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);
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb*saturate(dot(worldNormal, worldLightDir));
                fixed3 color = ambient + diffuse;
                return fixed4(color, 1.0);
            }
            ENDCG
      }
    }
    FallBack "Diffuse"
}
       逐像素光照可以得到更加平滑的光照效果。但是,即便使用了逐像素漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的,没有明暗的变化,情况大概如下图所示:


       即使添加了环境光,这种问题还是会存在.这就会损失一些模型相关的细节.为此,有一种改良模型被提出来,这就是半兰伯特光照模型.
(3)半兰伯特光照模型

       刚才我们所讨论的模型是兰伯特模型,但如果点积是负数的情况下就会被更改为0,造成细节的损失,因此,半兰伯特模型解决的是让背光面也可以有明暗变化,使得不同的点积结果会映射到不同的值上。
       如果我们将兰伯特模型推广到广义上,则广义上的兰伯特模型公式如下:


       而半兰伯特模型往往采用如下公式:


       相当于对结果进行了一个偏移,可以将点积结果从[-1,1]映射到,此时背面也是会有明暗变化的.
       注意,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
对刚才的代码做出以下修改可以将其更改为半兰伯特模型:
fixed4 frag(v2f i) :SV_Target
{
    //...与之前一样
    fixed halflambert = 0.5 + 0.5*dot(worldNormal, worldLightDir);
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb*halflambert;
    fixed3 color = ambient + diffuse;
    return fixed4(color, 1.0);
}
(4)三种情况的对比

以下三个Capsule从左到右分别为:
逐顶点的Lambert模型,逐像素的Lambert模型以及Half-Lambert模型:



可以看到,第三种在处理背面的时候会更好一些(但并不符合真实的物理世界).
5.Unity Shader中实现高光反射光照模型

(1)公式回顾



这里我们同样需要几种信息:光源的颜色和强度Clight,材质的高光反射系数mspecular,视点方向v以及理想反射方向r,以及高光系数mgloss,其中:


reflect函数

函数接口:reflect(i,n);
参数 :i,入射方向;n,法线方向。可以是float、float2、float3等类型。
描述 :当给定入射方向i和法线方向n时,reflect函数可以返回反射方向。下图给出了参数和返回值之间的关系。


注意,在高光模型中这里的入射方向和公式中的l方向是相反的,写代码的时候需要注意.
(2)实践

(a)逐顶点效果

(i)与往常一样,新建Shader,Material以及Capsule.
(ii)这里我们将漫反射与高光反射结合起来,这样做的好处是让我们的模型显得更真实一点.
关于Properties属性块:
Properties
{
    _Diffuse("Diffuse",Color)=(1,1,1,1)
    _Specular("Specular",Color)=(1,1,1,1) //高光反射颜色
    _Gloss("Gloss",Range(8.0,256))=20//高光系数
}
关于#include #pragma以及tags与漫反射模型是一样的,同时注意变量需要重新进行定义并与Properties中保持一致
以下直接给出代码:
Subshader
{

    Pass
    {
      Tags {"LightMode"="ForwardBase"}
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "Lighting.cginc"
      fixed4 _Diffuse;
      fixed4 _Specular;
      float _Gloss; //注意.类型写错了不会报错,但是结果是不能预料的,而且不好调试   
      ENDCG
    }
}
(iii)关于结构体的声明
由于我们是在顶点着色器当中处理,所以顶点着色器中需要包括法线方向,模型空间位置,而片元着色器中应包括SV_POSITION语义标识的变量以及顶点着色器算出的COLOR:
struct a2v
{
    float4 vertex:POSITION;
    float3 normal:NORMAL;
};
struct v2f
{
    float4 pos:SV_POSITION;
    fixed3 color:COLOR;   
};
(iv)在顶点着色器当中计算高光反射光照模型:
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));
    //新:镜面反射
    //1.视点方向
    fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);//把顶点坐标也变换到世界坐标,让坐标系统一
    //2.反射方向
    fixed3 reflectDir=normalize(reflect(-worldLightDir, worldNormal));//同样转换到世界坐标下面
    //3.高光反射项
    fixed3 specular=_LightColor0*_Specular.rgb*pow(saturate(dot(viewDir,reflectDir)),_Gloss);
    o.color=ambient+diffuse+specular;
    return o;
}
(v)片元着色器
fixed4 frag(v2f i):SV_Target
{
    return fixed4(i.color,1.0);
}
写完以上的Shader之后,观察效果:


       可以看到,这样的高光效果并不是特别理想(明显看到高光部分有棱角),这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。
解决方案:(b)逐像素光照

       如往常一样,新建一个Shader,这次我们的顶点着色器不需要进行光照模型的计算,只需要传递顶点坐标和法线方向给片元着色器,并在片元着色器中进行计算:      可执行的完整代码如下:
Shader "MyShader/Lighting/SpecularFrag"
{
    Properties
    {
      _Diffuse("Diffuse",Color)=(1.0,1.0,1.0,1.0)
      _Specular("Specular",Color)=(1.0,1.0,1.0,1.0)
      _Gloss("Gloss",Range(4.0,256))=8.0
    }
    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:<span class="n">TEXCOORD0;//世界坐标的法线
                float3 worldPos:TEXCOORD1; //在顶点着色器这里直接计算世界坐标比较好,方便后续运算
            };
            v2f vert(a2v v)//顶点着色器
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);//注意这里的unity的u是小写的
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                return o;//经过顶点着色器,返回的是世界坐标下的法线位置
            }
            fixed4 frag(v2f i):SV_Target
            {
                //环境光项
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                //光源方向
                fixed3 worldlightDir=normalize(_WorldSpaceLightPos0.xyz);
                //法线方向
                fixed3 worldNormal=normalize(i.worldNormal);
                //视口方向
                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
                //反射方向
                fixed3 reflectDir=normalize(reflect(-worldlightDir,worldNormal));
                //漫反射部分
                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldlightDir,worldNormal));
                //镜面反射部分
                fixed3 specular=
                  _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(viewDir,reflectDir)),_Gloss);
                fixed3 color=ambient+diffuse+specular;
                return fixed4(color,1.0);
            }
            ENDCG
      }
    }
    Fallback "Specular"
}
片元着色器处理的结果如下(见右图):



可以明显地看到,高光部分的处理变得平滑了许多,效果更好.
(c)Blinn-Phong光照模型

公式:



在这里由于片元着色器具有更好的光照效果,因此我们采用逐片元的方式来实现,只需修改片元着色器中的代码如下:
fixed4 frag(v2f i) :SV_Target
            {
                //环境光项
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
            //光源方向
            fixed3 worldlightDir = normalize(_WorldSpaceLightPos0.xyz);
            //法线方向
            fixed3 worldNormal = normalize(i.worldNormal);
            //视口方向
            fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
            //反射方向
            fixed3 reflectDir = normalize(reflect(-worldlightDir,worldNormal));
            //半程向量(新增)
            fixed3 halfDir = normalize(worldlightDir + viewDir);
            //漫反射部分
            fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldlightDir,worldNormal));
            //镜面反射部分
            fixed3 specular =
                _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
            fixed3 color = ambient + diffuse + specular;
            return fixed4(color,1.0);
      }
三种效果的比对

以下左,中,右分别为普通Phong模型的逐顶点光照,逐片元光照以及Blinn-Phong模型的效果:


       可以看出,Blinn-Phong光照模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝大多数情况我们都会选择Blinn-Phong光照模型。这三种都是经验模型,在一部分基于物理的渲染中,Blinn-Phong模型会更接近实际一些.
6.UnityShader内置函数

       在之前的代码中,我们仅仅考虑场景中有一个平行光,如果光源不止如此的话就会出现问题.在这里我们引入UnityShader内置的一些函数(位于UnityCG.cginc中):


       在这里可以看到,有些函数实现的内容就是我们刚才所写的,这里Unity为我们封装好了函数,比较方便.
       需要注意的是 ,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化。
       对于计算光源方向的3个函数:WorldSpaceLightDir、UnityWorldSpaceLightDir和ObjSpaceLightDir,需要注意的是 ,这3个函数仅可用于前向渲染(关于前向渲染后续会进行介绍.
利用UnityShader内置的函数改写Blinn-Phong模型

将之前的代码做出如下修改:
//1.顶点着色器修改
v2f vert(a2v v)//顶点着色器
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.worldNormal = UnityObjectToWorldNormal(v.normal);//Unity内置函数
    o.worldPos = mul(/span><span class="n">unity_ObjectToWorld,v.vertex).xyz;
    return o;//经过顶点着色器,返回的是世界坐标下的法线位
}
//2.片元着色器修改
fixed4 frag(v2f i) :SV_Target
{
    //...
    //光源方向
    //fixed3 worldlightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 worldlightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//Unity内置,默认不会归一化

    //视口方向
    //fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);//Unity内置,默认不归一化
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
    //...      
}
      
       至此,我们结束了光照部分的更新.从下一篇开始就开始介绍与纹理相关的内容了.感兴趣的朋友可以将本专栏收藏起来,后续会更新更多有用的内容.
页: [1]
查看完整版本: Unity Shader基础光照详解(含推导)