[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30

@[C++]DirectX 12 3D游戏开发实战(DX12龙书)
开始尝试使用****来对每天的学习进行记录,记录内容主要为书中的概念摘录与个人理解,由于本人代码知识比较浅薄,在理解上可能会有偏差,欢迎指正,谢谢!
仅作为个人学习用,请勿转载,谢谢

第8章 光照

[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
光照使我们感受到目标物体的实体形状,展现体积感,视觉上对世界感知依靠光照与其材质的交互。

词汇

光照:lighting 阴影:shading 材质:Material 光照模型:lighting model 光照方程:lighting equation
强度:intensity 介质(媒质):medium 三色论:trichromatic theory 散射:scatter
半光亮:semi-bright 局部光照模型:local illumination model 照明:illumination
全局关照模型:global illumination model 平面法线:face normal 法向量:normal vector
曲面法线:surface normal 顶点法线:vertex normal 逐像素光照:per pixel lighting
逐顶点光照:pre vertex lighting 求顶点法线平均值:vertex normal averaging
切向量:tangent vector 剪切变换:shear transformation
逆:inverse 行列式:determinant 反映:reflect 辐射通量 radiant flux
辐(射)照度:irradiance 漫反射:diffuse reflection 漫反射反照率:diffuse albedo
漫反射率:diffuse reflection 环境光:ambient light 菲涅尔效应:Fresnel effect
折射率:index of refraction 折射:refract 镜面反射:specular reflection 镜面光:specular light
斯涅尔折射定律:Snell’s Law 菲涅尔方程:Fresnel equations
石里克近似法:Schlick approximation 透射光:transmitted light 本体反射率:body reflectance
理想镜面:perfect mirror 粗糙度roughness 微观表面法线:micro-normal
宏观表面法线:macro-normal 镜面瓣:specular lobe 微平面:microfacet
中间向量:halfway vector 光泽度:shininess 粒度:granularity

学习目标

1.对光照与材质的交互有基本的了解。
2.了解局部光照与全局光照之间的差异。
3.探究如何用数学描述位于物体表面上某点的朝向,以此来确定入射光找到表面的角度。
4.学会如何正确的变换法向量。
5.区分环境光/漫反射光/镜面光。
6.学习如何实现平行光/点光/聚光3种光源。
7.理解通过控制距离函数的衰减参数实现不同的光照强度。

内容

8.1光照与材质的交互

1.不再直接指出顶点颜色,而采用指定材质与光照,再运用光照方程基于两者的交互来计算顶点颜色,使物体的颜色更趋于现实。
2.将材质看作确定光照与物体表面如何进行交互的属性集,其中包含:表面反射光和吸收光的颜色、表面下材质的折射率、表面的光滑度及表面的透明度。
3.光源可以发出各种强度的红绿蓝三色混合光来模拟多种光源颜色。
4.光线经过不断反射最终传入观察者眼中并照射到视网膜上的感光细胞。
5.三色论百度百科链接
6.本书中及大多数实时应用程序采用的光照模型为局部光照模型:每个物体的光照皆独立于其他物体,在光照过程中仅考虑光源直接发出的光线(忽略来自其它物体的反射光)。
7.全局光照模型:对一个物体进行照明时考虑整个场景中的所有事物,开销较大,不适合实时游戏,但可以生成接近照片级真实感的场景。
8.接近全局关照的实时方法仍处于研究阶段例如立体像素全局光照(voxel global illumination)技术 参考文献:Practical Real-Time Voxel-Based Global Illumination for Current GPUs
9.另一种方法是预计算静态物体的间接光照,再用得到的结果来近似地模拟动态物体的间接关照
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
之前在使用UE4时,天真的我以为把全部光源关闭,并且使场景的四周被墙体包围就可以实现一个漆黑的场景,现在想想是不是和局部光照模型的使用有关?

8.2法向量

1.平面法线是一种描述多边形朝向的的单位向量
2.曲面法线是一种垂直于曲面上一点处切平面的单位向量,根据曲面法线即可确定对于曲面上某点的朝向

[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
3.对于光照计算来说,需要通过三角形网格曲面上每一点出的曲面法线来确定光线照到对应点上的角度
4.为了求出曲面法线,我们仅先指定位于网格顶点处的曲面法线(顶点法线),接下来为了取得三角形网格曲面上每个点处的近似曲面法线,在三角形光栅化过程中对这些顶点发现进行插值计算(利用三角形顶点的属性值计算出其内部像素的属性值)
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
5.逐像素光照(phong关照)模型:对每一个像素逐一进行法线插值并执行光照计算的方法
6.逐顶点光照模型:针对每个顶点逐一进行光照计算的方法,开销低精度也低,接下来顶点着色器输出每个顶点光照的计算结果,再对三角形中的像素进行插值。
7.将运算作业从像素着色器移至顶点着色器是一种常见的性能优化手段,尤其在以质量为重但又允许结果存在少许视觉偏差的情况下

8.2.1计算法向量

1.为找到三角形p0,p1,p2的平面法线,我们首先计算位于三角形边上的两个向量
u=p1p0u=p1-p0
v=p2p0v=p2-p0
三角形的平面法线为
n=uv/uvn=u*v/|u*v|
计算三角形的三个顶点来计算该三角形正面的平面法线函数:

XMVECTOR ComputeNormal(FXMVECTOR p0,FXMVECTOR p1,FXMVECTOR p2)
{
	XMVECTOR u = p1 - p0;
	XMVECTOR v = p2 - p0;
	return XMVector3Normalize(XMVector3Cross(u,v));
}

2.三角形网格使用一种被称为“求顶点法线平均值”的计算方法:通过对网格*享顶点v的多边形平面法线求取平均值,从而获得网格中任意顶点v处的顶点法线n。
navg=(n0+n1+n2+n3)/n0+n1+n2+n3navg=(n0+n1+n2+n3)/|n0+n1+n2+n3|
3.还可以采取更加复杂的求平均值方法,比如根据多边形面积确定权重以求取加权平均值。

//输入
//1.一个顶点数组(mVertices)。每个顶点都有一个位置分量(pos)和一个法线分量(normal)
//2.一个索引数组(mIndices)
//对于网格中的每个三角形
for(UINT i = 0; i < mNumTriangles; ++i)
{
	//第i个三角形的索引
	UINT i0 = mIndices[i*3+0];
	UINT i1 = mIndices[i*3+1];
	UINT i2 = mIndices[i*3+2];
	//第i个三角形的顶点
	Vertex v0 = mVertices[i0];
	Vertex v1 = mVertices[i1];
	Vertex v2 = mVertices[i2];
	//计算平面法线
	Vector3 e0 = v1.pos - v0.pos;
	Vector3 e1 = v2.pos - v0.pos;
	Vector3 faceNormal = Cross(e0, e1);
	//该三角形共享了下面3个顶点,所以将此平面法线与这些顶点的法线相加以求取平均值
	mVertices[i0].normal += faceNormal;
	mVertices[i1].normal += faceNormal;
	mVertices[i2].normal += faceNormal;
}
//对于每个顶点v来说,由于我们已经对所有共享顶点v的三角形 平面法线进行求和,
//因此现在只需要进行规范化处理即可
for(UINT i = 0; i < mNumVertices; ++i)
	mVertices[i].normal = Normalize(&mVertices[i].normal);

8.2.2变换法向量

[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
1.切向量u=v1-v0正交于法向量n,如果对此应用一个非等比缩放变换A,则变换后的uA=v1A-v0A没能继续保持与变换后法向量nA的正交关系,因此
通过B=(A-1)T(矩阵A的逆转置矩阵)对法向量进行变换后,即可使它垂直与矩阵经A变换后的切向量uA。
2.当需要对经过非等比变换或剪切变换后的法向量进行变换时,则可使用逆转置矩阵。
MathHelper.h中计算逆转置矩阵的辅助函数

static XMMATRIX InverseTranspose(CXMMATRIX M)
{
	XMMATRIX A = M;
	A.r[3] =XMVectorSet(0.0f,0.0f,0.0f,1.0f);
	XMVECTOR det = XMMatrixDeterminant(A);
	return XMMatrixTranspos(XMMatrixInverse(&det,A));
}

3.再通过逆转置矩阵对向量进行变换时,可以将向量变换矩阵中与平行操作有关的项清零,而只允许点类才有平移变换。将向量的第4个分量设置为w=0,就可以防止向量因平移操作而受到影响,因此就无需再为矩阵中的平移项设置零。但是如果再连接逆转置矩阵以及另一个不含非等比缩放的矩阵,如观察矩阵(A-1TV,那么(A-1)T中经转置后的第4列平移项将进入最终的乘积矩阵而导致错误的计算结果,因此对矩阵中的平移项设置零是这个错误的预防措施。而法线变换所采用的正确公式是((AV)-1)T

A=[100000.500000.501111] A=\begin{bmatrix} 1 &amp; 0 &amp; 0 &amp;0\\ 0 &amp; 0.5 &amp;0&amp;0 \\ 0 &amp; 0 &amp; 0.5 &amp;0 \\ 1 &amp; 1 &amp; 1 &amp;1 \end{bmatrix}
(A1)T=[1001020200220001] (A^{-1})^T=\begin{bmatrix} 1 &amp; 0 &amp; 0 &amp;-1\\ 0 &amp; 2 &amp;0&amp;-2\\ 0 &amp; 0 &amp; 2 &amp;-2\\ 0 &amp; 0&amp; 0 &amp;1 \end{bmatrix}
*运用逆转置变换后的法相可能会失去单位长度,所以变换完成后可能需要进行规范化处理

8.3参与光照计算的一些关键向量

[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30

对朗伯余弦定律:向量L用于计算L·n=cosθi,cosθi是光向量L与法向量的夹角,反射向量r是入射光向量L关于法相n的镜像,观察向量v=normalize(E-p)是从表面上的点p到观察点E方向上的单位向量,它定义了由观察点向表面点观察的视线,我们有时还要用到向量-v,它是我们需要计算的由观察点到表面点这条光线路径上的单位向量。
反射向量r=I-2(n·I)n。着色器中实际上是利用内置函数reflect来计算r
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30

8.4朗伯余弦定理

1.辐射通量:光源每秒发出的能量
2.辐(射)照度:单位面积上的辐射通量密度,以此来确定某区域所接收到的光量,或是通过空间中某假想区域的光量
3.垂直照射到表面的面积为A1A_1,则A1内的辐照度为E1=PA1E_1=\frac P{A_1},转动光源,使光束以某个入射角度照射表面,光束覆于表面的面积为A2,此时的辐照度E2=PA2E_2=\frac P{A_2},则A1和A2的关系为
cosθ=A1A21A2=cosθA1cosθ=\frac {A_1} {A_2}→\frac1 A_2=\frac{cosθ}{A_1}
因此
E2=PA2=PA1cosθ=E1cosθ=E1(nL)E_2=\frac P {A_2}=\frac P {A_1}cosθ=E_1cosθ=E_1(n·L)
面积A_2内的辐照度相当于将受垂直方向光照的面积A1A_1内的辐照度按比例nL=cosθn·L=cosθ进行缩放,即朗伯余弦定律(Lamber’s Cosine Law)。考虑到光线照射到表面另一侧的情况(此时点积的结果为负值),用max来钳制(clamp,将某值限定在特定范围之内)缩放因子的取值范围:
f(θ)=max(cosθ0)=max(Ln0)f(θ)=max(cosθ,0)=max(L·n,0)
随着θ的变化,函数的值域为0.0—1.0(光照强度的变化范围为0%~100%)。
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30

8.5漫反射光照

[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
1.漫反射:光线照到表面上某一点时,一部分光会进入物体内部,并于表面附近的物质相互作用。这些光在物体内部四处反弹,一部分会被吸收,余下部分会向各个方向散射并返回表面的现象。
2.为了方便我们假设光在入射点出发生散射,并且光的吸收和散射程度与物体的材质密切相关,在光照与材质交互的近似模型中,规定光线会在表面的所有方向均匀散射,因此无论在哪个观察点进行观察,反射光都会进入观察者的眼中。所以可以不考虑观察点的具体位置而表面上的颜色在任何观察点上看也是相同的。
3.将漫反射光照的计算分为两部分
指定光照颜色及漫反射反照率颜色,漫反射反照率表示的是根据表面的漫反射率而被反射的入射光量。要对它们处理就要使用分量式颜色乘法。
例如假设表面上一点会反射50%的入射红光,100%的绿光以及75%的蓝光,如果这时有一束强度为80%的白光,那么此入射光的量值就可以表示为BL=(0.8,0.8,0.8)B_L=(0.8,0.8,0.8),又因为漫反射照率为md=(0.5,1.0,0.75)m_d=(0.5,1.0,0.75),此时反射光量为:
cd=BLm4=(0.8,0.8,0.8)(0.5,1.0,0.75)=(0.4,0.8,0.6)c_d=B_L\otimes m_4=(0.8,0.8,0.8)\otimes (0.5,1.0,0.75)=(0.4,0.8,0.6)
漫反射反照率必定在0.0到1.0之间,因此我们用小数来表示反射光。
4.再将朗伯余弦定律考虑在内,设BLB_L为入射光量,mdm_d为漫反射反照率,LL为光向量,而n为表面法线,则位于表面上某点出的反射光量为:
Cd=max(Ln,0)BLm4C_d=max(L·n,0)·B_L\otimes m_4

8.6环境光照

1.真实世界中的多是间接光,为了处理这种见解光照,因此为光照方程引进一个环境光项:
Ca=ALm4C_a=A_L\otimes m_4颜色ALA_L指定了表面接收到的简介光量,它可能与光源发出的光量不同,因为光源发射在其它表面反射的时候会被吸收一部分,漫反射光照m4m_4指示了根据表面漫反射率而被表面反射的入射光量,也借此来表明被表面反射的入射环境光量。
2.环境光是以统一亮度将物体稍稍照亮,而非按照真实的物理效果进行计算。
3.思路:间接光照在场景中会发生多次散射与反射,并会在所有方向上均匀的射向目标物体。

8.7镜面光照

1.菲涅尔效应:当光线到达两种不同折射率介质之间的界面时,一部分光被反射,而剩下的光发生折射。
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
2.事实图像处理中一般用alpha混合技术或后期处理特效来模拟透明对象的折射过程。
3.镜面光照的计算与观察者有关,随着观察者的位置在场景中移动,他所收到的镜面光量也会随之发生改变。

8.7.1菲涅尔效应

1.菲涅尔方程以数学方法描述了入射光线被反射的百分比,即 0RF10 \leqslant R_F \leqslant 1,如果RFR_F是反射光量,则(1RF)(1-R_F)为折射光量,RFR_F的值的一个RGB向量。
2.反射光量既依赖介质,也与法向量n与光向量L之间的夹角θ有关。一般不会将完整的菲涅尔方程用于实时渲染,而是以石里克近似法来代替:
RF(θi)=RF(0o)+(1RF(0o))(1cosθ)5R_F(\theta_i)=R_F(0^o)+(1-R_F(0^o))(1-cos\theta)^5
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
3.菲涅尔效应:反射光量取决于材质(RF(0)oR_F(0)^o)以及法线与光向量之间的夹角。
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
4.金属会吸收透射光,因此它们不具有本体反射率(?)但金属并不会看上去表现为纯黑色。因为它们的RF(0o)R_F(0^o)值较高,因此即使接近0°的入射光它们也可以反射客观的镜面光量

8.7.2表面粗糙度

1.理想镜面的粗糙度为0,随着粗糙度的增加,微观表面的法线方向开始偏离宏观表面法线从而使反射光逐渐扩展为一个镜面瓣
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
2.使用微平面模型对粗糙度进行建模,将微观表面模拟为由多个既微小又平滑的微平面所构成的几何,微观表面法线就是这些微平面上的法线。
3.计算法线为h=normalize(L+v)h=normalize(L+v)这种微平面片段在所有微平面中所占的比例,来确定多少光通过镜面反射的方式由此路径进入观察者眼中,发生由L到v反射过程的微平面越多,则观察者在此角度上看到的镜面光越明亮
4.ρ(θh)\rho (\theta_h)的一种流行的可控函数为
ρ(θh)=cosm(θh)=cosm(nh)\rho(\theta_h)=cos^m(\theta_h)=cos^m(n·h)
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
5.将ρ(θh)\rho (\theta_h)与某种归一化因子进行组合获得基于粗糙度来模拟镜面反射光量的新函数:
S(θh)=m+88cosm(θh)=m+88(nh)mS(\theta_h)=\frac {m+8} 8 cos^m(\theta_h)=\frac {m+8} 8(n·h)^m
[C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
6.归一化因子m+8m\frac{m+8}m控制函数的高度,随m变化镜面瓣变宽或变窄使得光能整体达到守恒
7.光滑的表面使用较大m值,粗糙表面则使用较小m值
8.将反射光量RF(ah)R_F(a_h)乘以粗糙度便可得到镜面反射光量,设(max(Ln,0)BL)(max(L·n,0)·B_L)表示入射光照射到表面上一点的光量,而根据菲涅尔效应,实际反射到观察者眼中的实际光量为:
cs=max(Ln0)BLRF(ah)m+88(nh)mc_s=max(L·n,0)·B_L\otimes R_F(a_h)\frac{m+8}8(n·h)^m
如果Ln0L·n\leqslant0,那么所计算的结果是射向表面另一侧的光,因而此时正表面不会接收任何光照。
##8.8光照模型的概述
1.结合先前所述,表面反射光量相当于环境反射光、漫反射光以及镜面反射光的总和。

  1. 环境光CaC_a:模拟经表面反射的间接光量
  2. 漫反射光CdC_d:假设在表面下与介质相互作用后的光进入表面处返回,并向各个方向均匀散射
  3. 镜面光CSC_S:模拟经菲涅尔效应与表面粗糙度共同作用的表面反射光。
    由此推导出光照方程:
    LitColor=Ca+Cd+CSLitColor=C_a+C_d+C_S
    =ALmd+max(Ln,0)BLmd+RF(ah)m+88(nh)m)=A_L\otimes m_d+max(L·n,0)·B_L\otimes\lgroup m_d+R_F(a_h)\frac{m+8}8(n·h)^m)\rgroup
    设式中所有向量均为单位长度
    1.L:指向光源的光向量
    2.n:表面法线
    3.h:光向量与观察向量间的中间向量
    4.ALA_L:入射的环境光量
    5.BLB_L:入射的直射光量
    6.mdm_d:根据表面漫反射率而反射的入射光量
    7.L·n:朗伯余弦定律
    8.aha_h:中间向量与光向量L之间的夹角
    9.RF(ah)R_F(a_h):根据菲涅尔效应,关于中间向量h所反射到观察者眼中的光量
    10.m:控制表面的粗糙度
    11.(nh)m(n·h)^m:指定法线h与宏观表面法线n之间夹角为θh\theta_h的所有微平面片段
    12.m+88\frac{m+8}8:镜面反射过程中为模拟能量守恒所采用的归一化因子
    [C++]DirectX 12 3D游戏开发实战—第8章 学习笔记01 2019.4.30
    *这仅是一种通用且流行的光照方程,还有一些其它的光照模型

8.9材质的实现

d3dUtil.h文件中

//演示程序中表示材质的简单结构体
struct Material
{
	//便于查找材质的唯一对应名称
	std::string Name;
	//本材质的常量缓冲区索引
	int MatCBIndex = -1;
	//漫反射纹理在SRV(着色器资源视图)堆中的索引。在第9章纹理贴图时会用到
	int DiffuseSrvHeapIndex = -1;
	//已更新标志(dirtyflag)表示本材质已有变动,而我们也就需要更新常量缓冲区了。
	//由于每个帧资源都有一个材质常量缓冲区,所以必须对每个帧资源进行更新,
	//因此当修改某个材质时,都应该设置NumFramesDirty = gNumFrameResources,
	//以使每个帧资源都得到更新
	int NumFramesDirty = gNumFrameResources;
	DirectX::XMFLOAT4 DiffuseAlbedo = {1.0f,1.0f,1.0f,1.0f};
	DirectX::XMFLOAT3 FresnelR0 = {0.01f,0.01f,0.01f};
	float Roughness = 0.25f;
	DirectX::XMFLOAT4x4 MatTransform = MathHelper::Indentity4x4();
}

1.为了模拟真实世界的材质,需要设置DiffuseAlbedo(漫反射反照率)和FresnelR0(材质属性),再辅以一些关乎艺术性的细节调整。例如,金属导体吸收了进入金属内部的折射光而因此金属材质将不会发生漫反射,但实际上不会完全按照物理学的光照理论进行建模而是稍加调整,一种方法是把DiffuseAlbedo设置为一个非0的较小值,适当权衡获得更佳视觉效果。
2.将粗糙度归一化为0到1之间的值,着色器代码中使用粗糙度推导公式中的指数m。
3.粗糙度与光泽度是一对相反的属性shininess=1roughness[0,1]shininess = 1 - roughness\in[0,1]
4.材质具体数值可能随着表面而发生改变
5.上述问题的其一解决方案是以每个顶点为基准来指定材质的具体数值、在三角形的光栅化处理期间,会对这些顶点的材质属性进行插值计算,一丘处每一点的材质数值,但例程中每个顶点的颜色依然很粗糙,以致不能逼真模拟出较为丰富的细节。此外,为了绘制每个顶点的颜色还需向顶点结构体中添加额外数据,并需要采用不同的工具来绘制这些顶点颜色。更普遍的解决方法是采用纹理贴图,将在第9章讨论
6.本章在绘制调用时允许对材质进行频繁更改,因此为每一种材质定义了唯一的属性,并将它们列于一个表中: