【Shader笔记】Unity基础光照

参考书籍:Unity Shader入门精要

一、认识光照

1.1 光源

实时渲染中,通常光源为一个没有体积的点,用 l {l} l表示其光照方向。

1.1.1 如何量化光

答:使用辐照度

  • 对平行光,通过计算在垂直于 l {l} l的单位面积上单位时间内穿过的能量得到
    默认方向的矢量为1,如下为理解图:
    【Shader笔记】Unity基础光照

1.2 吸收和散射

光线由光源发射,与一些物体相交产生的结果:散射(scattering) 和 吸收(absorption)。

改变方向 改变颜色 改变密度
散射
吸收

1.2.1 散射

光线经物体表面散射后,有两种方向:

  • 散射到外部:折射(refraction)或 透射(transmission)
  • 散射到内部:反射(reflaction)
    对于不透明物体,到内部的光线继续与内部颗粒相交。部分光线最后会重新发射出物体表面,另一部分被吸收。如下为理解图:
    【Shader笔记】Unity基础光照

1.2.2 在Unity中区分散射方向

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

根据入射光线数量和方向,计算出射光线的数量和方向,使用出射度(exitnce)来描述。

1.3 着色

根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。这个等式也成为光照模型(Lighting Model)。

1.4 BRDF光照模型

BDRF(Bidirectional Reflectance Distribution Function):回答物体表面与光线是如何交互。

二、标准光照模型

只关心直接光照(direct light),即经过物体表面一次反射直接进入摄像机的光线。它将进入摄像机的光线分为 自发光、高光反射、漫反射、环境光 共四部分。

2.1 自发光(emissive)

解释:给定一方向后,表面本身向该方向发射多少辐射量。

注意:未使用全局光照(global illumination)情况下,自发光的表面不会照亮周围物体,仅本身显得更亮

2.2 高光反射 (specular)

解释:当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量

2.3 漫反射(diffuse)

解释:当光线从光源照射到模型表面,该表面会向每个方向散射多少辐射量

符合兰伯特定律:反射光线的强度与表面发现和光源方向之间的夹角的余弦值成正比。
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 ∗ I ) {c_{diffuse} = (c_{light} *m_{diffuse})max(0, n*I)} cdiffuse​=(clight​∗mdiffuse​)max(0,n∗I)

  • n:表面法线
  • I {I} I:光源的单位矢量
  • m d i f f u s e {m_{diffuse}} mdiffuse​:材质的漫反射颜色
  • c l i g h t {c_{light}} clight​:光源颜色

注意:防止和避免法线和光源方向点乘的结果为负值。故使用 m a x ( 0 , n ∗ I ) {max(0,n*I)} max(0,n∗I)

2.4 环境光(ambient)

描述其他所有的间接光照。

间接光照:在多个物体之间反射,最后进入到摄像机。

2.5 逐像素与逐顶点

在片元着色器中计算:逐像素光照(per-pixel lighting)
在顶点着色器中计算:逐顶点光照(per-vertex lighting)

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

漫反射计算公式 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 ∗ I ) {c_{diffuse}=(c_{light}*m_{diffuse})max(0, n*I)} cdiffuse​=(clight​∗mdiffuse​)max(0,n∗I)

  • 入射光线的颜色和强度 c l i g h t {c_{light}} clight​
  • 材质的漫反射系数 m d i f f u s e {m_{diffuse}} mdiffuse​
  • 表面法线 n {n} n
  • 光源方向 I {I} I

函数:saturate( x x x)
参数 x x x:用于操作的标量或矢量,可以是float、float2、float3等类型
描述:把 x {x} x截取在[0, 1]范围内,如果 x {x} x是一个矢量,那么会对它的每一个分量进行这样的操作。

3.1 逐顶点光照 实践

  • Window -> Lighting -> Skybox 设置天空盒材质为null
  • 新建材质并命名"DiffuseVertexLevelMat"
  • 创建 -> 着色器 -> 标准表面着色器,命名为"Chapter6-DiffuseVertextLevel"
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;
				float4 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"
}
  • 添加一个Direction Light,并观察效果图,如下所示:
    【Shader笔记】Unity基础光照

