第六章——Unity中的基础光照

原理在games101中讲的很清楚,就不赘述了,可以看参考里的games101。
主要是光照模型在Unity中的实现。

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

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

1、首先,光线从光源(light source)中被发射出来。

2、然后,光线和场景中的一些物体相交:一些光线被物体吸收了,而另一些光线被散射到其

他方向。

3、最后,摄像机吸收了一些光,产生了一张图像。

下面,我们将对每个部分进行更加详细的解释。

1、光源

光是由光源发射出来的。在实时渲染中,我们通常把光源当成一个没有体积的点,用l来表示它的方向。

在光学里,我们使用辐照度(irradiance)来量化光。

对于平行光来说,它的辐照度可通过计算在垂直于1的单位面积上单位时间内穿过的能量来得到。

在计算光照模型时,我们需要知道一个物体表面的辐照度,而物体表面往往是和l不垂直的,那么如何计算这样的表面的辐照度呢?我们可以使用光源方向Ⅰ和表面法线n之间的夹角的余弦值来得到。需要注意的是,这里默认方向矢量的模都为1。

图6.1显示了使用余弦值来计算的原因。

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

2、吸收和散射

光线由光源发射出来后,就会与一些物体相交。通常,相交的结果有两个:散射(scattering)和吸收(absorption)。

散射只改变光线的方向,但不改变光线的密度和颜色。而吸收只改变光线的密度和颜色,但不改变光线的方向。光线在物体表面经过散射后,有两种方向:一种将会散射到物体内部,这种现象被称为折射(refraction)或透射(transmission);另一种将会散射到外部,这种现象被称为反射(reflection)。

对于不透明物体,折射进入物体内部的光线还会继续与内部的颗粒进行相交,其中一些光线最后会重新发射出物体表面,而另一些则被物体吸收。那些从物体表面重新发射出的光线将具有和入射光线不同的方向分布和颜色。图6.2给出了这样的一个例子。

为了区分这两种不同的散射方向,我们在光照模型中使用了不同的部分来计算它们:高光反射(specular)部分表示物体表面是如何反射光线的,而漫反射(diffuse)部分则表示有多少光线会被折射、吸收和散射出表面。根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们通常使用出射度(exitance)来描述它。辐照度和出射度之间是满足线性关系的,而它们之间的比值就是材质的漫反射和高光反射属性。

在本章中,我们假设漫反射部分是没有方向性的,也就是说,光线在所有方向上是平均分布的。同时,我们也只考虑某一个特定方向上的高光反射。

为了便于理解他们的关系,下面是一张关系图:

3、着色

着色(shading)指的是,根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型

(Lighting Model)。

不同的光照模型有不同的目的。例如,一些用于描述粗糙的物体表面,一些用于描述金属表面等。

4、BRDF光照模型

我们已经了解了光线在和物体表面相交时会发生哪些现象。当已知光源位置和方向、视角方向时,我们就需要知道一个表面是和光照进行交互的。例如,当光线从某个方向照射到一个表面时,有多少光线被反射?反射的方向有哪些?而 BRDF(Bidirectional Reflectance DistributionFunction)就是用来回答这些问题的。当给定模型表面上的一个点时,BRDF包含了对该点外观的完整的描述。在图形学中,BRDF 大多使用一个数学公式来表示,并且提供了一些参数来调整材质属性。通俗来讲,当给定入射光线的方向和辐照度后,BRDF可以给出在某个出射方向上的光照能量分布。本章涉及的BRDF 都是对真实场景进行理想化和简化后的模型,也就是说,它们并不能真实地反映物体和光线之间的交互,这些光照模型被称为是经验模型。尽管如此,这些经验模型仍然在实时渲染领域被应用了多年。读者可以从邓恩的著作《3D数学基础:图形与游戏开发》(英文名:《3D Math Primer For Graphics And Game Development》)中提到的一句名言来体会这其中的原因。

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

然而,有时我们希望可以更加真实地模拟光和物体的交互,这就出现了基于物理的BRDF模型,我们会在第18章基于物理的渲染中看到这些更加复杂的光照模型。

二、标准光照模型(Phong 模型)

Phong模型只考虑摄像机的直接光照。
Phong模型把进入到摄像机中的光线分为四个部分,每个部分用一种方法来计算它的贡献度:

1、环境光

虽然标准光照模型的重点在于描述直接光照,但在真实的世界中,物体也可以被间接光照(indirect light)所照亮。间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机,也就是说,在光线进入摄像机之前,经过了不止一次的物体反射。

例如,在红地毯上放置一个浅灰色的沙发,那么沙发底部也会有红色,这些红色是由红地毯反射了一部分光线,再反弹到沙发上的。

在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。下面的等式给出了计算环境光的部分:

Phong模型认为环境光是一个常量。


\large c_{ambient}=g_{ambient}

2、自发光

光线也可以直接由光源发射进入摄像机,而不需要经过任何物体的反射。标准光照模型使用自发光来计算这个部分的贡献度。它的计算也很简单,就是直接使用了该材质的自发光颜色:

