graphics-doc-光照

生活中的光

当光照射到物体表面时,一部分被物体表面吸收,另一部分被反射,对于透明物体而言,还有一部分光穿过透明体,产生透射光。被物体吸收的光能转化为热量,只有反射光和透射光能够进入眼睛,产生视觉效果。通过反射和透射产生的光波(光具有波粒二相性)决定了物体呈现的亮度和颜色,即反射和投射光的强度决定了物体表面的亮度, 而它们含有的不同波长光的比例决定了物体表面的色彩。

物体表面光照颜色由入射光、物体材质,以及材质和光的交互规律共同决定。

光与物体最基本的交互方式就是反射,遵循反射定律:反射光与入射光位于表面法向两侧,对理想反射面(如镜面),入射角等于反射角,观察者只能在表面法向的反射方向一侧才能看到反射光。 不过世界上并不存在真正的理想反射
体,正如物理学中绝对的匀速状态是不存在的。

光源类型

环境光

环境光(Ambient Light):从物体表面所产生的反射光的统一照明,称为环境光或背景光。环境光是对光照现象的最简单抽象,局限性很大。它仅能描述光线在空间中无方向并均匀散布时的状态。

由于环境光给予物体各个点的光照强度相同,且没有方向之分,所以在只有环境光的情况下,同一物体各点的明暗程度均一样,因此,只有环境光是不能产生具有真实感的图形效果。

光效果类型

漫反射

粗糙的物体表面向各个方向等强度地反射光, 这种等同地向各个方向散射的现象称为光的漫反射( diffuse reflection)。产生光的漫反射现象的物体表面称为理想漫反射体,也称为朗伯( Lambert)反射体。

对于仅暴露在环境光下的朗伯反射体,计算某点处漫反射的光强的公式:

$$I_{ambdiff}=K_{d}I_{a}$$

其中 $I_{a}$ 表示环境光强度(简称光强),$K_{d}(0< kd <1)$为材质对环境光的反射系数,$I_{ambdiff}$是漫反射体与环境光交互反射的光强。

而对于在光照下的朗伯反射体,表面光强与光线的入射方向(方向光)有关,漫反射光的光强与入射光的方向和入射点表面法向夹角的余弦成正比, 这称之为 Lambert 定律, 并由此构造出 Lambert漫反射模型:

$$I_{ldiff}=K_{d}I_{l}\cos\theta$$

$I_{l}$ 是 点 光 源 强 度 ,$\theta$ 是 入 射 光 方 向 与 顶 点 法 线 的 夹 角 , 称 为 入 射$(0\leq\theta\leq90°)$, $I_{ldiff}$ 是漫反射体与方向光交互反射的光强。入射角为零时,说明光线垂直于物体表面,漫反射光强最大;$90°$时光线与物体表面平行,物体接收不到任何光线。

通常我们都是知道法向量而不知道角度,因此使用法向量公式替换:

$$I_{ldiff}=K_{d}I_{l}(N \bullet L)$$

$N$ 为顶点单位法向量,$L$ 表示从顶点指向光源的单位向量(注意,是由顶点指向光源,不要弄反了),则$cosθ$ 等价于 $N$ 与 $L$ 的点积。

综合考虑环境光和方向来,Lambert 光照模型可写为:

$$I_{diff}=I_{ambdiff}+I_{ldiff}=K_{d}I_{a}+K_{d}I_{l}(N \bullet L)$$

漫反射光照模型顶点着色器程序参考代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct VertexIn
{
float4 position : POSITION;
float4 normal : NORMAL;
};

struct VertexScreen
{
float4 oPosition : POSITION;
float4 color : COLOR;
};

void main_v(VertexIn posIn,
out VertexScreen posOut,
uniform float4x4 modelViewProj,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float3 globalAmbient,
uniform float3 lightPosition,
uniform float3 lightColor,
uniform float3 Kd)
{
posOut.oPosition = mul(modelViewProj, posIn.position);
float3 worldPos = mul(worldMatrix, posIn.position).xyz;
float3 N = mul(worldMatrix_IT, posIn.normal).xyz;
N = normalize(N);

// 计算入射光方向
float3 L = lightPosition - worldPos;
L = normalize(L);

// 计算方向光漫反射光强
float3 diffuseColor = Kd*lightColor*max(dot(N, L), 0);

// 计算环境光漫反射光强
float3 ambientColor = Kd*globalAmbient;

posOut.color.xyz = diffuseColor+ambientColor;
posOut.color.w = 1;
}

