Shader学习笔记(五)Unity中的基础光照

5 篇文章 0 订阅
3 篇文章 0 订阅

声明:本篇为博主阅读冯乐乐所著《Unity Shader入门精要》所记录的笔记
原作者博客地址 https://me.csdn.net/candycat1992
如有不实请指正,如有侵权请联系本人删除。

渲染总是围绕着一个基础问题:我们如何决定一个像素的颜色?从宏观上来说,渲染包含了两大部分:
1.决定一个像素的可见性
2.决定一个像素上的光照计算
而光照模型就是用于决定在一个像素上进行怎样的光照计算。

本章着重讲光照模型的原理,因此实现的shader往往不能直接应用到项目上

一、我们是如何看到这个世界的

通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象。
首先、光线从光源(Light Source) 中被发射出来。
然后、光线和场景中的一些物体相交:一些光线被物体吸收了,而另一些光线被散射到其他方向。
最后、摄像机吸收了一些光,产生了一张图像。

1.光源

在实时渲染中,我们通常把光源当成一个没有提及的店,用I来表示他的方向。那么,我们如何测量一个光源发射出了多少逛了?也就是说,我们如何量化光呢?在光学里,我们使用辐照度(irradiance)来量化光。对于平行光来说,他的辐照度可通过计算在处置与l的单位面积上单位时间内穿过的能量来得到。在计算光照模型时,我们需要知道一个物体表面的辐照度,而物体表面往往是和l不垂直的,那么如何计算这样的表面辐照度呢?我们可以使用光源方向l和表面法线n之间的家教的余弦值来得到。

2.吸收和散射

光线有光源发射出来后,就会与一些物体相交。通常,相交的结果又两个:散射(Scattering)和吸收(absorption)。
散射只改变光线的方向,但不改变光线的密度和颜色。而吸收只会改变光线的密度和颜色,但不改变光线的方向。光线在物体表面经过散射后,有两种方向:一种将会散射到物体内部,这种现象被称为折射(refraction)或透射(transmission)。另一种将会散射到外部,这种现象被称为反射。对于不透明物体,折射进物体内部的光线还会继续与内部的颗粒进行相交,其中一些光线最后会重新发射出物体表面,而另一些则被物体吸收,那些从物体表变从新发射出的光线将具有和入社光线不同的方向分布和颜色。
为了区分这两种不同的三者方向,我们在光照模型中使用了不同的部分来计算他们:高光反射(Specluar) 部分表示物体表面是如何反射光线的, 漫反射(diffuse) 部分则表示有多少光线会被折射、吸收和散射出表面。根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,通常使用出射度(exitance) 来描述它。辐照度和出射度之间是满足现行关系的,而他们之间的比值就是材质的漫反射和高光反射属性

3.着色(shading)

着色指的是根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型(LightingModel) 不同的光照模型有不同的目的。例如一些用于描述粗糙的物体表面,一些用于描述金属表面等。

4.BRDF光照模型

我们已经了解了光线在和物体表面相交是会发生哪些现象。当已知光源位置和方向、视角方向时,我们就需要知道一个表面是和光照进行交互的。例如,当光线从某个方向照射到一个表面时,有多少光线被反射?反射的方向有哪些?而BRDF(Bidirectional Reflectance Distributuion Function) 就是来回答这都些问题的。
当给定模型表面上的一个点时,BRDF包含了对该点外观的完整的描述。在图形学中,BRDF大多使用一个数学公式来表示没并且提供了一些参数来调整材质属性。通俗来讲,当给定入射光线的方向和辐照度后,BRDF可以给出某个初设方向上的光照能量分布。

计算机图形学第一定律:如果他看起来是对的,那么他就是对的

二、标准光照模型

虽然光照模型有很多种类,但在早期的游戏引擎中往往只只是用一个光照模型,这个模型被称为标准光照模型。实际上,在BRDF理论被提出之前,标准光照模型就已经被广泛使用了。

1.基本理念

1975年,著名学者裴祥风提出了标准光照模型背后的基本理念。标准光照模型只关心直接光照(direct light)也就是那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。
他的基本方法是,把进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算他们的贡献度,这四个部分分别是:

(1).自发光(emissive):Cmissive

这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。需要注意的时,如果没有使用全局光照(globalillumination)技术,这些自发光的表面并不会真的着凉周围的物体,而是他包本身看起来更亮了一点。

(2).高光反射(specular):Cspecular

这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。

(3).漫反射(fiffuse):Cdiffuse

这个部分用于描述当光线从光源照射到模型表面时,该表面回想每个方向散射多少辐射量。

(4).环境光(ambient):Cambient

用于描述其他所有的间接光照。

2.环境光

虽然标准光照模型的重点在于描述直接光照,但在真是的世界中,物体也可以被间接光照(indirect light)所照亮。间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机,也就是说没在光线进入摄像机之前,经过了不止一次的物体反射。例如,在红地毯上放置一个浅色的灰色沙发,那么沙发底部也会有红色,这些红色是由红地毯反射了一部分光线,在反射到沙发上的。
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。环境光的计算非常简单,他通常是一个全局变量。即场景中的所有物体都是用这个环境光。下面的等式是计算环境光的部分
Cambient=gambient

3.自发光

光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的共享度。他的计算也很简单,就是直接使用了该材质的自发光的颜色
Cemissive=Memissive
通常在实时渲染中,自发光的表面往往不会着凉周围的表面,也就是说,这个物体并不会被当成一个光源。

4.漫反射