Phong模型中,直接使用该材质的自发光颜色:


\large c_{emissive}=m_{emissive}

通常在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当成一个光源。Unity 5引入的全新的全局光照系统则可以模拟这类自发光物体对周围物体的影响,我们会在第18章中看到。

3、漫反射

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

漫反射光照符合兰伯特定律(Lambert's law)反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。

\large c_{diffuse}=(c_{light}\cdot m_{diffuse})max(0,\hat{n}\cdot \hat{l})


兰伯特漫反射定律

观察这个式子,其中没有出现\hat{v},也证实了漫反射与观察方向无关

4、高光反射

这高光反射是一种经验模型,它并不完全符合真实世界中的高光反射现象。它可用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,例如金属材质。

计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等。在本节中,我们假设这些矢量都是单位矢量。图6.3给出了这些方向矢量。

在这四个矢量中,我们实际上只需要知道其中三个矢量即可,第四个矢量——反射方向可以通过其他信息计算得到:

\large \hat{r}=2(\hat{n}\cdot \hat{l})\hat{n}-\hat{l}

其中\hat{r}表示入射光l镜面反射的方向,由平行四边形法则可以的得到

5、逐像素还是逐顶点

上面,我们给出了基本光照模型使用的数学公式,那么我们在哪里计算这些光照模型呢?通常来讲,我们有两种选择:在片元着色器中计算,也被称为逐像素光照(per-pixel lighting);在顶点着色器中计算,也被称为逐顶点光照(per-vertex lighting)

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

与之相对的是逐顶点光照,也被称为高洛德着色(Gouraud shading)。在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射时)时,逐顶点光照就会出问题。在后面的章节中,我们将会看到这种情况。而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。

6、总结

虽然标准光照模型仅仅是一个经验模型,也就是说,它并不完全符合真实世界中的光照现象。但由于它的易用性、计算速度和得到的效果都比较好,因此仍然被广泛使用。

而也是由于它的广泛使用性,这种标准光照模型有很多不同的叫法。例如,一些资料中称它为Phong光照模型,因为裴祥风(Bui Tuong Phong)首先提出了使用漫反射和高光反射的和来对反射光照进行建模的基本思想,并且提出了基于经验的计算高光反射的方法(用于计算漫反射光照的兰伯特模型在那时已经被提出了)。而后,由于Blinn的方法简化了计算而且在某些情况下计算更快,我们把这种模型称为 Blinn-Phong 光照模型

但这种模型有很多局限性。首先,有很多重要的物理现象无法用 Blinn-Phong模型表现出来,例如菲涅耳反射(Fresnel reflection)。其次,Blinn-Phong模型是各项同性(isotropic)的,也就是说,当我们固定视角和光源方向旋转这个表面时,反射不会发生任何改变。但有些表面是具有各向异性(anisotropic)反射性质的,例如拉丝金属、毛发等。在第18章中,我们将学习基于物理的光照模型,这些光照模型更加复杂,同时也可以更加真实地反映光和物体的交互。

三、Unity中的环境光和自发光

在标准光照模型中,环境光和自发光的计算是最简单的。

在Unity 中,场景中的环境光可以在 Window ->Lighting -> Ambient Source/Ambient ColorlAmbientIntensity中控制,如图6.5所示。在 Shader 中,我们只需要通过Unity的内置变量UNITY LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息。

而大多数物体是没有自发光特性的,因此在本书绝大部分的 Shader中都没有计算自发光部分。如果要计算自发光也非常简单,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色上即可。

四、Unity Shader中实现漫反射光照模型

1、逐顶点光照

源代码:

Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-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 color : COLOR;
			};
			
			v2f vert(a2v v) {
				v2f o;
				// Transform the vertex from object space to projection space
				o.pos = UnityObjectToClipPos(v.vertex);
				
				// Get ambient term
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				// Transform the normal from object space to world space
				fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
				// Get the light direction in world space
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				// Compute diffuse term
				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
		}
	}
	FallBack "Diffuse"
}

我的:

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-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 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 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//完成漫反射部分
				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
        }
    }
			FallBack "Diffuse"
}

注意在计算法线和光源方向之间的点积时,需要把它们转换到统一坐标系下。例如在世界坐标下。具体可以看参考链接中的光线追踪:生成相机光线。
结果如下:

2、逐像素光照

代码与逐顶点非常相似,只需要进行如下修改:

1、修改顶点着色器的输出结构体v2f:

struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;// 用TEXCOORD0语义来存储自定义的变量 世界坐标的法线向量

			};

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

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

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

// 把顶点着色器传递的颜色插值后输出到frame_buffer
			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);
			}

于是我们得到了最终的逐像素实现的漫反射模型:

Shader "Unity Shaders Book/Chpater 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;
				float3 worldNormal : TEXCOORD0;// 用TEXCOORD0语义来存储自定义的变量 世界坐标的法线向量

			};

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

			// 把顶点着色器传递的颜色插值后输出到frame_buffer
			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);
			}

			ENDCG

		}
	}
	Fallback "Diffuse"
}

结果如下:
 


逐像素光照

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