镜面反射

一个光滑物体被光照射时,可以在某个方向上看到很强的反射光,这是因为在接近镜面反射角的一个区域内,反射了入射光的全部或绝大部分光强,该现象称为镜面反射。

phong

Phong Bui Tuong 提出一个计算镜面反射光强的经验模型,称为 phong 模型,认为镜面反射的光强与反射光线和视线的夹角相关,其数学表达如公式:

$$I_{spec}=K_{s}I_{l}(V \bullet R)^{n_s}$$

$K_s$为材质的镜面反射系数,$n_s$是高光指数, $V$ 表示从顶点到视点的观察方向,$R$ 代表反射光方向。

高光指数反映了物体表面的光泽程度。$n_s$ 越大,反射光越集中,亮点小,强度强,当偏离反射方向时,光线衰减的越厉害,只有当视线方向与反射光线方向非常接近时才能看到镜面反射的高光现象,此时,镜面反射光将会在反射方向附近形成亮且小的光斑;$n_s$ 越小,表示物体越粗糙,反射光分散,亮点大,强度弱。

反射光的方向 $R$ 可以通过入射光方向 $L$(从顶点指向光源)和物体法向量 $N$求出:

$$R+L=(2N \bullet L)N$$

同时推导出:
$$R=(2N \bullet L)N-L$$

phong光照模型片段着色实现参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct VertexIn
{
float4 position : POSITION;
float4 normal : NORMAL;
};

struct VertexScreen
{
float4 oPosition : POSITION;
float4 objectPos : TEXCOORD0;
float4 objectNormal : TEXCOORD1;
};

// 顶点着色器
void main_v(VertexIn posIn,
out VertexScreen posOut,
uniform float4x4 modelViewProj)
{
posOut.oPosition = mul(modelViewProj, posIn.position);
posOut.objectPos = posIn.position;
posOut.objectNormal = posIn.normal;
}

// 片段着色器
void main_f( VertexScreen posIn,
out float4 color : COLOR,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float3 globalAmbient,
uniform float3 eyePosition,
uniform float3 lightPosition,
uniform float3 lightColor,
uniform float3 Kd,
uniform float3 Ks,
uniform float shininess)
{
float3 worldPos = mul(worldMatrix, posIn.objectPos).xyz;
float3 N = mul(worldMatrix_IT, posIn.objectNormal).xyz;
N = normalize(N);

// 计算入射光方向、视线方向、反射光线方向
float3 L = normalize(lightPosition - worldPos);
float3 V = normalize(eyePosition - worldPos);
float3 R = 2*max(dot(N, L), 0)*N-L;
R = normalize(R);

// 计算漫反射分量
float3 diffuseColor = Kd * globalAmbient+Kd*lightColor*max(dot(N, L), 0);

// 计算镜面反射分量
float3 specularColor = Ks * lightColor*pow(max(dot(V, R), 0), shininess);

color.xyz = diffuseColor + specularColor;
color.w = 1;
}

Blinn-Phong

Blinn-Phong 光照模型, 又称为 Blinn-phong 反射模型( Blinn–Phong reflectionmodel)或者 phong 修正模型( modified Phong reflection model)。和传统 phong 光照模型相比,Blinn-phong 光照模型混合了 Lambert 的漫射部分和标准的高光,渲染效果有时比 Phong 高光更柔和、更平滑,此外它在速度上相当快,因此成为许多 CG 软件中的默认光照渲染方法。此外它也集成在了大多数图形芯片中,用以产生实时快速的渲染。在 OpenGL 和 Direct3D 渲染管线中, Blinn-Phong 就是默认的渲染模型

phong 光照模型中,必须计算 $V•R$ 的值,其中 $R$ 为反射光线方向单位向量,$V$ 为视线方向单位向量,但是在 Blinn-phong 光照模型中,用$N•H$的值取代了$V•R$。 Blinn-phong 光照模型公式为:

$$I_{spec}=K_{s}I_{l}(N•H)^{n_{s}}$$

$N$是入射点的单位法向量, $H$ 是“光入射方向 $L$ 和视点方向V 的中间向量”,通常也称之为半角向量。注意:半角向量被广泛用于各类光照模型,原因不但在于半角向量蕴含的信息价值,也在于计算半角向量是一件简单、耗时不多的工作。

$$H=\dfrac{L+V}{|L+V|}$$

blinn-phong光照模型片段着色实现参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct VertexIn
{
float4 position : POSITION;
float4 normal : NORMAL;
};

struct VertexScreen
{
float4 oPosition : POSITION;
float4 objectPos : TEXCOORD0;
float4 objectNormal : TEXCOORD1;
};

// 顶点着色器
void main_v(VertexIn posIn,
out VertexScreen posOut,
uniform float4x4 modelViewProj)
{
posOut.oPosition = mul(modelViewProj, posIn.position);
posOut.objectPos = posIn.position;
posOut.objectNormal = posIn.normal;
}

// 片段着色器
void main_f(VertexScreen posIn,
out float4 color : COLOR,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float3 globalAmbient,
uniform float3 eyePosition,
uniform float3 lightPosition,
uniform float3 lightColor,
uniform float3 Kd,
uniform float3 Ks,
uniform float shininess)
{
float3 worldPos = mul(worldMatrix, posIn.objectPos).xyz;
float3 N = mul(worldMatrix_IT, posIn.objectNormal).xyz;
N = normalize(N);

// 计算入射光方向\视线方向\半角向量
float3 L = normalize(lightPosition - worldPos);
float3 V = normalize(eyePosition - worldPos);
float3 H = normalize(L + V);

// 计算漫反射分量
float3 diffuseColor = Kd * globalAmbient+Kd*lightColor*max(dot(N, L), 0);

// 计算镜面反射分量
float3 specularColor = Ks * lightColor*pow(max(dot(N, H), 0), shininess);

color.xyz = diffuseColor + specularColor;
color.w = 1;
}

Cook-Torrance

使用 phong 和 blinn-phong 光照模型渲染出来的效果都存在一个问题:效果过于艺术化,不太真实。这是因为这两种模型都对材质细节方面没有进行考虑。

Cook-Torrance 光照模型将物体粗糙表面( rough surface)看作由很多微小平面(微平面)组成,每一个微平面都被看作一个理想的镜面反射体,物体表面的粗糙度由微平面斜率的变化来衡量。 一个粗糙表面由一系列斜率变化很大的微平面组成,而在相对平滑的表面上微平面斜率变化较小。

Cook-Torrance 模型将光分为两个方面考虑:漫反射光强和镜面反射光强。如公式:

$$I_{c-t}=I_{diff}+I_{spec}=I_{diff}+k_sI_lR_s$$

$I_{diff}$ 是漫反射光强,$k_sI_lR_s$ 是镜面反射光强的计算方法。从公式可以看出: cook-Torrance 模型与 phong、blinn-phong 模型的不同之处在于 $R_s$ 的计算方法。实际上, cook-Torrance、 phong和 blinn-phong 三种光照模型的本质区别都在于“使用不同数学表达式计算 $R_s$ ”。$R_s$在英文中称之为“specular term”。

$$R_s=\dfrac{FDG}{V•N}$$

$$R_s=\dfrac{FDG}{(V•N)*(N•L)}$$

$F$ 是 Fresnel 反射系数( Fresnel reflect term),表示反射方向上的光强占原始光强的比率; $D$ 表示微平面分布函数( Beckmann distribution factor),返回的是“给定方向上的微平面的分数值”; $G$ 是几何衰减系数( Geometric attenuationterm),衡量微平面自身遮蔽光强的影响。 $N$ 、$V$ 、$L$ 分别表示法向量、视线方向(从顶点到视点)和入射光方向(从顶点向外)。

$$F=f_0+(1-f_0)(1-V•H)^5$$

$f_0$ 为入射角度接近 0(入射方向靠近法向量)时的 Fresnel 反射系数, $V$ 是指向视点的向量, $H$ 为半角向量。

微平面分布函数:根据给定的半角向量 $H$,微平面分布函数返回微平面的分数值。最常使用的微平面分布函是 Backmann 分布函数:

$$D=\dfrac{1}{m^2cos^4α}e^{-\dfrac{tan^2α}{m^2}}$$

$m$ 值用于度量表面的粗糙程度,较大的 $m$ 值对应于粗糙平面,较小的 $m$ 值对应与较光滑的表面; $α$ 是顶点法向量 $N$ 和半角向量 $H$ 的夹角。其中

$$-\dfrac{tan^2α}{m^2}=-\dfrac{\dfrac{1-cos^2α}{cos^2α}}{m^2}=\dfrac{cos^2α-1}{m^2cos^2α}=\dfrac{(N•H)^2-1}{m^2(N•H)^2}$$

所以 Backmann 微平面分布函数的最终数学表达为公式:

$$D=\dfrac{1}{m^2cos^4α}e^{\dfrac{(N•H)^2-1}{m^2(N•H)^2}}=\dfrac{1}{m^2(N•H)^4}e^{\dfrac{(N•H)^2-1}{m^2(N•H)^2}}$$

微平面上的入射光,在到达一个表面之前或被该表面反射之后,可能会被相邻的微平面阻挡,未被遮挡的光随机发散,最终形成了表面漫反射的一部分。这种阻挡会造成镜面反射的轻微昏暗,可以用几何衰减系数来衡量这种影响。

微平面上反射的光可能出现三种情况:入射光未被遮挡,此时到达观察者的光强为 1;入射光部分被遮挡;反射光部分被遮挡。几何衰减系数被定义为:到达观察者的光的最小强度。所以:

$$G=min(1, G1, G2)$$

$$G1=\dfrac{2(N•H)(H•L)}{V•H}$$

$$G2=\dfrac{2(N•H)(N•V)}{V•H}$$

由此, Cook-Torrance 光照模型的 specular term 的最终数学表达为:

$$R_s={\dfrac{(f_0+(1-f_0)(1-V•H)^5){\dfrac{1}{m^2(N•H)^4}e^{\dfrac{(N•H)^2-1}{m^2*(N•H)^2}}min(1,{\dfrac{2(N•H)(H•L)}{V•H}},{\dfrac{2(N•H)(N•V)}{V•H}})}}{V•N}}$$

$$I_{c-t}=I_{diff}+I_{spec}=k_dI_l(N•L) + k_sI_lR_s$$

Cook-Torrance 光照模型着色程序参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 顶点着色器
void main_v( float4 position : POSITION,
float4 normal : NORMAL,
out float4 oPosition : POSITION,
out float3 worldPos : TEXCOORD0,
out float3 oNormal : TEXCOORD1,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float4x4 worldViewProj)
{
oPosition = mul(worldViewProj, position);
worldPos = mul(worldMatrix, position).xyz;
oNormal = mul(worldMatrix_IT,normal).xyz;
oNormal = normalize(oNormal);
}

// 片段着色器
void main_f(float3 position : TEXCOORD0,
float3 normal : TEXCOORD1,
out float4 color : COLOR,
uniform float3 globalAmbient,
uniform float3 lightColor,
uniform float3 lightPosition,
uniform float3 eyePosition,
uniform float3 Ka,
uniform float3 Kd,
uniform float3 Ks,
uniform float f,
uniform float m)
{
float3 P = position.xyz;
float3 N = normalize(normal);
float3 ambient = Ka * globalAmbient; // 计算环境光分量
float3 L = normalize(lightPosition - P);
float nl = max(dot(L, N), 0);
float3 diffuse = Kd * lightColor * nl; // 计算漫反射光分量
float3 V = normalize(eyePosition - P);
float3 H = normalize(L + V);
float3 specular = float3(0.0,0.0,0.0);
float nv = dot(N,V);
bool back = (nv>0) && (nl>0);

if(back)
{
float nh = dot(N,H);
float temp = (nh*nh-1)/(m*m*nh*nh);
float roughness = (exp(temp))/(pow(m,2)*pow(nh,4.0)); //粗糙度,根据 beckmann 函数
float vh = dot(V,H);
float a = (2*nh*nv)/vh;
float b = (2*nh*nl)/vh;
float geometric = min(a,b);
geometric = min(1,geometric); //几何衰减系数
float fresnelCoe=f+(1-f)*pow(1-vh,5.0); //fresnel 反射系数
float rs = (fresnelCoe*geometric*roughness)/(nv*nl);
specular = rs * lightColor * nl*Ks; // 计算镜面反射光分量(这是重点)
}

color.xyz = ambient + diffuse + specular;
color.w = 1;
}

