解读Unity中的CG编写Shader系列7——漫反射

折射与反射

在物理世界中,光的反射与折射往往是同时存在的,光源由真空或者空气中射入一种材料,光在进入这种材料的同时就发生了折射,折射的程度与各个介质的折射率有关,使光的传播路线偏离原来的路线;

继而如果光在通过不同传播介质的表面时,会像乒乓球一样弹回来,我们人眼能够看到东西,都是因为东西会反射光源,如果一种物质无法反射光,或者没有光源,我们就看不到东西。同样对于不同的材料,光的通过性越高,反射越不明显,比如玻璃(但是玻璃同样会有一定的反射,不然我们是看不到玻璃的,实际上我们看得到);光的通过性越低,反射越明显,比如水面,我们可以看到水中的鱼(光从水射到鱼的皮肤,鱼的皮肤发生了漫反射),也可以看到自己的倒影(水面发生了镜面反射)。

镜面反射与漫反射

那么镜面反射与漫反射的区别就是光碰到的物体表面被弹回来的过程中,表面的平整程度,越平越接近镜面反射,越粗糙就是漫反射。我们现实生活中看到的绝大部分东西的表面都是粗糙的,所以都是漫反射。
在讨论漫反射之前一定要回过头来复习一下我们的物理科学中的镜面反射与折射,因为漫反射可以拆分为无数个微小的镜面反射(就像圆形可以看做无数条边的正多边形),毕竟游戏世界也是为了模拟物理世界,游戏引擎必须要符合物理规律。画个图回忆一下:
上面这张图我画的不是很好,我需要说明的入射光线与法平面的夹角是入射角,反射光线与法平面的夹角是反射角,入射角永远等于反射角,图没画好。
同理对于凹凸不平的介质交汇面,我们可以无限细分为无数个法平面,对于每一个表面的点的反射情况,我们都可以沿着当前点做一个切面与物体表面在该点相切,得到一个法平面。所以镜面反射的规律可以直接适用到漫反射。示意一下:
我们对于介质B的表面的某一点的反射情况进行分析的时候就在这个点做一个切面与表面相切于这个点,同样得到了这个位置的法平面、法线、入射角、反射角
好了复习到这里了,在前面的系列中我们已经解除到了法线与法平面,我们将我们的三维物体看到上图的介质B,那么黑色曲线部分为它的部分表面,则这个表面上的每一个点的法线的朝向都不相同,因为每一个点做切面切与这个表面得到的平面不一定平行,所以在CG中mesh信息中传递过来的每一个顶点都有一个normal值,即法向量。
法向量就是我们的法线中切点往介质A的方向上的单位向量。

反射过程

好了 接下来我们将前文提到的入射光线、法线 替换为入射向量L(表面到光源的方向为正方向)、法向量N。
那么我们的物体表面发生漫反射时,漫反射的量即是由入射向量I与每一个顶点位置的法向量N确定的:
diffuse=L·N
这个过程是向量I与N进行点乘,其过程为:
diffuse=|L|*|N|*cos∠(L,N)
由于我们直接讨论入射向量与法向量的单位向量,所以两个绝对值为1,那么漫反射duffuse=cos∠(L,N)
由此我们可以得到单位向量的入射光的漫反射强度diffuse的值域为:[-1,1]
由余弦曲线可以得知,diffuse在夹角为0~90°为正,90°~270°为负,270~360°为正
对于cos∠(L,N)超过90°的情况,我们认为入射光已经在介质表面的另外一面了,这种情况我们不考虑,一般来说漫反射都是在外表面进行的。
所以我们的diffuse当夹角超过90°时便认为反射量为0,所以我们对该式子修改为:
diffuse=max(0,cos∠(L,N))
即对入射向量与法向量的夹角的余弦 与0 取最大值,保证了反射量不会出现负数。
接下来让我们从物理和数学世界回到CG世界中

用CG编写漫反射Shader

