1.1.1 Unity中的基础光照
从宏观上说,渲染包含了两大部分:决定一个像素的可见性,决定这个像素上的光照计算。而光照模型就是用于决定着一个像素上进行怎样的光照计算。
通过模拟真实的光照环境来生成一张图像,需要考虑3中物理现象:
[1]光线从光源(light source)中被发射出来
[2]光线和场景中的一些物体相交;一些光线被物体吸收了,而另一些光线被散射到其他方向
[3]摄像机吸收了一些光,产生了一张图像
通常把光源当作一个没有体积的点,用l来表示它的方向,用辐射度来量化光。对于平行光来说,光源的辐射度可通过计算在垂直于l的单位面积上单位时间内穿过的能量来得到。而在计算光照模型时,物体表面往往和l不垂直,对此,可通过使用光源方向l和表面发现n之间的夹角的余弦值来得到。
图 1.35 右图单位面积上接收到的光线少于左图
吸收和散射:
光线由光源发射出来后,就会与一些物体相交。通常,相交的结果有两个:散射(scattering)和吸收(absorption)。
散射只改变光线的方向,但不改变光线的密度和颜色;吸收只改变光线的密度和强度,不改变光线的方向。光线在物体表面经过散射后,有两种方向:一中将会散射到物体内部,该现象称为折射(refraction)或透射(transmission);另一种则会散射到外部,该现象称为反射(reflection)。
图 1.36 光线照射不透明物体后的过程
为区分两种不同的散射方向,在光照模型中使用了不同的部分来计算它们:高光反射(specular)部分表示物体表面是如何反射光线的,而漫反射(diffuse)部分则表示有多少光线会被折射、吸收和散射出表面。[个人可以是那么想的,高光反射是不进入物体内部,只看有多少光是会被弹开;而漫反射是需要光进入物体内部后,再与物体内部碰撞过后出来有多少光,以及被物体内部吸收了多少光,也就是损耗了多少光能,该光的反射后的方向是多变的]
着色:
着色(shading)指的是,根据材质属性(如漫反射属性)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。这个等式被称为光照模型(Lighting model)。不同的光照模型有不同的目的,如一些用于描述粗糙的物体表面,一些用于描述金属表面。
BRDF光照模型:
当给定模型表面上的一个点时,BRDF(Bidirectional Reflectance Distribution Function)包含了对该点外观的完整描述。在图形学中,BRDF大多使用一个数学公式来表示,并且提供了一些参数来调整材质属性。通俗来讲,当给定入射光线的方向和辐照度后,BRDF可以给出在某个出射方向上的光照能量分布。
1.1.2 标准光照模型
1973年,裴祥风提出标准光照模型背后的理念,标准光照模型只关心光照,也就是直接从光源发射出来照射到物体表面后,经过物体表面的第一次反射直接进入摄像机的光线。其基本方法是,把进入到摄像机内的光线分为4部分,每个部分使用一种方法来计算它的贡献度,4个部分分别是:
自发光(emissive)部分,该部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。如果没有全局光照(global illumination)技术,这些自发光并不会真的照亮周围的物体,而是它本身看起来更亮。
高光反射(specular)部分,该部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
漫反射(diffuse)部分,该部分用于描述,当光线从光源照射到模型表面时,该表面会像每个方向散射多少辐射量。
环境光(ambient)部分,该部分用于描述其他所有的间接光照。
(分析一张图片*有多少种光: https://m.weibo.cn/5596473314/4653168161722212,挺有意思的,可以看一下不同光的效果)
环境光:
在真实世界中,物体可以被间接光照(indirect light)所照亮,间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机,即在光线进入摄像机之前,经过了不止一次的物体反射,如在红地毯上放上一个浅色沙发,沙发底部将会有红色,该红色是由红地毯反射了一部分光线再反弹到沙发上。
在标准光照模型中,开发者使用一种被称为环境光的部分来近似模拟间接光照环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都适用这个环境光。下面的等式给出计算环境光的部分:
自发光:
光线直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的贡献度,其计算是直接使用了该材质的自发光颜色。
在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不能被当成一个光源。Unity5引入的全新的全局光照系统则可以模拟这类自发光物体对周围物体的影响。
漫反射:
漫反射光照是用于哪些被物体表面随机散射到各个方向的辐射度进行建模的。在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的,但入射光线的角度很重要。
漫反射光照符合兰伯特定律(Lambert’s law):反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。其公式如下:
n为表面法线; l为光源的单位矢量; m 为材质的漫反射颜色; c光源颜色
高光反射:
计算高光反射需要信息较多,如表面法线、视觉方向、光源方向、反射方向等。(高中或大学物理大多数的光照计算题皆是高光反射)
反射方向计算公式:
图 1.37 Phong模型计算高光
就算高光反射部分的公式:
m (gloss)为材质的光泽度(gloss),也被称为反光度(shininess),m (gloss)越大,亮点越小(即材质表面越光滑) m(specular) 为材质的高光反射颜色,其用于控制该材质对于高光反射的强度和颜色 c(light)为光源的颜色和颜色
与Phong模型相比,还有Blinn模型
图 1.38 Blinn模型
计算公式:
在片元着色器中计算,也被称为逐像素光照(pre-pixel lighting);在顶点着色器中计算,也被称为逐顶点光照(pre-vertex lighting)
在逐像素光照中,将以每个像素为基础,得到它的法线,从而进行光照模型的计算,该面片之间对顶点发现进行插值就散称为phong着色。
而逐顶点光照也被称为高洛德着色,在其中将会在每个顶点上计算光照,然后在渲染图元内部进行线性插值,最后输出像素颜色。
1.1.3 Unity中的环境光和自发光
在标准光照模型中,环境光和自发光的计算是最简单的。
在Unity中,场景中的环境光可以在Uindow->Lighting->Ambient Source/Ambient Color/Ambient Intensity中控制。如下图所示:
图 1.39 Unity组件控制环境光
在Shader中通过Unity的内置函数UNITY_LIGHTMODEL_AMBIENT可以得到环境光的颜色和强度信息。
而如果需要添加自发光,只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色即可。
1.1.4 Unity中实现漫反射光照模型
逐顶点光照:
Cg中的saturate函数可以起到防止点积结果为负值,起到max操作的作用[注:saturate译:饱和; 使饱和; 使湿透; 浸透;]
函数:saturate(x)
参数:x:用于草做的标量或矢量,可以是float、float2、float3等
描述:把x截取到[0,1]范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作
实践:逐顶点光照
[1]构建场景
[2]新建材质与shader文件,并将shader文件赋给该材质
[3]打开shader代码,并清空,并把下代码赋值上去
Shader "Unity Shaders Book/Chapter6/DiffuseVertexLevel" { //给该shader起名 Properties{ //得到并控制材质的漫反射颜色,在Properties语义块中生命一个color类型的属性,并初始值设为白色 _Diffuse("Diffuse", Color) = (1,1,1,1) } Subshader{ Pass{ //顶点/片元着色器的代码需要写在pass中,所以需要定义一个pass语义块而非subshader中 Tags{ "LightMode"="ForwardBase" //明确该pass的光照模式,LightMode标签是Pass标签中的一种,用于定义该Pass在Unity的光照流水线的角色,只有定义了正确的LightMode才可以得到一些Unity的内置光照变量 } CGPROGRAM //CGPROGRAM和ENDCG来包裹cg代码片,以定义顶点着色器和片元着色器 #pragma vertex vert //顶点着色器名字叫vert #pragma fragment frag //片元着色器名字叫frag #include "Lighting.cgnic" //使用内置的一些函数,从而需要包含对应内置文件 fixed4 _Diffuse; /因为在Properties中生命了实行,所以要定义一个与之匹配的变量。同时这里将得到漫反射公式所需要的材质的漫反射属性,颜色范围在0-1之间,从而使用fixed精度定义来存储 struct a2v{ //顶点着色器的输入结构体 float4 vertex : POSITION; float3 normal : NORMAL; //为访问顶点的法线从而需定义normal变量,并通过使用NORMAL来告诉Unity要把模型顶点的法线信息存储到normal变量中 } struct v2f{ //顶点着色器的输出结构体同时也是片元着色器的输入结构体 float4 pos : SV_POSITION; fixed3 color : COLOR; //为访问顶点着色器中急速三得到的光照颜色传递给片元着色器从而需要定义color变量 } v2f vert(a2v v){ //本例子关注如何实现逐顶点的漫反射光照,从而漫反射部分的计算都将在顶点着色器中进行 v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); //顶点坐标变化从模型空间转到投裁剪空间 //该部分就是开始按照前文所给的公式开始进行计算 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //通过该内置变量得到环境光部分 //法线和光源方向需要在同一个坐标轴下点积才有意义,因此皆转换为世界坐标系中 fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object)); fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//_WorldSpaceLightPos()提供光源方向 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight)); //_LightColor是unity提供的内置变量,可访问该Pass处理的光源颜色和强度,同时防止为负数,从而使用saturate函数 o.color = ambient + diffuse; return o; } fixed4 frag(v2f i):SV_Target{ //计算过程皆在顶点着色器中完成,从而片元着色器只需要把顶点颜色输出即可 return fixed(i.color, 2.0); } ENDCG } } Fallback"Diffuse" //把该Unity shader的回调shader设置为内置的Diffuse }
[4]可查看效果
逐像素光照:
[1]构建场景
[2]构建材质与shader,并把该shader赋给该材质
[3]复制代码,和上面的差不多但是顶点着色器不需要计算光照模型,只需要在世界空间下的法线传递给片元着色器即可;片元着色器需要计算漫反射光照模型
逐像素光照可以得到更加平滑的光照效果,而逐顶点光照对于戏份度程度较高的模型会得到比较好的光照效果,但对于细分成度较低的模型则会出现锯齿问题。但逐像素光照在光照无法到达的区域,模型的外观通常是全黑的,而对此可通过添加环境光来得到非全黑效果,但无法解决背光面明暗一样的缺点,从而半兰伯特光照模型被提出。
半兰伯特光照模型:
漫反射光照模型也被称为兰伯特光照模型。半兰伯特光照模型规律为:在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。而半兰伯特光照模型是在其基础上进行简单的修改。
广义半兰伯特光照模型公式如下:
在绝大多情况下,和的值取0.5,即:
通过该公式,可以将的结果范围从[-1,1]映射到[0,1]
半兰伯特没有任何物理依据,仅仅只是一个视觉加强技术。
fixed halfLambert = dot( worldNormal,worldLightDir ) *0.5 *0.5; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert.rgb; fixed3 color = ambient + diffuse; return fixed4(color,1.0);
图 1.40 效果对比图(逐顶点、逐像素、半兰伯特)
1.1.5 Unity中实现高光反射光照模型
函数:reflect(i,n)——[计算反射方向函数]
参数:i,入射方向;n,法线方向
当给定入射方向i和法线方向n时,reflect函数可以返回反射方向
图 1.41 reflect函数
逐顶点光照代码:
Shader "Unity Shaders Book/Chapter6/Specular Vertex-Level" { //材质面板中为方便控制高光反射属性,从而在Porperties语义块中声明3个属性 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" //标签,定义该pass在unity的光照流水线的角色 } CGPROGRAM //CG代码块开始 #pragma vertex vert //定义顶点着色器 #pragma fragment frag //定义片元着色器 #include "Lighting.cgnic" //调用内置文件 fixed4 _Diffuse; //定义与Properties对应的属性,color范围为[0,1],从而可用fixed来存储 fixed4 _Specular float _Gloss //Gloss范围较大,从而用float存储 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 = mul(UNITY_MATRIX_MVP,v.vertex); //顶点从模型空间转换到投影空间 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_World2Object)); //将法线坐标从模型空间转换到世界空间 fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //得到世界空间的光线方向 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir)); //计算扩散项 fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); //得到世界空间下反射后的方向 fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_Object2World, v.vertex).xyz); //得到世界空间下的视线方向 fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir, viewDir))._Gloss); //计算反射项 o.color = ambient + diffuse; return o; } fixed4 frag(v2f i):SV_Target{ return fixed(i.color, 1.0); } ENDCG //CG代码块结束 } } Fallback"Specular" }
逐像素光照则是片元着色器中计算关键的光照模型,而顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器。按像素方式可以得到更光滑的高光效果。
Blinn光照模型没有使用反射方向,而是插入新的矢量,并通过视角方向和光照方向相加后再归一化得到/
其高光反射公式如下:
修改代码:
v2f vert(a2v v){ //计算包含高光反射的光照模型 v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); //顶点从模型空间转换到投影空间 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_World2Object)); //将法线坐标从模型空间转换到世界空间 fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //得到世界空间的光线方向 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir)); //计算扩散项 fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); //得到世界空间下反射后的方向 fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); //得到世界空间下的视线方向 fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss); return fixed4(ambient + specular + diffuse, 1.0); }
图 1.42 高光反射(逐顶点、逐像素、blinn)
1.1.6 Unity内置函数
在计算光照模型的时候,开发者通常需要得到光源方向、视角方向两个基本信息。在上述例子中,开发者可使用 normalize(_WorldSpaceLightPos0.xyz)来得到光源方向(该方法通常只适用于平行光);normalize(_WorldSpaceCameraPos.xyz-i.worldPosition.xyz)来得到视角方向。但如果想要处理更复杂的光照类型,如点光源和聚光灯,该计算光源方向是错误的。从而开发者需要在代码中先判断光源类型。计算它的光源信息。
手动计算光源信息的过程十分麻烦,从而Unity提供了一些内置函数来帮助开发者计算该信息。在UnityCG.cginc中有一些有用的帮助函数,如下图所示:
图 1.43 UnityCG.cginc中有用的函数
以上函数没有保证得到的方向矢量是单位矢量,从而在使用前需要进行归一化。
计算光源方向的3个函数:WorldSpaceLightDir、UnityWorldSpaceLightDir、ObjSpaceLightDir仅可用于前向渲染。只有在前向渲染时,以上3函数里使用的内置变量_WorldSpaceLightPos0等才会