Unity Shader-Phong光照模型与Specular

简介


学完了兰伯特光照模型,再来学习一个更加高级一点的光照模型-Phong光照模型。光除了漫反射,还有镜面反射。一些金属类型的材质,往往表现出一种高光效果,用兰伯特模型是模拟不出来的,所以就有了Phong模型。Phong模型主要有三部分构成,第一部分是上一篇中介绍了的Diffuse,也就是漫反射,第二部分是环境光,在非全局光照的情况下,我们一般是通过一个环境光来模拟物体的间接照明,这个值在shader中可以通过一个宏来直接获取,而第三部分Specular,也就是高光部分的计算,是一种模拟镜面反射的效果,也是本篇文章重点介绍的内容。


在现实世界中,粗糙的物体一般会是漫反射,而光滑的物体呈现得较多的就是镜面反射,最明显的现象就是光线照射的反射方向有一个亮斑。再来复习一下镜面反射的概念:当平行入射的光线射到这个反射面时,仍会平行地向一个方向反射出来,这种反射就属于镜面反射,其反射波的方向与反射平面的法线夹角(反射角),与入射波方向与该反射平面法线的夹角(入射角)相等,且入射波、反射波,及平面法线同处于一个平面内。反射光的亮度不仅与光线的入射角有关,还与观察者视线和物体表面之间的角度有关。镜面反射通常会造成物体表面上的“闪烁”和“高光”现象,镜面反射的强度也与物体的材质有关,无光泽的木材很少会有镜面反射发生,而高光泽的金属则会有大量镜面反射。


Phong光照模型


Phong光照模型中主要的部分就是对高光的计算,首先来看下面这张图片:
Unity Shader-Phong光照模型与Specular_第1张图片
理想情况下,光源射出的光线,通过镜面反射,正好在反射光方向观察,观察者可以接受到的反射光最多,那么观察者与反射方向之间的夹角就决定了能够观察到高光的多少。夹角越大,高光越小,夹角越小,高光越大。而另一个影响高光大小的因素是表面的光滑程度,表面越光滑,高光越强,表面月粗糙,高光越弱。L代表光源方向,N代表顶点法线方向,V代表观察者方向,R代表反射光方向。首先需要计算反射光的方向R,反射光方向R可以通过入射光方向和法向量求出,R + L = 2dot(N,L)N,进而推出R = 2dot(N,L)N - L。关于R计算的推导,可以看下面这张图:
Unity Shader-Phong光照模型与Specular_第2张图片

不过在cg中,我们不用这么麻烦,cg为我们提供了一个计算反射光方向的函数reflect函数,我们只需要传入入射光方向(光源方向的反方向)和表面法线方向,就可以计算得出反射光的方向。然后,我们通过dot(R,V)就可以得到反射光方向和观察者方向之间的夹角余弦值了。下面给出冯氏反射模型公式:

I(spcular) = I * k * pow(max(0,dot(R,V)), gloss) ,其中I为入射光颜色向量,k为镜面反射系数,gloss为光滑程度。

通过上面的公式,我们可以看出,镜面反射强度跟反射向量与观察向量的余弦值呈指数关系,指数为gloss,该系数反映了物体表面的光滑程度,该值越大,表示物体越光滑,反射光越集中,当偏离反射方向时,光线衰减程度越大,只有当视线方向与反射光方向非常接近时才能看到高光现象,镜面反射光形成的光斑较亮并且较小;该值越小,表示物体越粗糙,反射光越分散,可以观察到光斑的区域越广,光斑大并且强度较弱。

Phong光照模型在Unity中的实现


下面看一下冯氏光照模型在Unity中的实现,由于有高光,为了更好的效果,我们将主要的计算放到了fragment shader中进行。不多说,上代码:
Shader "ApcShader/SpecularPerPixel"
{
	//属性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 255)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"

			//定义函数
			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定义结构体:应用阶段到vertex shader阶段的数据
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD1;
			};

			//顶点shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法线转化到世界空间
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//顶点位置转化到世界空间
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//归一化光方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//再次归一化worldNorml
				fixed3 worldNormal = normalize(i.worldNormal);
				//diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//计算反射方向R,worldLight表示光源方向(指向光源),入射光线方向为-worldLight,通过reflect函数(入射方向,法线方向)获得反射方向
				fixed3 reflectDir = normalize(reflect(-worldLight, worldNormal));
				//计算该像素对应位置(顶点计算过后传给像素经过插值后)的观察向量V,相机坐标-像素位置
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//计算高光值,高光值与反射光方向与观察方向的夹角有关,夹角为dot(R,V),最后根据反射系数计算的反射值为pow(dot(R,V),Gloss)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0.0,dot(reflectDir, viewDir)), _Gloss);
				//冯氏模型:Diffuse + Ambient + Specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
			ENDCG
		}
	}

	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}

我们找一个球球,使用我们的shader看一下效果:
Unity Shader-Phong光照模型与Specular_第3张图片

调整观察的方向以及光滑程度,可以看见亮斑是随着观察的方向改变而变化的:
Unity Shader-Phong光照模型与Specular_第4张图片



Blinn-Phong光照模型


Phong光照模型能够很好地表现高光效果,不过Blinn-Phong光照的缺点就是计算量较大,所以,在1977年,Jim Blinn对Phong光照进行了改进,称之为Blinn-Phong光照模型。

关于Blinn-Phong和Phong光照模型的对比,可以参照这张图片:
Unity Shader-Phong光照模型与Specular_第5张图片
Blinn-Phong光照引入了一个概念,半角向量,用H表示。半角向量计算简单,通过将光源方向L和视线方向V相加后归一化即可得到半角向量。Phong光照是比较反射方向R和视线方向V之间的夹角,而Blinn-Phong改为比较半角向量H和法线方向N之间的夹角。半角向量的计算复杂程度要比计算反射光线简单得多,所以Blinn-Phong的性能要高得多,效果比Phong光照相差不多,所以OpenGL中固定管线的光照模型就是Blinn-Phong光照模型。

BlinnPhong光照模型如下:

I(spcular) = I * k * pow(max(0,dot(N,H)), gloss) ,其中I为入射光颜色向量,k为镜面反射系数,gloss为光滑程度。

Blinn-Phong光照在Unity中的实现


Shader "ApcShader/BlinnPhongPerPixel"
{
	//属性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 256)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"

			//定义函数
			#pragma vertex vert
			#pragma fragment frag

			//定义Properties中的变量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定义结构体:应用阶段到vertex shader阶段的数据
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD1;
			};

			//顶点shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法线转化到世界空间
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//顶点位置转化到世界空间 
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//世界空间下光线方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//计算Diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//计算视线方向(相机位置-像素对应位置)
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//计算半角向量(光线方向 + 视线方向,结果归一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//计算Specular(Blinn-Phong计算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//结果为diffuse + ambient + specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}

我们再放一个球,使用Blinn-Phong光照,与Phong光照模型进行对比:
Unity Shader-Phong光照模型与Specular_第6张图片
换个角度再看一下:
Unity Shader-Phong光照模型与Specular_第7张图片

带纹理的Phong光照Shader


只有两个没有纹理的球,是说明不了问题的,下面来看一下带有纹理的Phong光照Shader。首先,我们要思考一个问题,如果我们要使用Specular类型的Shader,那么这个物体一般是金属类型的,这样,这个物体就会呈现金属特有的高光属性。然而,实际上完全是金属的物体并不是很多,现实世界中漫反射和镜面反射是共存的,拿一把刀来说,刀身是金属,刀柄是木头,那么,只有刀身适合这种类型的shader。可能我们最简单的想法是把刀拆成两个部分,刀身用的是Specular,刀柄用Diffuse;但是这种做法很麻烦,而且一个物体通过了两个drall call才能渲染出来。所以,聪明的前辈们总是能想到好的办法,次时代类型游戏中最简单的一种贴图就诞生了---高光贴图(通道)。

所谓高光贴图,或者说成高光通道,就是通过在制作贴图时,把图片的高光信息存储在一个灰度图或者直接存储在贴图的通道内,如果不需要Alpha Test的话,可以直接把高光通道放在Diffuse贴图的Alpha通道。而我们在shader中计算时,通过采样,就可以获得这个贴图中每个像素对应的位置是否是有高光的。这样,在Fragment Shader中可以直接通过这个Mask值乘以高光从而通过一个材质渲染出同一个模型上的不同质地。比如在通道内,0表示无高光,1(255)表示高光最强,那么,不需要高光的地方我们就可以在制作这张图的时候给0,需要高光的,刷上颜色,颜色越接近白色,高光越强。

