Unity Shader学习(3)-光照模型

模拟真实的光照环境:

光线从光源发射
光线与场景中的物体相交
被物体吸收
散射到其他方向
摄像机吸收光线产生图像
相交
散射Scatering
吸收Absorption
物体内部 折射refraction
物体外部 反射reflection

高光反射(specular):表示物体表面是如何反射光线的
漫反射(diffuse):表示有多少光线被折射、吸收和散射出表面

标准光照模型:

直接光照(direct light):直接从光源发射出来经过物体表面一次发射后进入摄像机的光线

基本方法:把进入到摄像机的光线分为4部分,每一部分单独计算贡献度

  • 自发光(emissive)部分:当给定一个方向时,物体本身会向这个方向发射多少辐射量。
  • 高光反射(specular)部分:描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
  • 漫反射(diffuse)部分:描述当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。
  • 环境光(ambient)部分:描述其他所有的间接光照。

环境光:一般是一个全局变量,即场景中的所有物体都使用同一个环境光

c a m b i e n t = g a m b i e n t c_{ambient} = g_{ambient} cambient=gambient

自发光:直接使用该材质的自发光颜色

c e m i s s i v e = m e m i s s i v e c_{emissive} = m_{emissive} cemissive=memissive

漫反射:符合兰伯特定律(Lambert’s Law):反射光线的强度与表面法线和光源方向的夹角的余弦值成正比

c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) m a x ( 0 , n ⃗ ⋅ l ⃗ ) c_{diffuse} = (c_{light}·m_{diffuse})max(0,\vec{n}·\vec{l}) cdiffuse=(clightmdiffuse)max(0,n l )

  • c l i g h t c_{light} clight:光源颜色
  • m d i f f u s e m_{diffuse} mdiffuse:材质漫反射颜色
  • n ⃗ \vec{n} n :表面法线
  • l ⃗ \vec{l} l :指向光源的单位矢量
  • m a x ( 0 , n ⃗ ⋅ l ⃗ ) max(0,\vec{n}·\vec{l}) max(0,n l ):防止点乘结果为负值

高光反射

需要知道的信息:表面法线 n ⃗ \vec{n} n 、视角方向 v ⃗ \vec{v} v 、光源方向 l ⃗ \vec{l} l 、反射方向 r ⃗ \vec{r} r
r ⃗ = 2 ( n ⃗ ⋅ l ⃗ ) n ⃗ − l ⃗ \vec{r} = 2(\vec{n}·\vec{l})\vec{n} - \vec{l} r =2(n l )n l

Phong模型:

c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , v ⃗ ⋅ r ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{v}·\vec{r})^{m_{gloss}} cspecular=(clightmspecular)max(0,v r )mgloss

  • c l i g h t c_{light} clight:光源颜色和强度
  • m s p e c u l a r m_{specular} mspecular:材质的高光反射颜色和强度
  • m g l o s s m_{gloss} mgloss:材质的光泽度,越大,高光区域亮点越小

Blinn模型:
为了避免计算反射方向 r ⃗ \vec{r} r ,引入一个新的矢量 h ⃗ \vec{h} h h ⃗ \vec{h} h 是由视角方向 v ⃗ \vec{v} v 与光源方向 l ⃗ \vec{l} l 取平均再归一化得到的,即

h ⃗ = v ⃗ + l ⃗ ∣ v ⃗ + l ∣ ⃗ \vec{h} = \frac{\vec{v}+\vec{l}}{\vec{|v}+\vec{l|}} h =v +l v +l

然后使用 n ⃗ \vec{n} n h ⃗ \vec{h} h 之间的夹角进行计算
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , n ⃗ ⋅ h ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{n}·\vec{h})^{m_{gloss}} cspecular=(clightmspecular)max(0,n h )mgloss

如果摄像机和光源距离模型足够远的情况下,我们可以认为 v ⃗ \vec{v} v
l ⃗ \vec{l} l 是定值,所以 h ⃗ \vec{h} h 是一个常量,此时Blinn模型会快于Phong模型