在编写我们的第一个单光源漫反射shader之前,先说明一下我们需要的几个参数的来源:
1.光源的位置由unity的内置uniform参数 _WorldSpaceLightPos0给出,由此我们可以计算出每个顶点的入射向量
2.光源的颜色由uniform参数_LightColor0给出
3.法向量直接通过顶点着色器的输入参数中的带语义NORMAL来获得
有了入射向量、法向量、光源颜色,我们就能在顶点着色器中计算出每个顶点位置的反射量,并与光源颜色和材料颜色相乘得到最终表面实际着色的颜色。
用户自定义的参数:
1.材料颜色,通过shader的property定义
Shader "Custom/CustomDiffuse" {
	Properties {
		//材料颜色默认为黑色,可在inspector中调节
		_Color ("Material Color", Color) =  (1,1,1,1)
	}
	SubShader {
		//LightMode我们在后面会继续讨论
		Tags { "LightMode" = "ForwardBase" }
		
		Pass{
		CGPROGRAM
		// Upgrade NOTE: excluded shader from OpenGL ES 2.0 because it does not contain a surface program or both vertex and fragment programs.
		//#pragma exclude_renderers gles
		//定义顶点着色器与片段着色器入口
		#pragma vertex vert 
		#pragma fragment frag
		//获取property中定义的材料颜色
		uniform float4 _Color; 
		
		// 光源的位置或者方向
		//uniform float4 _WorldSpaceLightPos0;
		
		
		// 光源的颜色 (from "Lighting.cginc")
		uniform float4 _LightColor0;

		
		//定义顶点着色器的输入参数结构体 
		//我们只需要每个顶点的位置与对应的法向量
		struct vertexInput {
			float4 vertex : POSITION;
			float3 normal : NORMAL;
		};
		//定义顶点着色的输出结构体/片段着色的输入结构体
		//已经计算好的颜色
		struct vertexOutput {
			float4 pos : SV_POSITION;
			float4 col : COLOR;
		};
		
		

		//顶点着色器
		vertexOutput vert (vertexInput input) {
			vertexOutput output;
			//对象坐标系到世界坐标系的变换矩阵
			//_Object2World与_World2Object均为unity提供的内置uniform参数
			float4x4 modelMatrix = _Object2World;
			//世界坐标系到对象坐标系的变换矩阵
			float4x4 modelMatrixInverse = _World2Object;
			
			
			//计算对象坐标系中的顶点法向量的单位向量
			//将mesh传递过来的顶点法向量与模型-->对象坐标系矩阵相乘得到对象坐标系中的法向量
			//然后单位化
			float3 normalDirection = normalize(float3(mul(float4(input.normal, 0.0), modelMatrixInverse)));
			
			//计算入射向量的单位向量
			float3 lightDirection = normalize(float3(_WorldSpaceLightPos0));
			
			//计算反射后的颜色
			//先将光源颜色与材料颜色向量相乘
			//再乘以上文提到的max(0,cos∠(N,L))
			float3 diffuseReflection=float3(_LightColor0) * float3(_Color)* max(0.0, dot(normalDirection, lightDirection));	
			
			//上面计算的是RGB颜色,差个A,补充一维就可以传递给片段着色器了	
			output.col=float4(diffuseReflection,1.0);
			
			//国际惯例,顶点变化三步曲,这个例子中可写可不写
			output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
			
			return output;
		}
		
		//片段着色器,老规矩,把顶点着色器的输出参数作为片段着色器的输入参数
		float4 frag(vertexOutput input): COLOR
		{
			return input.col;
		 
		}
		
		ENDCG
	}
}
}
至此我们创建一个material 然后选择我们新建的这个shader,在inspector中选择一个颜色,然后再场景中布置一个唯一光源,将这个material给一个物体。
我把这个material给了奥迪A6L的引擎盖,光源在正前方,可以直观的看一下,引擎盖使用的shader是我们刚刚编写的漫反射shader,车身其余部分为镜面反射shader: