参考书籍:Unity Shader入门精要
一、认识光照
1.1 光源
实时渲染中,通常光源为一个没有体积的点,用 l {l} l表示其光照方向。
1.1.1 如何量化光
答:使用辐照度。
- 对平行光,通过计算在垂直于
l
{l}
l的单位面积上单位时间内穿过的能量得到
默认方向的矢量为1,如下为理解图:
1.2 吸收和散射
光线由光源发射,与一些物体相交产生的结果:散射(scattering) 和 吸收(absorption)。
改变方向 | 改变颜色 | 改变密度 | |
---|---|---|---|
散射 | √ | ||
吸收 | √ | √ |
1.2.1 散射
光线经物体表面散射后,有两种方向:
- 散射到外部:折射(refraction)或 透射(transmission)
- 散射到内部:反射(reflaction)
对于不透明物体,到内部的光线继续与内部颗粒相交。部分光线最后会重新发射出物体表面,另一部分被吸收。如下为理解图:
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,并观察效果图,如下所示:
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,观察效果,如下图所示:
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∗mspecularmax(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);
}
- 效果如下图所示:
四、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()函数
:返回值未被归一化,使用需要先归一。