BRDF

双向反射分布函数。该函数描述了入射光线在非透明物体表面如何进行反射。

BRDF 的结果是一个没有单位的数值,表示在给定入射条件下,某个出射方向上反射光的相对能量,也可以理解为“入射光以特定方向离开的概率”。

$$f_r(w_i,w_o)=\dfrac{dL_r(w_o)}{dE_i(w_i)}=\dfrac{dL_r(w_o)}{L_i(w_i)cosθ_idw_i}$$

$w_i$ 表示光线入射方向, $w_o$ 表示光线出射方向(入射点到视点),$L_r(w_o)$ 表示从 $w_o$ 方向反射的光线的辐射亮度( Radiance);$E_i(w_i)$表示从 $w_i$ 方向入射的光线在辐射照度( Irradiance)。

辐射亮度和辐射照度是表示光照性质的光学量, 辐射亮度是每单位立体角在垂直于给定方向的平面上的单位正投影面积上的功率。辐射照度则是整个入射表面的功率,等于投射在包括该点的一个面元上的辐射通量 $dφ$ 除以该面元的面积 $dA$。故而,从物理光学上我们可以将公式理解为: BRDF 函数计算的是“特定反射方向的光强与入射光强的比例”。

Bank BRDF

Bank BRDF 属于经验模型,由于其计算简单,且效果良好,所以该模型在各向异性光照效果的模拟方面非常有用。 Bank BRDF 的镜面反射部分可以表达为公式:

$$f=k_s(\sqrt{1-(L•T)^2}\sqrt{1-(V•T)^2})-(L•T)(V•T))^{n_s}$$

$k_s、n_s$分别表示镜面反射系数和高光系数;$L$ 表示入射光线方向、$V$ 表示实
现观察方向、$T$ 表示该点的切向量。尤其要注意切向量的计算方法,因为一个三
维空间点可能存在无数个切向量, 通常我采用“顶点的法向量和视线方向做叉积,
其结果作为 $T$。

Bank BRDF参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 顶多着色器
void main_v(float4 position : POSITION,
float4 normal : NORMAL,
out float4 oPosition : POSITION,
out float3 worldPos : TEXCOORD0,
out float3 worldNormal : TEXCOORD1,
uniform float4x4 modelViewProj,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT)
{
oPosition = mul(modelViewProj, position);
worldPos = mul(worldMatrix, position).xyz;
worldNormal = mul(worldMatrix_IT, normal).xyz;
}

// 片段着色器
void main_f(float4 position : TEXCOORD0,
float3 normal : TEXCOORD1,
out float4 color : COLOR,
uniform float3 globalAmbient,
uniform float3 lightColor,
uniform float3 lightPosition,
uniform float3 eyePosition,
uniform float3 Ka,
uniform float3 Kd,
uniform float3 Ks,
uniform float shininess)
{
float3 P = position.xyz;
float3 N = normalize(normal);
float3 ambient = Ka * globalAmbient; //计算环境光分量
float3 L = normalize(lightPosition - P);
float ln = max(dot(L, N), 0);
float3 diffuse = Kd * lightColor *ln; // 计算有向光漫反射分量

// 计算镜面反射分量
float3 V = normalize(eyePosition - P);
float3 H = normalize(L + V);
float3 specular = float3(0.0,0.0,0.0);
bool back = (dot(V,N)>0) && (dot(L,N));

if(back)
{
float3 T = normalize(cross(N,V)); //计算顶点切向量
float a = dot(L,T);
float b = dot(V,T);
float c = sqrt(1-pow(a,2.0))* sqrt(1-pow(b,2.0)) - a*b; //计算 Bank BRDF 系数
float brdf = Ks* pow(c, shininess);
specular = brdf * lightColor *ln;
}

color.xyz = ambient + diffuse + specular;
color.w = 1;
}