# 前言

这周被项目主管拉过去谈了一次话,提到这一年多的的进步之类的问题,说我算是进步很大的了 —— 顿时感觉心情 UPUP 的,相比之前被老板说的没进步之类的话打击后,觉得平复多了。另外还谈到了 Shader 之类的问题,还问我感不感兴趣之类的?这当然回答 “是” 了,毕竟现在至少一般应用的话,自我感觉都是没问题的了,不过,我依然没有自大地说:我已经会了,有什么需要的吗?

毕竟,我目前只是算会用了而已,高级点的,恐怕也依然不足。而且最主要是,不清楚究竟会有什么样的要求?万一刚说自己会,然后叫你实现什么却又不行,那就尴尬了.... 所以保险起见,想想还是大成之后再透露吧。

最近一直在研究数学,于是决定暂且回到 Shader 上,这儿,就回顾一下基础,分析一下基本光照模型。

基本光照模型,一般由三种颜色合成:环境光、漫反射及镜面高光。

所以公式一般为:Color_final=Color_ambient+Color_diffuse+Color_specialerColor\_{final}=Color\_{ambient}+Color\_{diffuse}+Color\_{specialer}
下面会一个个解释。

# 环境光

# 简述

这里指的环境光并非物理意义上的那种 (因为目前而言,实时处理是一个几乎不可能的任务),而是指模仿这样的效果,简单使用一个设定好的固定颜色,并让所有物体都被它进行 “照明”,所以也被称作 “全局漫反射”。一般来说,都是直接将颜色与物体本身的颜色进行混合产生。

# 实现

而在 Unity 的 Shader 中,则是提供了一个内置变量 UNITY_LIGHTMODEL_AMBIENT 作为环境光颜色。而且这个颜色可以在 Unity 的编辑器 ->Lighting 中配置。
代码如下所示 (默认物体颜色为白色):

	fixed4 frag (v2f i) : SV_Target
	{
		float4 color=float4(1,1,1,1);
		float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;

		float4 finalColor=ambColor;	
		UNITY_APPLY_FOG(i.fogCoord, finalColor);	
		return finalColor;
	}

![效果](/blogimages/oldpictures/2016.5.22-1_Ambient Color.JPG)

# 漫反射

# 简述

漫反射计算了光线与物体顶点的角度,然后对物体颜色做相应的衰减。

简单来说,它的概念就是:顶点法线与入射光线越角度越一致,那么颜色就越亮,反之则越暗。这个功能简单依靠点积实现,如果结果为负,则将其置为 0,即背光面的最终颜色将会变成 0 (所以如果只有漫反射的话,背光面就只会是黑的,因此才会有前面的环境光)。

公式也是很简单:

Colordiff=max(0,(NL))×ColoroColor_{diff}=max(0,(N \cdot L)) \times Color_o

# 实现

在 Shader 中,因为需要用到法线,因此我们必须在定义的结构体上加上法线相关的语义绑定:

struct appdata
{
	float4 vertex : POSITION;
	float4 normal:NORMAL;
};

然后在顶点程序中,还需要将法线及顶点坐标转换至世界空间,并将其传入片段程序进行计算。当然这儿直接在顶点程序计算,然后通过插值获得最后结果也是可以的,不过从理论上来说,效果会比在片段中进行计算要差那么一点点 (如果是低模的,那就不止差一丁半点了)... 所以这儿我选择将数据传入片段程序,然后再行计算。

所以首先在传入片段程序的结构体 v2f 中添加两个参数,用于存放法线及光源方向:

	float3 N:TEXCOORD0;
	float3 L:TEXCOORD1;

然后在顶点计算中进行赋值:

	float3 wPos=mul(_Object2World,v.vertex).xyz;
				
	o.N=mul(float4(v.normal,0),_World2Object).xyz;
	o.L=_WorldSpaceLightPos0.xyz;

需要注意的是对法线的转换,因为对法线的转换时不能使用与顶点一样的矩阵进行的,而是使用世界矩阵转置的逆矩阵进行,我们通过右乘世界矩阵的逆矩阵实现。至于原因的话,百度一下应该就可以找到比较详细解释,这只是提示一下而已。

然后在片段程序中,使用法线与光源方向计算漫反射,并添加到最后的颜色贡献中:

	float4 diffColor=color*max(0,dot(normalize(i.N),normalize(i.L)))*_LightColor0;
				
	float4 finalColor=ambColor+diffColor;	

效果(左边是Unity自带的Shader,左边是我们自定义的)

# 镜面反射

# 简述

一般在比较光滑的表面,我们都会注意到一些高光,并且随着我们视线角度的不同,都会产生一定的变化,而且,几乎所有物体都是具有这个属性的。比如说,现在盯着你的键盘看一下,每个按键上边,是不是都有一点油亮的感觉呢?而且低一下头、转一下头,这些发亮区域还会发生变化。

这就是镜面反射。

由此我们也可以清楚地知道,首先它肯定是与我们视线相关,其次,与物体表面的光滑程度相关,最后,当然就是与光线相关了。这东西其实也是属于经验公式,即并不完全符合物理,但却可以带来相似的视觉效果。刚才我们通过 “体验”,应当大约明白这是个什么样子了吧?

# 推导

先看一下图 (手绘的,不要在意细节):

镜面反射

相比其它的计算方式,镜面反射主要就是反射向量 R 计算起来比较麻烦一点。
因为很久以前听说,市面上挺流行考这个问题的,所以当年还是仔细研究过的,毕竟背公式什么的,就太 Low 了,要画个草图,就明白过程才是。那么,让我们来仔细推导一下吧,要知道这算还算是图形学里边比较简单的了。

在上面的那个简图中,我们可以把这三个方向向量看作两个三角形提取出来,如图所示:

三角形

那么,我们只需要先计算出这个 “大三角形” 的 “底” 的向量,无论是 R 到 L 还是 L 到 R 的方向,都可以通过 L 与其计算出最后的 R。那么,这儿我就设 L 到 R 方向的 “底” 为 K,那么R=L+KR=L+K
OK,大致方向就是这样了,那么第一个就是计算光源 L 在法线 N 上的投影 (我们设其为 M),也就是这两个三角形的 “高”,之后即可通过 M-L 计算出其中一个三角形的 “底” 向量,将这个 “底” 乘以 2,就是光源到 R 的向量。首先,我们都知道AB=ABCosθA \cdot B=|A||B|Cos\theta,同时由于我们这儿计算的向量都是经过规范化的,如此AB=1|A||B|=1,就可以直接略过了。
剩下的CosθCos\theta 就是 M 的长度,这时候,只需要再乘以法向量 N,就计算出向量 M 了。
然后

(ML)×2=K(M-L) \times 2=K

K+L=RK+L=R

即最终公式为:

((LN)×NL)×2+L=R((L \cdot N) \times N-L) \times 2+L=R

化简之后可得:

2N×(LN)L=R2N \times (L \cdot N)-L=R

这就是经典的 Phong 模型计算公式了 —— 才怪 —— 要知道我们这才刚计算出反射向量呢,接下来就是将反射 R 实际运用起来了。
刚开始我们就提到过,镜面反射肯定与视线也是相关的,也就是我们看过去的方向和反射光线的方向,即取决于反射向量 R 与视线 V 的夹角,跟上边的漫反射 N 与 L 的关系有点类似:当两者方向越接近时越亮,反之则越暗,不过多了一个高光指数用于控制高亮区域大小之类的。

公式如下:

Colorspecialer=Colorreflect(VR)powerColor_{specialer}=Color_{reflect}*(V \cdot R)^{power}

# 实现

# Phong 模型

如果已经明白了原理的话,那么实现起来,就相当简单了。
上面实现漫反射过程中,已经将光源方向机法线方向都传入了片段程序,因为镜面高光的实现还需要一个 “视线”,也就是相机的方向,在 Unity 中,则可由内置变量 “_worldSpaceCameraPos” 提供。

为了向片段程序再传入一个视线方向,所以得在结构体 v2f 中再加一个参数:

float3 V:TEXCOORD2;

然后在顶点程序中,因为主要计算在片段程序中,所以也是一句代码搞定:

o.V=_WorldSpaceCameraPos-wPos;

来到片段程序,首先将相机方向进行归一化处理,然后通过上述公式计算反射向量 R,并通过 Phong 公式计算最终镜面高光。最后,将所有的颜色加上去:

float3 V=normalize(i.V);
float3 R=normalize(2*N*(dot(N,L))-L)
float4 specColor=_SpColor*pow(saturate(dot(V,R)),_SpPower);

float4 finalColor=ambColor+diffColor+specColor;	

效果:

镜面反射效果(左StandarShader,左边是我们自定义的)

这个光似乎显得太 “硬” 了... 所以可以在最后计算出来的镜面高光中,乘上一个值对其进行缩放:

float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower);

在这儿我使用了镜面颜色的 Alpha 值作为这个参数,之所以这样,跟 Unity5 内置的高光相比,显得太过难看,而且有些版本的镜面高光就有这个缩放参数。它可以将其柔和化。另外使用 Alpha 则是因为它在整个计算中根本没用,本着性能和废物利用的想法,就把它当作缩放参数来调整了。

效果:

镜面反射效果2(左StandarShader,左边是我们自定义的)

另外,其实计算反射的方法,CG 中就已经内置了函数 “reflect (R,N)” 进行计算,主要注意传入的光源参数是指光源到顶点的方向就可以了。

# Blinn 模型

上面的是 Phong 模型,另外还有一个 Blinn 模型也是用来计算镜面高光的,公式如下:

H=L+VH=L+V

Colorspecialer=Colorreflect(HN)powerColor_{specialer}=Color_{reflect}*(H \cdot N)^{power}

可以稍微看看在下的拙图:

Blinn描绘

其中 H 是半角向量,指的是光源方向和视线的中间向量,除了其中点乘的两个参数换了,其它的跟 Phong 模型基本一致。不过由于半角向量的计算比 Phong 模型的反射向量 R 计算少了些计算步骤,所以性能上会好些,另外的区别自己比较下就知道了 (后面我会上源码,大家可以取消相应的注释进行测试)。