3.1.1 属性 准备

Properties{}语义块中声明Color属性,用于控制漫反射颜色。

Properties {
	_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}

3.1.2 标签 LightMode

顶点着色器需写在Pass{}语义块,指明该Pass{}的光照模式

Tags { "LightMode"="ForwardBase" }
  • LightMode标签:定义该Pass{}在Unity的光照流水线中的角色。
    仅定义了正确的LightMode下,才能获取一些Unity的内置光照变量。

3.1.3 定义 顶点/片元着色器

分别定义 [vertex顶点着色器]/[fragment片元着色器] 为 [vert]/[frag]

#pragma vertex vert
#pragma fragment frag

3.1.4 Unity内置文件 Lighting.cginc

Unity提供的内置变量的文件,这里需要Lighting.cginc中的一些变量,故需要包含这文件

#include "Lighting.cginc"

3.1.5 存储 变量

fixed4 _Diffuse:定义一变量与Properties{}中的属性相匹配,由于颜色属性在[0, 1]范围内,故选择fixed4精度存储。

3.1.6 定义 结构体

定义顶点着色器输入结构体:

struct a2v {
	float4 vertex : POSITION;
	float4 normal : NORMAL;
};
  • NORMAL:访问模型顶点的法线变量

定义顶点着色器输出结构体(等同于片元着色器输入结构体):

struct v2f {
	float4 pos : SV_POSITION;
	fixed3 color : COLOR;
};
  • color:接受计算的颜色变量,并未实际使用COLOR语义,部分情况也有使用TEXCOORD0语义。

3.1.7 实现 逐顶点漫反射

为实现漫反射,参照 2.3漫反射,需要以下四个参数:

  • 表面法线 worldNormal
  • 光源方向 worldLight
  • 材质颜色 _Diffuse
  • 光源颜色 LightColor0

代码内容如下

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

在Shader码中:

  • UnityObjectToClipPos():将模型空间坐标转换成裁剪空间坐标
  • UNITY_LIGHTMODEL_AMBIENT:Unity内置变量,此处为获取环境光部分。
  • unity_WorldToObject:原为_World2Object[已替换],此处为从模型空间到世界空间中的变换矩阵的逆矩阵。
  • _WorldSpaceLightPos0:Unity内置变量,此处为获取光源方向。
  • _LightColor0:Unity内置变量,此处为访问该Pass{}处理的光源颜色和强度信息。
    注意:需要定义合适的LightMode标签来获取正确的值
  • normalize():向量归一化(以单位向量表示,仅0或1)
  • _Diffuse:材质的颜色
  • saturate():防取值为0,限制值在[0, 1]
  • dot():矩阵点乘

3.1.8 输出 颜色

fixed4 frag(v2f i):SV_Target {
	return fixed4(i.color, 1.0);
}
  • i.color:颜色,返回的是float3类型,故1.0为透明度

3.1.9 回调(防该SubShader无法运行)

FallBack "Disffuse"

3.2 逐像素光照 实践

  • 新建材质,命名为 DiffusePixelLevelMat
  • 新建Shader,命名为 Chapter6-DiffusePixelLevel,并赋于给上述材质。
  • 添加下述内容:
Shader "Unity Shaders Book/Chapter 6/Diffuse 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;
				float4 normal : NORMAL;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 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 "Diffuse"
}
  • 添加一Direction Light,观察效果,如下图所示:
    【Shader笔记】Unity基础光照

3.2.1 属性 准备

Properties {
	_Diffuse ("Diffuse", Color) = (1,1,1,1)
	_Specular ("Specular", Color) = (1, 1, 1, 1)
	_Gloss ("Gloss", Range(8.0, 256)) = 20
}
  • Diffuse:漫反射颜色
  • Specular:光照颜色
  • Gloss:光滑程度