逐顶点光照(per-vertex lighting):在顶点着色器中使用基本光照模型
逐像素光照(per-pixel lighting):在片元着色器中使用基本光照模型

Unity中的环境光与自发光

环境光
Unity Shader学习(3)-光照模型_第1张图片

在Shader中,只需要通过内置变量UNITY_LIGHTMODEL_AMBIENT就可以直接得到环境光的信息

大多数物体是没有自发光特性的,所以不用去计算;如果非要计算自发光的话,就在片元着色器输出最后颜色之前,将材质的自发光颜色加进去就可以

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

逐顶点光照

  • 新建一个材质Material
  • 新建一个Shader并赋给Material
  • 创建一个Capsule,把Material赋给该胶囊体

Shader内容为:

Shader "Custom/Chapter6-DiffuseVertexLevel" {
	Properties{
		//声明一个颜色属性:白色
		_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
	}
	SubShader{
		Pass{
			//定义该Pass在渲染中的角色
			Tags{"LightMode" = "ForwardBase"}
			//以下为Cg代码片,ENDCG结束
			CGPROGRAM
		
			#pragma vertex vert//顶点着色器
			#pragma fragment frag//片元着色器
			//包含内置的光照模型
			#include "Lighting.cginc"	
			//使用定义的属性必须定义一个与属性相匹配的变量
			//由于颜色范围在(0,1),所以使用fixed精度的变量
			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);
				//按照公式计算漫反射 saturate在这里等同于max
				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
			}		
	}
	//回调Shader设置为内置Diffuse
	Fallback "Diffuse"
}

与原来代码相比:

  • Unity自动将模型-世界-投影矩阵UNITY_MATRIX_MVP与顶点坐标的点乘替换为UnityObjectToClipPos函数,将顶点坐标从模型空间(object space)变换到裁剪空间(clipping space),更直观更好理解

运行结果:
Unity Shader学习(3)-光照模型_第2张图片

逐像素光照

Shader "Custom/Chapter6-DiffusePixelLevel" {
		Properties{
		//声明一个颜色属性:白色
		_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
	}
	SubShader{
		Pass{
			//定义该Pass在渲染中的角色
			Tags{"LightMode" = "ForwardBase"}
			//以下为Cg代码片,ENDCG结束
			CGPROGRAM
		
			#pragma vertex vert//顶点着色器
			#pragma fragment frag//片元着色器
			//包含内置的光照模型
			#include "Lighting.cginc"	
			//使用定义的属性必须定义一个与属性相匹配的变量
			//由于颜色范围在(0,1),所以使用fixed精度的变量
			fixed4 _Diffuse;

			//定义结构
			struct a2v{	
				float4 vertex:POSITION;//顶点位置
				float3 normal:NORMAL;//顶点法线
			};

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

			//顶点着色器
			v2f vert(a2v v){
				v2f o;
				//将顶点坐标从模型空间变换到裁剪空间
				o.pos = UnityObjectToClipPos(v.vertex);
				//将法线方向从模型空间变换到世界空间
				o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);			

				return o;
			}
			//片元着色器
			fixed4 frag(v2f i) :SV_Target {
				//获得环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				//获取世界空间的法线方向
				fixed3 worldNormal = normalize(i.worldNormal);
				//获取世界空间中的光线方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//按照公式计算漫反射 saturate在这里等同于max
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
				//最终颜色值等于环境光加漫反射
				fixed3 color = ambient + diffuse;

				return fixed4(color,1.0);
			}			
			ENDCG
			}		
	}
	//回调Shader设置为内置Diffuse
	Fallback "Diffuse"
}

运行结果:
Unity Shader学习(3)-光照模型_第3张图片

逐像素光照可以达到更加平滑的光照效果,但是有一个问题存在:在光照无法到达的区域,模型一般是黑的,没有任何明暗变化。这可以通过添加环境光来改善,但是仍然有背光面的存在。因此,半兰伯特光照模型被提出

半兰伯特光照模型:Half Lambert

为了解决上述问题,Valve公司在开发《半条命》的时候提出了一种新技术,在原Lambert模型上进行了修改
c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( α ( n ⃗ ⋅ l ⃗ ) + β ) c_{diffuse} = (c_{light}·m_{diffuse})(\alpha(\vec{n}·\vec{l})+\beta) cdiffuse=(clightmdiffuse)(α(n l )+β)

与原模型相比,半兰伯特光照模型并没有使用max来限定 n ⃗ \vec{n} n , l ⃗ \vec{l} l 为(0,1),而是对其结果进行一个 α \alpha α倍的缩放在进行一个 β \beta β的偏移,一般默认为0.5
c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ( 0.5 ( n ⃗ ⋅ l ⃗ ) + 0.5 ) c_{diffuse} = (c_{light}·m_{diffuse})(0.5(\vec{n}·\vec{l})+0.5) cdiffuse=(clightmdiffuse)(0.5(n l )+0.5)
m a x ( 0 , n ⃗ ⋅ l ⃗ ) max(0,\vec{n}·\vec{l}) max(0,n l ),当 n ⃗ \vec{n} n · l ⃗ \vec{l} l 为负值时,即模型的背光面,点积值都变为了0,这样就导致没有明暗变化,而 α ( n ⃗ ⋅ l ⃗ ) + β \alpha(\vec{n}·\vec{l})+\beta α(n l )+β将点积结果映射到了不同的值上

半兰伯特光照模型是没有任何物理依据的,只是一种视觉加强技术

在Shader代码中按公式修改漫反射计算部分即可

运行结果:
Unity Shader学习(3)-光照模型_第4张图片

在Unity Shader中实现高光反射光照模型

逐顶点光照

公式回忆:
c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , v ⃗ ⋅ r ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{v}·\vec{r})^{m_{gloss}} cspecular=(clightmspecular)max(0,v r )mgloss
r ⃗ = 2 ( n ⃗ ⋅ l ⃗ ) n ⃗ − l ⃗ \vec{r} = 2(\vec{n}·\vec{l})\vec{n} - \vec{l} r =2(n l )n l
在Cg中提供了反射方向 r ⃗ \vec{r} r 的计算函数reflect(i, n)i为入射方向,一般为光源方向的相反方向

运行结果:
Unity Shader学习(3)-光照模型_第5张图片

可以观察到高光部分不平滑,主要的原因是高光的计算是非线性的,而逐顶点的计算是线性的,这破坏了原计算的非线性关系,就出现以上问题

逐像素光照

顶点着色器只需要计算世界空间下的法线方向和顶点坐标,然后传递给片元着色器即可

运行结果:Unity Shader学习(3)-光照模型_第6张图片
Blinn-Phong光照模型

Blinn模型没有使用反射方向 r ⃗ \vec{r} r ,而是定义了一个新的矢量 h ⃗ \vec{h} h
h ⃗ = v ⃗ + l ⃗ ∣ v ⃗ + l ∣ ⃗ \vec{h} = \frac{\vec{v}+\vec{l}}{\vec{|v}+\vec{l|}} h =v +l v +l

c s p e c u l a r = ( c l i g h t ⋅ m s p e c u l a r ) m a x ( 0 , n ⃗ ⋅ h ⃗ ) m g l o s s c_{specular} = (c_{light}·m_{specular})max(0,\vec{n}·\vec{h})^{m_{gloss}} cspecular=(clightmspecular)max(0,n h )mgloss

按照公式将高光计算部分修改即可

//计算h
fixed3 h = normalize(worldLight + viewDir);
//计算高光
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, h)), _Gloss);

运行结果:
Unity Shader学习(3)-光照模型_第7张图片

可以看出:Blinn-Phong模型的高光反射部分更大更亮一点,大多数情况下,在渲染的过程中都会选择此模型。

你可能感兴趣的:(Unity,Shader)