知道了原理之后,我们就可以找一个人物模型贴图,然后在PhotoShop中给RGB格式的贴图增加一个通道,作为高光通道,然后把需要高光的部分抠出来,其他部分置为黑色
Unity Shader-Phong光照模型与Specular_第8张图片
好吧,作为一个程序员,一直想说,我.......的美工,实在不怎么样。不管图扣得再怎么差,原理没错就好。下面上shader:
Shader "ApcShader/BlinnPhongWithTex"
{
	//属性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_SpecularScale("SpecularScale", Range(0.0, 5.0)) = 1.0
		_Gloss("Gloss", Range(0.0, 1)) = 20
		_MainTex("RGBSpecular", 2D) = "white"{}
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"

			//定义函数
			#pragma vertex vert
			#pragma fragment frag

			//定义Properties中的变量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			float _SpecularScale;
			sampler2D _MainTex;
			float4 _MainTex_ST;

			//定义结构体:应用阶段到vertex shader阶段的数据
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD0;
				float2 uv : TEXCOORD1;
			};

			//顶点shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法线转化到世界空间
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//顶点位置转化到世界空间 
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				//转化uv
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				//世界空间下光线方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//计算Diffuse
				fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
				//计算视线方向(相机位置-像素对应位置)
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//计算半角向量(光线方向 + 视线方向,结果归一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//计算Specular(Blinn-Phong计算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//纹理采样
				fixed4 tex = tex2D(_MainTex, i.uv);
				//纹理中rgb为正常颜色,a为一个高光的mask图,非高光部分a值为0,高光部分根据a的值控制高光强弱
				fixed3 color = (diffuse  + ambient + specular * tex.a * _SpecularScale) * tex.rgb;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}
看一下最终效果,左侧为使用了高光的效果,右侧为普通diffuse的效果,可以看出,使用了高光贴图,我们只有上面的刀才表现出了高光,其他部分仍然是正常的。

Unity Shader-Phong光照模型与Specular_第9张图片

高光有个很搭配的后处理,就是 Bloom效果(全屏泛光),也是我很喜欢的效果之一。虚幻四的Bloom效果非常好,带有金属的效果加上泛光,让人特别舒服,我们在Unity中用一个简单的Bloom效果,让金属效果更好。
Unity Shader-Phong光照模型与Specular_第10张图片

最后来一张动图,Specular的主要特点就在于当观察者与对象角度有变化时,也就是V向量和R向量之间的角度有变化时,高光效果会有变化,所以只有动起来,才能真正看出Specular与Diffuse的不同。
Unity Shader-Phong光照模型与Specular_第11张图片

Shader的优化


一般情况下,fragment shader是性能的瓶颈,所以优化Shader的重要思路之一就是减少逐像素计算,将计算挪到vertex shader部分,然后通过vertex shader向fragment shader中传递参数。正常情况下,一个物体在屏幕上,逐顶点计算的量级要远远小于物体在屏幕上逐像素计算的量(当然如果物体离相机特别远,光栅化之后在屏幕上只占了很小的一部分时,有可能有反过来的情况,但是有LOD之类的技术的话,远了之后,更换为低模,也会降低顶点数,所以还是逐像素计算的比较可怕,尤其是分辨率大了之后)。当然,我们也不能把所有计算都放在vertex shader中,上一篇文章中说过,如果将高光计算放在vertex shader中,效果很差,下面就来看一下,效果有多差:
Unity Shader-Phong光照模型与Specular_第12张图片
为什么会有这样的结果呢,主要是顶点计算的结果是通过顶点传递好的颜色进行高洛德着色,只是一个颜色的插值。而放在像素着色阶段,是通过顶点传递过来的参数,并且传递到像素阶段时经过了插值计算得到的信息,逐像素计算光照效果得到最终结果。更加详细的解释可以参照 上一篇文章。2001年左右第三代modern GPU开始支持vertex shader,而在2003年左右,NVIDIA的GeForce FX和ATI Radeon 9700开始,GPU才开始支持fragment shader,也就是说fragment更先进,可以得到更好的效果。所以,我们只是将一些不会影响效果的计算放在vertex shader中即可。

上面的blinn-phong shader中,我们在fragment shader中计算了世界空间下的ViewDir,我们可以把这个计算移到vertex shader中进行:
//blinn-phong shader
//puppet_master
//2016.12.11
Shader "ApcShader/BlinnPhongPerPixel"
{
	//属性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 256)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"

			//定义函数
			#pragma vertex vert
			#pragma fragment frag

			//定义Properties中的变量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定义结构体:应用阶段到vertex shader阶段的数据
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 viewDir : TEXCOORD1;
			};

			//顶点shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法线转化到世界空间
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//顶点位置转化到世界空间 
				float3 worldPos = mul(_Object2World, v.vertex).xyz;
				//计算视线方向(相机位置 - 像素对应位置)
				o.viewDir = _WorldSpaceCameraPos - worldPos;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//世界空间下光线方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//计算Diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//normalize
				fixed3 viewDir = normalize(i.viewDir);
				//计算半角向量(光线方向 + 视线方向,结果归一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//计算Specular(Blinn-Phong计算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//结果为diffuse + ambient + specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}
在优化前后,没有特别明显的变化:
Unity Shader-Phong光照模型与Specular_第13张图片








你可能感兴趣的:(图形学,Unity3d,Shader,Unity,Shader!!!)