代码:

float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);

其实我觉得 Unity5 内置的 Standard Shader 就是使用的 Blinn 模型,我还是上一张比较能看出区别的侧视图吧:

Phong模型与Blinn模型比较(左StandarShader,左边是我们自定义的)

看出区别了吗?

# 点光源处理

最后的最后,就是对点光源的处理了。
之前那些处理过程,在 Unity 中都只针对第一个平行光源有效,所以为了支持点光源的话,还得多做一些处理。不过也都是些比较简单小改造。

在 Unity 中,点光源的渲染是通过在 Shader 中叫做 “ForwardAdd” 的第二个 Pass 进行,之前的我们的工作只能算是完成了第一个 Pass,也就是 “ForwardBase”。不过也不用担心,这两个 Pass 的内容区别也不是很大,只需要在前面的第一个 Pass 中进行些许修改,就可以挪到第二个 Pass 中了。这也是 Unity 的设定,而我们只需在 Pass 当先的 LightModel 设定好,那么它就可以工作了。

另外,我们要知道点光源与平行光源是不一样的!

  1. 最大的不同就是:点光源是有距离,而平行光在游戏中,是不算距离的。那么在此,计算点光源的时候,就得通过计算点光源的距离,而对其照明进行衰减。我们可以直接通过计算光源的距离,然后传入片段程序:
    结构体 v2f 加入衰减值:
float atten:TEXCOORD3;

计算衰减 (注意与平行光计算的不同之处):

o.L=_WorldSpaceLightPos0.xyz-wPos;
o.atten=1/length(o.L);
  1. 还有一点需要注意的就是,第二个 Pass 当先还需要使用 Blend 命令使其与第一个 Pass 计算出来的结果混合。
Blend One One

效果:

加入点光源支持效果(左StandarShader,左边是我们自定义的)

# 源码

//Author:CWHISME
//Date:2016.5.22
//Decription:基本光照模型
Shader "CWH/Base Light Model"
{
	Properties
	{
		_SpColor ("Specialer Color", COLOR) = (1,1,1,1)
		_SpPower("Specialer Power",FLOAT)=1
	}
	
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"

			uniform float4 _SpColor;
			uniform float _SpPower;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal:NORMAL;
			};

			struct v2f
			{
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
				float3 N:TEXCOORD0;
				float3 L:TEXCOORD1;
				float3 V:TEXCOORD2;
			};
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				
				float3 wPos=mul(_Object2World,v.vertex).xyz;
				
				o.N=mul(float4(v.normal,0),_World2Object).xyz;
				o.L=_WorldSpaceLightPos0.xyz;
				o.V=_WorldSpaceCameraPos-wPos;
				
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}

			fixed4 frag (v2f i) : SV_Target
			{
				float4 color=float4(1,1,1,1);
				float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;
				
				float3 N=normalize(i.N);
				float3 L=normalize(i.L);
				float3 V=normalize(i.V);
				float4 diffColor=color*max(0,dot(N,L))*_LightColor0;
				float3 R=normalize(2*N*(dot(N,L))-L);
				//float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);
				float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower);
				
				float4 finalColor=ambColor+diffColor+specColor;	
				UNITY_APPLY_FOG(i.fogCoord, finalColor);	
				return finalColor;
			}
			ENDCG
		}
		
		//计算点光源
		Pass
		{
			Tags{"LightMode"="ForwardAdd"}
			Blend One One
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"

			uniform float4 _SpColor;
			uniform float _SpPower;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal:NORMAL;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 N:TEXCOORD0;
				float3 L:TEXCOORD1;
				float3 V:TEXCOORD2;
				float atten:TEXCOORD3;
			};
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				
				float3 wPos=mul(_Object2World,v.vertex).xyz;
				
				o.N=mul(float4(v.normal,0),_World2Object).xyz;
				o.L=_WorldSpaceLightPos0.xyz-wPos;
				o.atten=1/length(o.L);
				o.V=_WorldSpaceCameraPos-wPos;
				
				return o;
			}

			fixed4 frag (v2f i) : SV_Target
			{
				float4 color=float4(1,1,1,1);
				float4 ambColor=color*UNITY_LIGHTMODEL_AMBIENT;
				
				float3 N=normalize(i.N);
				float3 L=normalize(i.L);
				float3 V=normalize(i.V);
				float4 diffColor=color*max(0,dot(N,L))*_LightColor0*i.atten;
				float3 R=normalize(2*N*(dot(N,L))-L);
				//float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(N,normalize(L+V))),_SpPower);
				float4 specColor=_SpColor.a*_SpColor*pow(saturate(dot(V,R)),_SpPower)*i.atten;
				
				float4 finalColor=ambColor+diffColor+specColor;	
				return finalColor;
			}
			ENDCG
		}
	}
}


好了,总算差不多完了!

写写停停,可不止花了一两天... 唉~时间啊....

这就是目前多数游戏中使用的最基本的光照模型了,稍微分析了下,也算是复习下基础了。