3.2.2 标签 LightMode

参考3.1.2描述

3.2.3 定义 顶点/片元着色器

参考3.1.3描述

3.2.4 Unity内置文件 Lighting.cginc

参考3.1.4描述

3.2.5 存储 变量

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

_Diffuse_Specular[同为Color]:选择 fixed4 数据类型存储
_Gloss:选择 float 数据类型存储

3.2.6 定义 结构体

顶点着色器输入

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

获取模型空间下的 顶点坐标 与 法线坐标。

片元着色器输入

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

获取从顶点着色器转片元着色器的数据,如模型在模型空间下的坐标、法线坐标和纹理坐标。

3.2.7 实现 逐像素光照

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;
}
  • UnityObjectToClipPos():将模型空间坐标 转换为 裁剪空间坐标
  • unity_WorldToObject:原为_World2Object[已替换],此处为从模型空间到世界空间中的变换矩阵的逆矩阵。
  • mul():计算矩阵的乘积
  • normalize():向量归一化(以单位向量表示,仅0或1)

3.2.8 输出 颜色

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);
}
  • UNITY_LIGHTMODEL_AMBIENT:Unity内置变量,此处为获取环境光部分。
  • normalize():向量归一化(以单位向量表示,仅0或1)
  • _LightColor0:Unity内置变量,此处为访问该Pass{}处理的光源颜色和强度信息。
    注意:需要定义合适的LightMode标签来获取正确的值
  • _WorldSpaceCameraPos:Unity内置变量,此处为屏幕相机的世界空间的坐标
  • pow(x, y):x的y次方
  • _WorldSpaceLightPos0:Unity内置变量,此处为获取光源方向。
  • saturate():防取值为0,限制值在[0, 1]
  • dot():矩阵点乘

3.2.9 回调

参考 3.1.9

3.3 Blinn-Phong 光照模型

Blinn模型没有使用反射方向,而是引入一个新的矢量 h 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 ) ) {c_{specular}=(c_{light}*m_{specular}max(0, n*h))} cspecular​=(clight​∗mspecular​max(0,n∗h))

  • 新建材质,命名 BlinnPhongMat
  • 新建Unity Shader,命名为 Chapter6-BlinnPhone,并赋予以上材质
  • 复制 Chapter6-specularPixelLevel 代码,仅修改以下代码:
fixed4 frag(v2f i) : SV_Target {
	...
	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.world.xyz);
	fixed3 halfDir = normalize(worldLightDir + viewDir);
	fixed3 specular = _LightColor0.xyz * _Specular.xyz + pow(max(0, dot(worldNormal, halfDir)), _Gloss);
	
	return fixed4(ambient + diffuse + specular, 1.0);
} 
  • 效果如下图所示:

【Shader笔记】Unity基础光照

四、Unity内置函数

UnityCG.cginc

函数名 描述
float3 WorldSpaceViewDir(float4 v) 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向
float3 UnityWorldSpaceViewDir(float4 v) 输入一个世界空间中的顶点坐标,返回世界空间中从该点到摄像机的观察方向
float3 ObjSpaceViewDir(float4 v) 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向
float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向
float3 UnityWorldSpaceLightDir(float4 v) 仅可用于前向渲染中,输入一个世界空间中的顶点位置。返回世界空间中从该点到光源的光照方向
float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中,输入一个模型空间中的顶点位置。返回模型空间中从该点到光源的光照方向
float3 UnityObjectToWorldNormal(float3 norm) 把法线方向从模型空间转换到世界空间中
float3 UnityObjectToWorldDir(in float3 dir) 把方向矢量从模型空间变换到世界空间中
float3 UnityWorldToObjectDir(float3 dir) 把方向矢量从世界空间变换到模型空间中
  • WorldSpaceViewDir()函数:返回值未被归一化,使用需要先归一。
上一篇:Pigeon源码分析(四) -- 服务端接收请求过程


下一篇:Jenkins 基础篇 - 插件安装