漫反射光照是用于对那些被物体表面随机散射到哥哥方向的辐射度进行建模的。在漫反射中,视角的位置是不重要的,因为反射完全是随机的,因此可以认为在任何反射方向的分量是一样的,但是,入射光线的角度很重要。
漫反射的光照符合兰伯特定律(Lambert`s law) :反射光线的强度与表面法线和光源之间的夹角的余弦值成正比,因此,漫反射部分的计算
Cdiffuse=(ClightMdiffuse)max(0,nI)
其中,n是表面法线、I是指向光源的单位矢量,Mdiffuse是材质的漫反射颜色,Clight是光源颜色。需要注意的是,我们需要放置法线和光源方向点乘的结果为负值,为此我们使用取最大值的函数来将其截取到0,这可以防止物体被从后面来的光源照亮。

5.高光反射

这里的高光反射是一种经验模型,也就是说,他并不完全符合真实世界中的高光反射现象,它可用于计算那些沿着镜面反射方向被反射的光线,让物体看起来是有光泽的。
计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。在本届中,我们假设这些矢量都是单位矢量。
反射方向矢量 r = 2(N*I)N - I (打不出来)
这样,我们就可以利用 Phong模型 来计算高光反射部分
Cspscular=(ClightMspecular)max(0,vr)
其中 Mglass是材质的光泽度(gloss) ,也被称为 反光度(shininess) 。它用于控制高光区域的“亮点”有多宽,Mgloss越大、亮点越小。Mspscular是材质的高光反射颜色,它用于控制该材质对于高光反射的强度和颜色。Clight是光源的颜色和强度。同样,这里也需要防止 矢量v、r之间乘积为负数。

和上面的Phong相比,Blinn提出了一个简单的修改方法来得到类似的效果。他的基本思想是,避免计算反射方向r。为此Blinn模型引入了一个新的矢量h,他是通过v和I取平均后再归一化得到的。

然后,使用n和h之间的夹角进行计算,而非v和r之间的夹角。

Cspecular=(ClightMspecular)max(0,nh)
在硬件实现时,如果摄像机和光源距离模型足够远的话,blinn模型会快于Phong模型,这是因为此时可以认为V和I都是定值,因此h僵尸一个常量,但是,当v和I不是定值时,Phong模型可能反而更快。需要注意的是,两种光照模型都是经验模型,也就是说,我们不应该认为Blinn模型时对“正确的”Phong模型的近似,实际上,在一些情况下,Blinn模型更符合实验结果

6.逐顶点还是逐像素

上面,我们给出列基本光照模型使用的数学公式,那么我们在哪里计算这些光照模型呢?通常来讲,我们有两种选择:在片元着色器中计算,被我们称为逐像素光照(per-pixel lighting) 在顶点着色器中计算,被我们称为 逐顶点光照(per-vertex lighting)
在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理采样得到的),然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术被成武Phong着色(Phong shading)也被称为Phong插值或法线插值着色技术。不同于前面的Phong光照模型。

与之相对的是逐顶点光照,也被称为高洛德着色(Gouraud shading)。逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图源内部进行线性插值,最后输出成像素颜色。由于顶点数目往往小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是由于据定点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性计算(如计算高光反射)时,逐顶点光照会出问题。

7.总结

虽然标注南光照模型仅仅是一个经验模型,也就是说,它并不完全符合真是解决中的光照现象,但由于他的易用性、计算速度和得到的效果都比较好,因此仍然被广泛使用。而也正是由于他的广泛使用性,这种标准光照模型有很多不同的叫法。例如,一些资料中称它为Phong模型。而后由于Blinn的方法简化了计算而且在某些情况下计算更快,我们把这种模型称为 Blinn-Phong 光照模型。
但这种模型有很多局限性。首先、有很多重要的物理现象无法用Blinn-Phong模型表现出来,例如菲涅尔反射(Fresnel reflection) 其次、Blinn-Phong模型时各项同性(isotrople) 的,也就是说,当我们固定视角和光源旋转方向旋转这个表面时,反射不会发生任何变化,但有一些表面时具有各项异性(anisotropic) 反射性质的,例如拉丝金属、毛发等。

三、Unity中环境光和自发光

在标准光照模型中,环境光和自发光的计算使最简单的。
在Unity中,场景中的环境光可以在Window->Lighting -> Ambient Source/Ambient Color/Ambient Intensity中控制。在shader中我们只需要通过Unity内置变量UNITY_LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息。
而大多数物体是没有自发光特性的,因此在本书绝大部分的shader中都没有计算自发光部分。如果要计算自发光也非常简单,我们只需要在偏远着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。

四、在UnityShader中实现漫反射光照模型

漫反射部分的计算公式
Cdiffuse = (ClightMdiffuse)max(0,nI)
Clight:入射光线颜色和强度
Mdiffuse:材质的漫反射系数
N:表面法线
I:光源方向
为了防止点积结果为负值,使用了max函数,这里使用CG中另一个函数saturate(x)
参数 x:为用于操作的标量或矢量,可以使float、float2、float3等类型。
描述:把x截取在[0,1]范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。

1.实践:逐顶点光照

准备流程:工程、物体、material、shader、胶囊体

开始:
(1)命名: Shader “Unity Shaders Book/Chapter 6/Diffuse Vertex-Level”
(2)为了得到并控制材质的漫反射颜色,我们首先在shaderdeproperties语义块中声明了一个Color类型的数学,并把它的初始值设为白色:

Properties{
	_Diffuse ( "Diffuse",Color )= (1,1,1,1)
} 

(3)然后,在SubShader语义块中定义了一个Pass语义块。这是因为顶点/片元着色器的代码需要卸载Pass语义块,而非SubShader语义块中。而且,我们在Pass的第一行指明了该Pass的光照模式:

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

LightMode标签是Pass标签的一种,它用于定义该Pass在Unity的光照流水线中的角色。定义正确的LightMode后才能得到一些Unity内置光照变量。
(4)然后,我们使用CGPROGRAM和ENDCG来包围CG代码片,已定义最重要的顶点着色器和片元着色器代码。首先我们使用 #pragma 告诉Unity我们对两种着色器的命名

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

(5)为了使用Unity内置变量,如后面要提的_LightColor0,还需要包含进Unity的内置文件Light.cginc

#include "Lighting.cginc"

(6)为了在Shader中使用Propeities语义块中声明的数学,我们需要定义一个和该属性类型相匹配的变量:

fixed4 _Diffuse;

通过这样的方式,我们就可以得到漫反射公式中需要的参数之一------材质的漫反射属性。由于颜色的范围在0到1之间,因此我们可以使用fixed精度的变量来存储它。

(7)然后,我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的输入结构体):

struct a2v{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
};
struct v2f{
	float4 pos : SV_POSITION;
	fixed3 color : COLOR;
};

为了访问顶点的法线,我们需要在a2v中定义一个normal变量,并通过使用NORMAL语义来告诉Unity要把模型顶点的法线信息存储到normal变量中。为了把在顶点着色器中计算得到的光照颜色传递给片元着色器,我们需要在v2f中定义一个color变量,并且不是必须使用COLOR语义,一些资料中会使用TEXCOORD0语义。

(8)接下来是关键的顶点着色器。由于本小姐关注如何实现一个逐顶点的漫反射社光照,因此漫反射部分的计算豆浆在顶点着色器中进行

v2f vert(a2v v){
	v2f o;
	//Transform the vertex from object space to projection space
	//
	o.pos= mul(UNITY_MATRIX_MVP, v.vertex);

	//Get ambient term 
	fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

	//Transform the normal fram object space to world space
	fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
	
	//Get the light direction in world space
	fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
	
	//Comput diffuse  term
	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
	
	o.color = ambient + diffuse ;
	return o;
}

在第一行,定义了返回值o。
强调:顶点着色器的基本任务是吧顶点位置从模型空间转换到裁剪空间中,因此我们需要使用Unity内置的模型世界投影矩阵 UNITY_MATRIX_MVP来完成这样的转换,杰西莱我们通过Unity的内置变量 UNITY_LIGHTMODEL_AMBIENT得到了环境光部分。

然后就是真正计算漫反射光照的部分。为了计算漫反射光照我们需要知道4个参数。在前面的步骤中,我们已经知道了漫反射颜色_DIffuse 以及顶点法线 v.normal 。 我们还需要知道光源的颜色和强度信息以及光源方向。Unity提供给我吗一个内置变量 _LightColor0来访问该Pass处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightModer标签)而光源方向可以由 _WorldSpaceLightPos0来得到。需要注意的是,这里对光源方向的计算并不具有通用性。在本节中,我们假设的场景中只有一个光源且该光源的类型是平行光。但如果场景中有多个光源并且类型可能是点光源等其他类型,直接使用_WorldSpaceLightPos0就不能得到正确的结果。

在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一坐标空间下,它们的点积才有意义。在这里,我们选择了世界坐标空间。而由a2v得到的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。可以通过使用顶点的变换觉镇的逆专职矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需截取_World2Object的前三行前三列即可。

在得到了世界空间中的法线和光源方向后,我们需要对他进行归一化操作。在得到他们的点积结果后我们需要防止这个结果为负值,,为此我们使用了saturate函数。saturate函数时CG提供的一种函数,它的作用是可以把参数截取到[0,1]的范围内,最后,在与光源的颜色和强度以及漫反射颜色相乘即可得到最终的漫反射光照部分。

最后,对环境光和漫反射光部分相加,得到最终的光照结果。

(9)由于所有的计算在顶点着色器中都已经完成了,因此片元着色器的代码很简单,我们只需要把点顶点颜色输出即可
fixed4 frag(v2f i ) : SV_Target {
return fixed4(i.color,1.0);
}
(10)最后,我们需要把这个UnityShader的回调shader设置为内置的Diffuse:

Fallback "Diffuse"

至此,我们已经详细解释了逐顶点的漫反射光照的实现。对于细分程度高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现一些视觉问题,例如一些背光面与向光面交界处的锯齿,我们可以使用逐像素的漫反射光照。

2.实践:逐像素光照

我们只需要对shader进行一些更改就可以实现逐像素的漫反射效果。
做一些与上一节一样的准备工作。

代码复制上一节的拿来修改修改即可
(1)修改顶点着色器的输出结构体v2f

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

(2)顶点着色器不需要计算光照模型,只需要把世界空间下的法线传递给片元着色器即可:

v2f vert(a2v v){
	v2f o;
	//Transform the vertex from object space to projection space
	o.pos= mul(UNITY_MATRIX_MVP,v.vertex);

	//Transform the normal fram object space to world space
	o.worldNormal = mul(v.normal , (float3x3)_World2Object);

	return o;
}

(3)片元着色器需要计算漫反射的光照模型:

fixed4 frag(v2f i) : SV_Target{
	//Get ambient term
	fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
	
	//Get the normal in world space
	fixed3 worldNormal = normalize(i.worldNormal);

	//Get the light direction in world space
	fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xzy);

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

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

逐像素光照可以得到更加平滑的光照效果,但是即便使用了逐像素漫反射光照,有一个问题仍然存在,就是在光照无法到达的区域,模型的外观通常是全黑的,没有明暗变化,实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样仍然无法解决背光面明暗一样的缺点。为此有一种改善技术被提出来,那就是半兰伯特(Half Lambert)光照模型

3.半兰伯特模型

第一小节中我们使用的漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定律——在平面某点漫反射光的光强与该反射点的方向量的入射光角度的余弦值成正比。为了改善上一节最后的问题,Value提出一种技术,由于该技术是在原兰博欧特光照模型的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型

广义的半兰伯特光照模型的公式如下:
Cdiffuse=(ClightMdiffuse)(a(N * I)+ b) ——(a-阿尔法、b-贝塔)
可以看出,与原兰伯特光照模型相比,半兰伯特光照模型没有使用max操作来防止N和I的点积为负值,而是对其结果进行了一个a倍的缩放再加上一个b大小的偏移。绝大多数情况下,a和b的值均为0.5
即 :
Cdiffuse= (Clight
Mdiffuse)(0.5*(N * I)+0.5)
通过这样的方式,我们可以把N*I的结果范围从[-1,1]映射到[0,1]范围内。也就是说,对于模型的背光面,在原兰伯特光照模型中点积的结果将映射到同一个值,即0值处;而半兰伯特模型中,背光面也有明暗变化,不同的点积结果会映射到不同的值上。

需要注意的是,半兰伯特时没有任何物理依据的,它仅仅是一个视觉加强技术。

对上一节的代码复制一份,将frag修改如下:

fixed4 frag(v2f i): SV_Target{
	...
	//Compute diffuse term 
	fixed halfLambert = dot(worldNormal,worldLightDir)*0.5+.05;
	fixed3 diffuse =_LightColor0.rgb * _Diffuse.rgb * halfLambert;
	
	fixed3 color= ambient + diffuse;

	return fixed4(color, 1.0);
}

五、在Unityshader中实现高光反射光照模型

在前面我们给出了基本光照模型中高光反射部分的计算公式:
Cspecular=(Clight*Mspecular)max(0,v * r)Mglass

从公式中可以看出,要计算高光反射需要知道4个参数:
Clight:入射光线的颜色和强度
Mspecular:材质的高光反射系数
v:视角方向
r:反射方向

其中,反射方向r可以由表面法线N和光源方向I计算而得:
r=2(N * I)N - I
CG提供了计算反射方向的函数reflect

函数:reflect(i,n)
参数:i,入射方向。n,法线方向。可以是float,float2,float3
描述:当给定入射方向和法线方向时reflect可以返回反射方向。

1.实践:逐顶点光照

老样子一套基础准备工作
(1)起名

Shader "Unity Shaders Book/Chapter 6/Specular Vertex-Level"{

(2)为了在材质面板中能够更方便地控制高光反射属性,我们在Shader的Properties语义块中声明了三个属性:

Properties{
	_Diffuse("Diffuse",Color) = (1,1,1,1)
	_Specular("Specular",Color ) = (1,1,1,1)
	_Gloss("Gloss",Range(8.0,256))=20
}

其中新添加的_Specular用于控制材质的高光反射颜色,而_Gloss用于控制高光区域的大小。
(3)然后,我们在SubShader语义块中定义了一个Pass语义块,这是因为顶点/片元着色器的代码需要写在Pass语义块,而非SubShader语义块中。而且,我们在Pass的第一行指明了该Pass的光照模式:

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

LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。

(4)然后,老样子 CGPROGRAM ENDCG

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

(5)为了使用Unity内置变量,如后面要提的_LightColor0,还需要包含进Unity的内置文件Light.cginc

#include "Lighting.cginc"

(6)为了在Shader中使用Propeities语义块中声明的数学,我们需要定义一个和该属性类型相匹配的变量:

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

(7)然后我们定义了顶点着色器的输入和输出结构体(之前说过,顶点着色器的输出结构体也是片元着色器的输入结构体)

struct a2v{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
};
struct v2f{
	float4 pos : SV_POSITION;
	fixed3 color : COLOR;
};

(8)在顶点着色器中,我们计算了包含高光反射的光照模型

v2f vert(a2v v){
	v2f o;
	//Transform the vertex from object space to projection space
	//基本任务
	o.pos= mul(UNITY_MATRIX_MVP, v.vertex);

	//Get ambient term 
	fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

	//Transform the normal fram object space to world space
	fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
	
	//Get the light direction in world space
	fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
	
	//Comput diffuse  term
	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
	
	//从这里开始出现新变化
	//Get the reflect direction in world space 
	fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
	//Get the view direction in world space 
	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World , v.vertex).xyz)
	
	//compute specular term
	fixed3 specular = _LightColor0.rgb *_Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss);

	o.color = ambient +diffuse + specular;

	return 0;
}

其中,漫反射部分和前面完全一致。对于高光反射部分,我们首先计算了入射光线方向关于表面法线的反射方向reflectDir。由于CG的reflect函数的入射方向要求是由光源指向交点处的,因此我们需要对worldLightDir取反后再传给reflect函数。然后,我们通过WorldSpaceCameraPos得到了世界空间中的摄像机位置,再把顶点位置从模型空间变换到世界空间下,再通过和_WorldSpaceCameraPos相减得到实际空间下的视角方向。
由此,将已得到的四个参数代入公式即可得到高光反射的光照部分。最后,再和环境光、漫反射光相加存储到最后的颜色中。

(9)片元着色器的代码很简单,我们只需要把点顶点颜色输出即可

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

(10)最后,我们需要把Unityshader的回调shader设置为内置的specular

Fallback "Specular"

2.实践:逐像素光照

我们可以使用逐像素光照来得到更加平滑的高光效果
(1)修改顶点着色器的输出结构体v2f

struct v2f{
	float4 pos : SV_POSITION;
	float3 worldNormal : TEXCOORD0;
	float3 worldPos : TEXCOORD1;
}

(2)顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器即可:

v2f vert(a2v v){
	v2f o;
	o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
	o.worldNormal= mul(v.normal,(float3x3)_World2Object);
	o.worldPos=mul(_Object2World,v.vertex).xyz;
}

(3)片元着色器需要计算关键的光照模型:

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

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

	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 );
	
}

可以看出,按逐像素的方式处理光照可以得到更加拼花的高光效果,至此我们就实现了一个完整的Phong光照模型。

3.Blinn-Phong光照模型

之前提到过另一种高光反射的实现方法——Blinn光照模型
没有使用反射方向,而是引入了一个新的矢量h(由v和I相加后再归一化得到的)
Cspecular=(Clight*Mspecular)max(0,N * H)Mgloss
把上一节中片元着色器中对高光反射部分的计算代码进行更改:

fixed4 frag(v2f i): SV_Target{
	//.....
	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 + diffuse + specular , 1.0);
}

Blinn-Phong模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝大对数情况我们都会选择Blinn-Phong光照模型。需要再次提醒的是,这两种光照模型都是经验模型。

六、使用Unity的内置函数

在计算光照模型的时候,我们往往需要得到光源方向、视角方向这两个基本信息。在上面的例子中,我们都是自行在代码里计算的(如normalize(_WorldSpaceLightPos0.xyz)来得到光源方向,实际上只适用于平行光; 使用normalize(_WorldSpaceCameraPos.xyz - i.worldPosition.xyz)来得到视角方向),但如果需要处理更复杂的光照模型,如点光源和聚光灯,我们计算的光源方向就是错误的。这需要我们在代码中先判断光源类型,在计算它的光源信息。
手动计算这些信息比较麻烦(但不意味着不需要明白原理)。幸运的是Unity提供了一些内置函数来帮助我们计算这些信息。

函数名描述
float3 WorldSpaceViewDir(float4 v)输入一个模型空间中顶点位置,返回世界空间中该点到摄像机的观察方向,内部实现使用了UnityWorldSpaceViewDir函数
float3 UnityWorldSpqceViewDir(float4 v)输入一个世界空间的顶点位置,返回世界空间在从该点到摄像机的方向。
float3 ObjSpaceViewDir(float4 v)输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向
float3 WorldSpaceLightDir(float4 v)仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从 该点到光源的光照方向,内部使用了UnityWorldPaaceLightDir函数,没有被归一化。
float3 ObjSpaceLightDir(float4 v)仅可用于前向渲染中。输入一个世界空间中的顶点位置,返回实际空间中从该点到光源的光照方向,没有被归一化。
float3 ObjSpaceLightDir(float4 v)仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向,没有被归一化。
float3 UnityObjectToWorldNormal(float3 norm)把法线方向从模型空间转换到世界空间中
float3 UnityObjectToWorldDir(in float3 dir)把方向矢量从模型空间变换到世界空间中
flaot3 UnityWorldToObjectDir(float3 dir)把方向矢量从世界坐标变换到模型空间中

注意:类似UnityXX的几个函数时Unity5中添加的内置函数,在后面的具体还不太清楚,等学完这本书以后如果想起来再补充。
这些帮助函数使我们不需要跟各个变化矩阵、内置变量打交道,也不需要考虑各种不同情况(例如使用了哪些光源)而仅仅调用一个函数就可以得到信息,上面9个函数中,有5个我们已经掌握(????还是用了解吧)其内部实现,例如 WorldSpaceViewDir函数实现如下

inline float3 UnityWorldSpaceViewDir(in float3 worldPos)
{
	return _WorldSpaceCameraPos.xyz - worldPos;
}

可以看出,这与之前计算视角方向的方法一致。需要注意的是 ,这些函数都没有保证得到的方向矢量是单位矢量,因此在使用前需要进行归一化。
而计算光源方向的3个函数:WorldSpaceLightDir\UnityWorldSpaceLightDir\ObjSpaceLightDir稍微复杂一些,这是因为,Untiy帮助我们处理了不同种类光源的情况。需要注意的是 这个三个函数仅可用于前向渲染。主卧室因为只有在前向渲染时,这三个函数里使用的内置变量_WorldSpaceLightPos0等才会被正确赋值。

下面介绍使用内置函数改写UnityShader

复制一份上一节的Blinn-Phong光照模型的shader

(1)在顶点着色器中,我们使用内置的UnityObjectToWorldNormal函数计算世界空间下的法线方向

v2f vert(a2v v){
	v2f o 
	......
	//use the build-in function to compute the normal in world space
	o.worldNormal = UnityObjectToWorldNormal(v.normal);
	......
	return o;
}

(2)在片元着色器中,我们使用内置的UnityWorldSpaceLightDir函数和UnityWorldSpaceViewDir函数来分别计算实际空间的光照和视角方向:

fixed frag(v2f i):SV_Target{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
	...
	fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
}

注意归一化。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值