3、半兰伯特模型(Half Lambert)

在逐顶点和逐像素光照中,我们可以发现背光区域是全黑的,就像一个平面一样,失去了模型细节表现。
而半兰伯特模型解决了这个问题,半兰伯特模型认为:


\large c_{diffuse}=(c_{light}\cdot m_{diffuse})(\alpha(\hat{n}\cdot \hat{l})+\beta)

\large c_{diffuse}= (c_{light}\cdot m_{diffuse})(0.5(\hat{n}\cdot\hat{l})+0.5)

通过这样的方式,我们可以把合·Ⅰ的结果范围从[-1,1]映射到[0,1]范围内。也就是说,对于模型的背光面,在原兰伯特光照模型中点积结果将映射到同一个值,即0值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。


半兰伯特模型

半兰伯特模型只需要在逐像素光照的代码基础上,修改一下计算漫反射光照的部分即可:

// 把顶点着色器传递的颜色插值后输出到frame_buffer
			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);
			}

Shader "Unity Shaders Book/Chpater 6/HalfLambert" {
	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;// 用TEXCOORD0语义来存储自定义的变量 世界坐标的法线向量

			};

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

			// 把顶点着色器传递的颜色插值后输出到frame_buffer
			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 Shader实现高光反射模型

reflect(i,n)函数可以用来计算反射方向,i为入射方向,n为法线方向

1、逐顶点光照

与逐顶点光照漫反射模型相似,只有在顶点着色器的光照模型部分,除了之前的漫反射部份外,还新增了计算反射方向reflectDir,将顶点位置从模型空间变换到世界空间,再减去世界空间的摄像机位置,最后将四个参数带入公式:

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

				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;

				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函数:输入入射方向(向内)和法线方向(向外),返回镜面反射方向(向外)
				// 对于一个Shading Point,所有方向向量都向外
				// 所以这里要取入射方向的反方向
				fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
				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;
			}

最终代码:

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 {
			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;
				fixed3 color : COLOR;
			};

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

				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;

				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函数:输入入射方向(向内)和法线方向(向外),返回镜面反射方向(向外)
				// 对于一个Shading Point,所有方向向量都向外
				// 所以这里要取入射方向的反方向
				fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
				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

		}
	}
	Fallback "Specular"
}

注意一下讨论光照的时候,对于一个Shading Point,所有方向都是向外的。

效果如下:

使用逐顶点的方法得到的高光效果有比较大的问题,我们可以在图6.10中看出高光部分明显不平滑。这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。因此,我们就需要使用逐像素的方法来计算高光反射。

2、逐像素光照

逐像素光照与逐顶点光照类似,只需要做如下修改:

1、修改顶点着色器的输出结构体v2f:

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

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

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

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

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

最终代码如下:

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;
				fixed3 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 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"
}

效果如下:


逐像素光照的高光反射模型

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

3、Blinn-Phong光照模型

对于Blinn-Phong模型的实现,我们只需要再逐像素实现高光反射模型的的基础上做如下修改:

1、修改片元着色器中对高光反射部分的计算代码:

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);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				return fixed4(ambient + diffuse + specular, 1.0);
			}

最终代码如下:

Shader "Unity Shaders Book/Chapter 6/BlinnPhong" {
	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;
				fixed3 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);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				return fixed4(ambient + diffuse + specular, 1.0);
			}

			ENDCG

		}
	}
	Fallback "Specular"
}

效果如下:

可以看出,Blinn-Phong光照模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝大多数情况我们都会选择 Blinn-Phong 光照模型。需要再次提醒的是,这两种光照模型都是经验模型,也就是说,我们不应该认为 Blinn-Phong模型是对“正确的”Phong模型的近似。实际上,在一些情况下,Blinn-Phong模型更符合实验结果。

六、UnityCG.cginc中常用帮助函数

下面给出计算光照模型时,我们常常用的一些内置函数。
UnityCG.cginc中常用帮助函数


注意表中的方向向量不保证是单位矢量,使用前需要进行归一化(normalize)。

下面用它们改写BlinnPhong光照模型:

Chapter6-BlinnPhongUseBuildInFunction中的代码几乎和 Chapter6-BlinnPhong 中的完全一样,只是计算时使用了Unity的内置函数。修改部分的代码如下:

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

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

				// o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

				return o;
			}

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

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

				fixed3 worldNormal = normalize(i.worldNormal);
				// fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

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

				// fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				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);
			}

需要注意的是,由内置函数得到的方向是没有归一化的,因此我们需要使用normalize函数来对结果进行归一化,再进行光照模型的计算。

最终代码:

Shader "Unity Shaders Book/Chapter 6/BlinnPhong" {
	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;
				fixed3 worldPos : TEXCOORD1;
			};

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

				// o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
				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(_WorldSpaceLightPos0.xyz);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

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

				// fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				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);
			}

			ENDCG

		}
	}
	Fallback "Specular"
}

参考

《Unity Shader 入门精要》第六章

光线追踪:生成相机光线

games101 L1 笔记

语雀第六章——Unity中的基础光照

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值