在现实生活中,光线照射到物体上,一部分光线反射进我们的眼睛,从而使我们看到了这个物体,知道这个物体的颜色,材质等等信息。这么说估计很抽象,举个相反的例子,你在漆黑的小屋里,能辨别出放在眼前的是什么东西是什么颜色么?你第一反应肯定是要来点光,点燃打火机,打开手机电棒什么的照一照你就能看到眼前的物体是什么就是这个意思。在电脑屏幕中,物体的高光,固有色,材质等特质的形成也是仿照现实世界光照原理的结果,而在决定如何在一个像素上进行光照计算时,使用的就是光照模型,在此要说明一下光照模型都是对真实场景进行理想化和简化后的模型,也可以称为是经验模型,尽管如此这些经验模型在图形渲染方面也应用了很多年。
首先还是大致阐述下现实中光的一些特性。我们用**辐照度(irradiance)**来描述光的数量,这时你可能就会疑惑这如何量化,你这是在忽悠我,说实话当我看到这个概念的时候我也觉得我被忽悠了,但是看到说明后才理解实际上这是一个相对的概念,不是描述光源本身的数量,而是描述物体接收到光的数量。对于平行光来讲,它的辐照度可以通过计算垂直于光源方向的单位面积上单位时间内穿过的能量来得到。在计算光照模型时就需要知道物体表面的辐照度,而物体表面和光源方向往往不是垂直的,因为物体表面法线是垂直于物体表面的,因此就可以用光源方向和表面法线之间夹角的余弦值来得到。具体说明如下图:
光线照射到物体要么发生散射要么被吸收,散射分为反射和折射两种情况,为了区分这两种不同的散射方向,在光照模型中使用了不同的部分来计算它们:高光反射部分表示物体表面是如何反射光线的,而漫反射部分则表示有多少光线会被折射、吸收。 标准光照模型,也称为Blinn-Phong光照模型,其是将进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算它的贡献度,这四个部分分别是自发光、高光反射、漫反射、环境光。标准光照模型本身有许多局限性,比如无法展现菲涅尔反射,无法展示各向异性。

自发光,用于描述当给定一个方向时,物体表面朝向该方向发射多少辐照量。光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的贡献度,计算则是直接使用材质的自发光颜色。在Shader中,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。
高光反射,描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少光线。
环境光,光线通常会在多个物体之间反射,最后进入摄像机,虽然标准光照模型的重点在于描述直接光照,但在真实世界中,物体也可以被间接光所照亮,因此在光照模型中以环境光来近似模拟间接光。在Shader中,我们可以通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT得到环境光的颜色和强度信息。
漫反射,用于描述当光线从光源照射到模型表面时,该表面会向每个方向散射多少光线。在漫反射中入射光线的角度很重要,漫反射光照符合兰伯特定律:反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。
得知上述参数的意义后,当我们想计算光照模型时,我们有两种选择:在片元着色器中计算,也称为逐像素光照;在顶点着色器中计算,也被称为逐顶点光照。两种各有利弊。 接下来我们动手来尝试实现一下光照模型,首先是漫反射光照部分,这里涉及到一个计算公式,如下: diffuse=(c·m)max(0,n·I),其中c表示入射光线的颜色和强度,m为材质的漫反射系数,n为表面法线,l为光源方向。max可以理解为一个函数,为了防止n与l的点积为负值,在CG中对应同等效应的saturate函数,saturate(x);x可以是标量或者是矢量,可以是float、float2、float3等类型,函数作用就是将x的值截取在[0,1]的范围内。 了解了漫反射光照的计算公式后,我们就用代码来实践一下,我们先在顶点函数中去计算漫反射,也就是逐顶点光照。打开Unity,新建一个场景,新建一个材质,再新建一个Standard Surface Shader,将这个Shader拖给材质,接着在场景中建一个胶囊体,将材质拖给胶囊体,双击打开Shader,我们来编写一些代码。 首先,在Properties语义块中声明一个Color类型的属性,并将初始状态设为白色: Properties { _Diffuse("Diffuse",Color)=(1,1,1,1) } 之后在SubShader语义块中定一个Pass块,在Pass块中写一个标签,指定LightMode为ForwardBase: SubShader { Pass { Tags{"LightMode"="ForwardBase"} } }
然后,开始CG代码部分,先写上CGPROGRAM和ENDCG作为开头和结尾,之后在中间用#pragma指令来定义顶点着色器和片元着色器,顶点着色器名称为vert,片元着色器名称为frag。为了能方便的使用UnityShader的一些内置变量,还需要引入内置文件Lighting.cginc: SubShader { Pass { Tags{"LightMode"="ForwardBase"} CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
ENDCG
}
}
为了能在CG代码中使用Properties块中的属性,需要重新用fixed4类型来声明Color类型的_Diffuse,通过这个就得到了漫反射计算公式中需要的材质漫反射属性m。之后,还需要定义顶点着色器的输入a2v和输出v2f结构体(输出结构体同时也是片元着色器的输入结构体): fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; //将模型的顶点信息存储到vertex变量中 float3 normal : NORMAL; //为了访问顶点法线,需要在a2v中定义一个normal变量,之后用NORMAL语义来表明在编译时将模型顶点法线信息存储到normal变量中 }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR; //color用来存储从顶点着色器中计算得到的光照颜色信息,这里:COLOR语义也可以换成:TEXCOORD0语义 };
之后我们来实现顶点函数部分: 因为计算都在顶点函数中,因此片元函数只需要将计算结果输出即可: fixed4 frag(v2f i):SV_Target { return fixed4(i.color,1.0);//i.color为rgb三个分量,1.0为alpha分量,凑在一起为就为颜色的rgba四个分量。 } 最后设置降级回调函数为Diffuse: FallBack "Diffuse"

这里需要说明一下,因为有些资料算是比较久了,为了验证其可用性笔者用的Unity版本为2019.3.5f1,像unity_WorldToObject,UnityObjectToClipPos(x)分别对应以前的_World2Object,mul(UNITY_MATRIX_MVP,x),如果是用以前的版本,将这两个函数替换一下即可。
完整代码如下: 运行后的结果如下,可以看到明暗交界线部分有明显锯齿,这也算是逐顶点光照特点: 未完待续,下一篇紧接着使用逐像素光照,对比一下效果


本文深入探讨光照模型在图形渲染中的应用,解析光照模型的基本原理,包括漫反射、高光反射、环境光和自发光等组成部分,并通过Unity Shader实例演示逐顶点光照的实现。
1459

被折叠的 条评论
为什么被折叠?



