第七章 光照

如图7.1左边,一个没加光照的球体,右边一个加光照的球体。可见,左边的球看起来是平的 - 也许它根本不是一个球,而只是一个二维圆。另一方面,右边的球体看上去是3D的-照明和阴影帮助我们感知物体的形状和体积。事实上,我们对世界的视觉感受依赖于光及其与物质的相互作用,因此,产生真实照片的场景的许多问题都与物理上精确的照明模型有关。

当然,模型越精确,计算量越大; 因此必须权衡效果和速度。如,用于电影的3D特效场景十分复杂,并且使用非常逼真的照明模型,因为电影的帧是预先渲染的,所以他们可以花费数小时或数天来处理帧。游戏, 另一方面是实时应用,因此,帧需要以至少30帧/秒的速率绘制。

注意,本书中解释和实现的照明模型主要基于[Möller02]中描述的模型。

目标:
1.对灯光和材料之间的相互作用有一个基本的了解。
2.了解局部光照与全局光照的区别。
3.掌握我们如何用数学方法描述表面上的一个点的方向是“面向”,以便我们可以确定入射光照到表面的角度。
4.学习如何正确转换法向量。
5.能够区分环境光,漫射光和镜面光。
6.学习如何实现定向灯,点灯和聚光灯。
7.了解如何通过控制衰减参数来改变作为深度函数的光强度。

7.1光和材料的相互作用

使用照明时,不再直接指定顶点颜色; 而是指定材质和灯光,然后应用一个光照方程,根据光线/材料的相互作用计算出顶点颜色。这使对象的更真实的着色(比较图7.1a和7.1b)。

7-1
图7.1 (a)未加光照的球体看起来是二维的 (b)加光照的球体看起来是3D的

材质可以被认为是决定光线如何与物体表面相互作用的属性。例如,表面反射和吸收的光的颜色,反射率,透明度和光泽度都是组成表面材料的参数。然而在本章中,我们只关心表面反射和吸收的光线的颜色,以及光泽度。

在我们的模型中,光源可以发出不同强度的红光,绿光和蓝光; 通过这种方式,我们可以模拟出多种颜色的光。当光线从光源向外传播并与物体发生碰撞时,一些光线可能被吸收,一些光线可能被反射(一些光线会通过透明物体,如玻璃,但我们在这里不考虑透明度)。反射光现在沿着新的路径传播,并可能撞击其他物体,在这些物体上再次吸收和反射一些光线。光线在被完全吸收之前可能撞击许多物体。一些光线最终会进入眼睛(见图7.2),并撞击视网膜上的感光细胞(称为视锥细胞和视杆细胞)。

7-2
图7.2 (a)入射白光的通量。 (b)光照射在圆柱体上,一些光线被吸收,其他光线被散射到眼睛和球体上。(c)从圆柱体向球体反射的光被再次吸收或反射并传播到眼睛中。(d)眼睛接收到的光线决定了眼睛看到的东西。

根据三色理论(见[Santrock03]),视网膜包含三种感光细胞,分别对应红,绿和蓝色光敏感(有一些重叠)。输入的RGB光根据光的强度以不同的强度刺激其相应的光感细胞。 当感光细胞被刺激(或不受刺激)时,神经冲动由视神经传给大脑,大脑根据光受体的刺激在脑中产生图像。(当然,如果你闭上你的眼睛,受体细胞就不会受到刺激,大脑就会将其记录为黑色。)

例如,再次考虑图7.2。假设圆柱体的材料反射75%的红光,75%的绿光,吸收其他所有光,球体反射25%的红光,吸收其他所有光。还假设从光源发出纯白光。当光线撞击圆柱体时,所有的蓝光都被吸收,只有75%的红光和绿光被反射(即中等强度的黄光)。这些光线是散射的 - 有些光线会传播到眼睛,有些则会传播到球体。进入眼睛的部分以一半强度主要刺激红色和绿色的锥形细胞;因此,观察者将该圆柱视为黄色的半明亮影像。现在,其他光线向球体传播并撞击。球体反射25%红光,吸收其余部分;因此稀释的入射红光(中高强度红光)被进一步稀释并反射,并且所有入射的绿光都被吸收。这剩余的红光然后进入眼睛,主要刺激红色的视锥细胞到一个较低的程度。因此,观众看到球体是一个深红的阴影。

和大多数实时应用一样,在本书中采用的照明模型被称为局部照明模型。在局部模型中,每个物体独立于另一个物体而被照亮,并且在照明过程中仅考虑从光源直接发出的光(即,从其他场景物体弹起后打在当前被照亮的物体的光是忽略)。图7.3显示了这个模型的一个结果。

相反,全局照明模型不仅考虑从光源直接发出的光线,还考虑到从场景中的其他物体反射回来的间接光线。 这些被称为全局照明模型,因为它们在照亮物体时考虑全局场景中的所有事物。全局照明模式通常对于实时游戏而言过于昂贵(但是非常接近于产生照片真实感的场景)。实时全局照明方法正在进行研究。

7-3
图7.3 在物理上,墙壁阻挡了灯泡发出的光线,球体在墙的阴影中。 然而,在一个局部照明模型中,球体像墙壁不在那里一样点亮

7.2 法向量

面法线是描述多边形面向的方向(即,与多边形上的所有点正交)的单位向量; 见图7.4a。表面法线是与表面上的点的切平面正交的单位矢量;请参见图7.4b。观察表面法线确定表面上的一个点“面对”的方向。

对于照明计算,我们需要每个三角形网格定点的表面法线,以便我们可以确定光照在网格表面上点的角度。为了获得曲面法线,我们仅在顶点处(即所谓的顶点法线)指定曲面法线。然后,为了在三角形网格的表面上的每个点上获得曲面法线近似,这些顶点法线将在光栅化期间在三角形内插(参见§5.10.3和图7.5)。

NOTE:对每个像素的法线进行插值计算称为像素照明或phong照明。 一个便宜但不太精确的方法是在每个顶点进行照明计算。 然后,从顶点着色器输出每个顶点光照计算的结果,并在三角形的像素内插。 从像素着色器到顶点着色器的移动计算是一个常见的性能优化的质量,有时视觉差异是非常微妙的,使这种优化非常有吸引力。

7-4
图7.4 (a)面法线正交于面上的所有点。 (b)曲面法线是与曲面上的一个点的切平面正交的向量。

7-5
图7.5 顶点法线n0和n1在分段顶点点p0和p1处定义。线段内部的点p的法线向量n通过顶点法线之间的线性内插(加权平均)来找到; 即n = n0 + t(n1-n0),其中t是使得p = p0 + t(p1-p0)。尽管为了简单起见,我们在一个线段上说明了正常的插值,但是这个想法直接推广到在三角形上进行插值。

7.2.1计算法向量

为了找到一个三角形Δp0p1p2 的面法线,我们首先计算两个位于三角形边缘上的向量:

u=p1p0v=p2p0

那么面向量为:
n=u×v||u×v||

以下是从三角形的三个顶点计算三角形正面(§5.10.2)的面法线的函数。
void ComputeNormal(const D3DXVECTOR3& p0,
                const D3DXVECTOR3& p1,
                const D3DXVECTOR3& p2,
D3DXVECTOR3& out)
{
    D3DXVECTOR3 u = p1 - p0;
    D3DXVECTOR3 v = p2 - p0;
    D3DXVec3Cross(&out, &u, &v);
    D3DXVec3Normalize(&out, &out);
}

对于可微分曲面,我们可以使用微积分来找到曲面上的点的法线。不幸的是,三角网格是不可微分的。通常应用于三角形网格的技术称为顶点法线平均。通过对共享顶点v的网格中的每个多边形的面法线进行平均来找到网格中任意顶点v的顶点法线n。例如,在图7.6中,网格中的四个多边形共享顶点v; 因此,v的顶点法线由下式给出:

narg=n0+n1+n2+n3||n0+n1+n2+n3||

7-6
图7.6 中间顶点由相邻的四个多边形共享,所以我们通过对四个多边形面法线进行平均来逼近中间顶点法线。

在前面的例子中,我们不需要像典型的平均值那样除以4,因为我们将结果归一化。还要注意,可以构建更复杂的平均方案; 例如,可以在权重由多边形的面积确定的情况下使用加权平均(例如,具有较大面积的多边形具有比具有较小面积的多边形更多的权重)。

下面的伪代码展示了如何在给定三角形网格的顶点和索引列表的情况下实现这种平均:

// Input:
// 1. An array of vertices (mVertices). Each vertex has a
// position component (pos) and a normal component (normal).
// 2. An array of indices (mIndices).

// For each triangle in the mesh:
for(UINT i = 0; i < mNumTriangles; ++i)
{
    // indices of the ith triangle
    UINT i0 = mIndices[i*3+0];
    UINT i1 = mIndices[i*3+1];
    UINT i2 = mIndices[i*3+2];
    // vertices of ith triangle
    Vertex v0 = mVertices[i0];
    Vertex v1 = mVertices[i1];
    Vertex v2 = mVertices[i2];
    // compute face normal
    Vector3 e0 = v1.pos - v0.pos;
    Vector3 e1 = v2.pos - v0.pos;
    Vector3 faceNormal = Cross(e0, e1);
    // This triangle shares the following three vertices,
    // so add this face normal into the average of these
    // vertex normals.
    mVertices[i0].normal += faceNormal;
    mVertices[i1].normal += faceNormal;
    mVertices[i2].normal += faceNormal;
}
// For each vertex v, we have summed the face normals of all
// the triangles that share v, so now we just need to normalize.
for(UINT i = 0; i < mNumVertices; ++i)
    mVertices[i].normal = Normalize(&mVertices[i].normal));

7.2.2法向量的转换

考虑图7.7a,其中我们有一个与法向量n正交的切向量u=v1v0 。 如果我们应用非均匀缩放变换A,我们从图7.7b可以看出,变换后的切向量uA=v1Av0A 并不保持与变换的法向量nA正交。

所以我们的问题是这样的:给定一个变换点和向量(非正态)的变换矩阵A,我们想要找到一个变换矩阵B来变换法向量,使得变换后的切向量与变换的法向量正交(即, uA·nB = 0)。 要做到这一点,首先让我们从我们知道的事情开始:我们知道法向量n正交于切向量u则有:

u · n =0 法向量与切向量正交
unT = 0 将点积写为矩阵乘法
u(AA-1) nT = 0 插入单位矩阵I=AA-1
(uA)(A–1 nT) = 0 矩阵乘法结合律
(uA) ((A–1 nT)T)T = 0 转置属性(AT)T=A
(uA) (n(A–1)T)T = 0 转置属性(AB)T=BTAT
uA · n(A–1)T = 0
uA · nB = 0 转换后的正切与方向向量正交

因此,B =(A-1)T(A的逆转置)在转换法向矢量方面做了工作,以使它们垂直于其相关的变换的切向量uA.

7-7
图7.7 (a)转换前的表面正常。 (b)在x轴上缩放2个单位后,法线不再与表面垂直。 (c)通过缩放变换的逆转置正确变换的曲面法线。

注意如果矩阵是正交的AT=A1 ,那么B =(A^{-1})^T =(A^T)^T = A; 也就是说,我们不需要计算逆转置,因为A在这种情况下完成了工作。 总之,当通过非均匀或剪切变换来变换法向矢量时,使用逆转置。

我们在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 XMMatrixTranspose(XMMatrixInverse(&det, A));
}

我们从矩阵中清除任何翻译,因为我们使用逆转置来转换向量,翻译仅适用于点。 然而,从§3.2.1我们知道设置w = 0的矢量(使用齐次座标)防止了矢量被翻译修改。 因此,我们不需要将矩阵中的翻译归零。 问题是如果我们想连接逆转置和不包含非均匀缩放的另一个矩阵,比如视图矩阵(A-1TV,(A-1T“第4列中的转置转换泄漏” 进入产品矩阵造成错误。 因此,为了避免这种错误,我们将翻译作为预防措施进行归零。 正确的方法是用((AV)-1)T来转换法线。以下是一个缩放和平移矩阵的例子,以及第四列不是[0,0,0,1]的逆转置看起来如何:

A=[100000.500000.501111](A1)T=[100000.500000.501111]

NOTE:即使进行逆转置转换,法向矢量也可能会丢失其单位长度; 因此,转型后可能需要重新归一化。

7.3兰伯特的“余弦法”

直射表面某点的光比斜射表面某点的光更强烈; 见图7.8。 考虑一小束横截面积为dA的入射光。

解决方案是想出一个函数,根据顶点法线和光线矢量的对齐返回不同的强度。(注意,光矢量是从表面到光源的矢量;也就是说,光矢量的方向与光线传播的方向相反)。当顶点法线和光线矢量完全对齐时,函数应该返回最大强度( 即它们之间的角度θ是0°),并且随着顶点法线与光线矢量之间的角度变化,其强度应该平滑地减小。 如果θ> 90°,那么光照到一个表面的背面,所以我们设置强度为零。兰伯特的余弦定律给出了我们所寻求的功能,这是由

f(θ)=max(cosθ,0)=max(L·n,0)

7-8
图7.8 考虑一个小面积元素dA。(a)当法向矢量n和光矢量L对齐时,区域dA接收最多的光。(b)随着n和L之间的角度θ增加,面积dA受到较少的光线(如经过表面dA的光线所示)。

其中L和n是单位矢量。 图6.9显示了f(θ)的曲线图,看看0.0到1.0(即0%到100%)范围内的强度是如何随θ变化的。

7-9
图7.9 对于-2≤θ≤2,函数f(θ)= max(cosθ,0)= max(L·n,0)的绘图。注意π/2≈1.57。

7.4 散射光

考虑一个粗糙的表面,如图7.10所示。当光照在这样一个表面上的一个点上时,光线会以各种随机方向散射; 这被称为漫反射。在我们对这种光/表面相互作用进行近似建模时,我们规定光在表面上方的所有方向均匀散射; 因此,无论视点位置如何,反射光都会到达。因此,我们不需要考虑视点(即漫射照明计算与视点无关),并且无论视点如何,表面上的点的颜色总是看起来相同。

我们将漫射照明的计算分为两部分。对于第一部分,我们指定漫反射光的颜色和漫反射材料的颜色。漫射材料指定表面反射和吸收的入射漫射光的量; 这是用分量色彩乘法处理的。例如,假设表面上的某个点反射了50%的入射红光,100%绿光和75%蓝光,而入射光的颜色是80%强度的白光。因此,入射漫射光的颜色由nd=0.8,0.8,0.8 给出,漫射材料的颜色由md=0.5,1.0,0.75 给出。那么从该点反射的光量由下式给出:

D=ldmd=(0.8,0.8,0.8)(0.5,1.0,0.75)=(0.4,0.8,0.6)

为了完成漫反射照明计算,我们只包括兰伯特(Lambert)的余弦定律(它根据表面法线和光线矢量之间的角度来控制表面接收的原始光线的多少)。假设ld 是漫射光的颜色,md 是漫射材料的颜色,kd=maxL·n0 ,其中L是光矢量,n是表面法线。 那么从一个点反射的漫反射光量由下式给出:
(()cd=kd·ldmd=kdDeq.7.1)

7-10
图7.10 入射光线在散射表面时随机散射。 这个想法是,微观层面的表面是粗糙的。

7.5环境光照

如前所述,我们的照明模型并没有考虑在场景中反射其他物体的间接光。然而,我们在现实世界中看到的光线是间接的。例如,与房间连接的走廊可能与房间内的光源不在直接的视线中,但是灯光从房间的墙壁反弹,其中一些可能使其进入走廊,从而使其照亮 提高一点。作为第二个例子,假设我们坐在桌子上有一个茶壶的房间里,房间里有一个光源。茶壶只有一面在光源的直线上, 不过,茶壶的背面不会变黑。这是因为有些灯光散落在房间的墙壁或其他物体上,最终撞击茶壶的背面。

为了对这种间接光线进行破解,我们给光照方程引入一个环境项:

A=lama

颜色la 指定表面从光源接收到的间接(环境)光的总量。环境材料颜色ma 指定表面反射和吸收的入射环境光的量。所有的环境光线都是一点一点地使物体变亮 - 根本没有真正的物理计算。间接的光在场景周围散射和反射很多次,以致它在各个方向上均匀地击中对象。

将环境条件与漫射项组合在一起,我们的新照明方程如下所示:

(()LitColor=lama+kd·ldmd=A+kdDeq.7.2)

7.6 镜面反射

考虑一个光滑的表面,如图7.11所示。当光线照射到这样一个表面时,光线通过一个反射锥体在一个大致方向上急剧反射; 这被称为镜面反射。与漫射光相比,镜面反射光可能不会进入眼睛,因为它反射的方向是特定的; 镜面光照计算与视点有关。这意味着当眼睛在场景中移动时,它接收到的镜面光量将会改变。

7-11
图7.11 入射光线由I表示。镜面反射不是在所有方向上散射,而是反射在一个通用的反射锥中,我们可以用一个参数来控制它的大小。 如果v在锥体内,则眼睛接收到镜面光; 否则,不接收。 越靠近反射矢量r,眼睛接收的镜面反射光越多。

镜面光反射的圆锥由相对于反射向量r的角度ϕmax 定义。直观地说,基于反射矢量r和视点矢量v=(EP)||EP|| (即,从表面点P到眼睛位置E的单位矢量)之间的角度ϕ 以如下方式改变镜面光强度是有意义的:我们规定:当ϕ=0 时镜面光强度达到最大,并且随着ϕ 达到ϕmax 而平滑地减小到零。为了在数学上对此进行建模,我们修改了Lambert余弦定律中使用的函数。图7.12显示了p≥1的不同幂的余弦函数图。本质上,通过选择不同的p,我们间接地控制光强度下降到零的圆锥角ϕmax 。参数p可以用来控制表面的光泽;也就是说,高度抛光的表面将具有比光泽度较低的表面更小的反射率锥(光反射更强烈)。所以,比磨砂表面你会在光泽表面使用一个更大的p。

7-12
图7.12 余弦函数具有p≥1的不同幂的函数图。

请注意,因为v和r是单位向量,所以我们有cos(ϕ)=v·r
从一个点进入眼睛反射的镜面反射光量由下式给出:
cs=ks·lsms=ksS

这里
ks={max(v·r,0)pL·n>00L·n0

颜色ls指定光源发射的镜面光量。镜面材质颜色ms指定表面反射的镜面光线的数量。因子ks根据r和v之间的角度来调整镜面反射光的强度。图7.13显示了一个表面可能不接收漫射光(L·n <0),但是可以接收镜面反射光。然而,如果表面没有接收到漫射光,那么表面就不会接收镜面反射光,所以我们在这种情况下设置ks=0
7-13
图7.13 即使光照在表面的背面,眼睛也可以接收镜面光。这是不正确的,所以我们必须检测到这种情况,并在这种情况下设置ks=0

NOTE:镜面光焦度p应该总是大于或等于1。

我们新的光照模型为:

LitColor=lama+kd·ldmd+ks·lsms=A+kdD+ksSkd=max(L·n,0)ks={max(v·r,0)pL·n>00L·n0

Note:反射向量由下式给出:r = I - 2(n·I)n; 见图7.14 (假设n是一个单位向量)然而,我们实际上可以使用HLSL内在反射函数在着色器程序中为我们计算r。

观察到入射光的方向即入射光的方向(即光矢量L的相反方向)。

7-14
图7.14 几何反射

7.7 简要介绍

在我们的模型中,光源发出三种不同的光:
1.环境光:模拟间接照明。
2.漫射光:模拟相对粗糙表面的直接照明。
3.镜面光:模拟相对光滑表面的直接照明。
相应地,表面点具有与其相关的以下材料属性:
1.环境材料:表面反射和吸收的环境光量。
2.漫射材料:表面反射和吸收的漫射光量。
3.镜面反射材料:表面反射和吸收的镜面反射光量。
4.镜面指数:在镜面光照计算中使用的指数,它控制反射的锥体,从而表面是多么光亮。 锥体越小,表面越光滑/光亮。

将光分解成三个组件的原因是为了灵活性; 艺术家有几个自由度来调整以获得所需的输出。 图7.15显示了这三个组件如何一起工作。

7-15
图7.15 (a)只有环境光线的球体才能均匀地照亮它。(b)环境照明和漫射照明相结合。 由于兰伯特的余弦定律,现在有从光明到黑暗的平滑过渡。(c)环境照明,漫射照明和镜面照明。 高光照明产生镜面反光。

7.8 指定材料

我们应该如何指定物质价值? 材质值可能在表面上有所不同;也就是说,表面上的不同点可能有不同的材料值(见图7.16)。例如,考虑汽车模型,其中框架,窗户,灯光和轮胎反射和吸收光线的方式不同,因此材料值需要在汽车表面上变化。

7-16
图7.16 汽车网格模型本划分为五个材质属性组。

为了近似地模拟这种变化,一种解决方案可能是在每个顶点基础上指定材料值。 这些每个顶点材质将在栅格化过程中在三角形内插,为三角形网格表面上的每个点提供材质值。但是,正如我们在第6章的“Hills”演示中看到的,每个顶点颜色仍然太粗糙,不能真实地模拟细节。而且,每个顶点颜色为我们的顶点结构增加了额外的数据,我们需要有工具来绘制每个顶点颜色。相反,普遍的解决方案是使用纹理映射,这将不得不等到下一章。同时,我们允许以平局的频率进行重大变更。也就是说,我们将材质值设置为常量缓冲区的成员,随后所有绘制的几何将使用该材质,直到在绘制调用之间进行更改。下面的伪代码显示了我们如何绘制汽车:

Set Primary Lights material to constant buffer
Draw Primary Lights geometry
Set Secondary Lights material to constant buffer
Draw Secondary Lights geometry
Set Tire material to constant buffer
Draw Tire geometry
Set Window material to constant buffer
Draw Windows geometry
Set Car Body material to constant buffer
Draw car body geometry

材料的结构在LightHelper.h 中定义,类似如下结构:

struct Material
{
    Material() { ZeroMemory(this, sizeof(this)); }
    XMFLOAT4 Ambient;
    XMFLOAT4 Diffuse;
    XMFLOAT4 Specular; // w = SpecPower
    XMFLOAT4 Reflect;
};

现在忽略反射成员; 稍后当我们模拟像镜子一样的反射,并且需要指定一个表面如何作用的镜子时,将会使用它。另请注意,我们将镜面幂指数p嵌入到镜面材质颜色的第四个分量中。这是因为照明不需要alpha分量,所以我们不妨使用空槽来存储一些有用的东西。散射材料的alpha分量也将用于后面章节中的alpha混合。

最后,我们提醒读者,我们需要在三角形网格表面的每个点上的法向量,以便我们可以确定光照到网格表面上的一个点的角度(对于朗伯余弦定律)。为了在三角形网格的每个表面上获得法向矢量逼近,我们在顶点级别指定法线。在光栅化过程中,这些顶点法线将在整个三角形内插入。

到目前为止,我们已经讨论了光的组成部分,但是我们没有讨论特定种类的光源。接下来的三部分将介绍如何实现平行光点,点光源和聚光灯。

7.9 平行光

平行光(或定向光)近似于非常远的光源。因此,我们可以将所有入射光线近似为平行(图7.17)。平行光源由向量定义,该向量指定光线传播的方向。由于光线是平行的,它们都使用相同的方向矢量。光矢量的目标是使光线行进的方向相反。一个真正的定向光源的常见例子是太阳(图7.18)。定向光的方程正如7.3。

7-17
图7.17 照在表面的平行光线

7-18
图7.18 该图不是按比例绘制的,但如果您在地球上选择一个较小的表面区域,撞击该区域的光线大致平行。

7.10 点光源

点光源的一个很好的物理例子是灯泡。它在各个方向上都呈球形辐射状(图7.19)。特别是对于任意的点P,存在从点光源位置Q向该点传播的光线。像往常一样,我们将光矢量定义为相反的方向; 即从点P到点光源的方向Q:

L=QP||QP||

本质上,点光源和平行光源之间唯一的区别在于光源矢量是如何计算的 - 点光源从一个点到另一个点是不同的,但是对于平行光源而言,它保持不变。
7-19
图7.19 点光源向各个方向辐射; 特别是对于任意点,存在从点光源Q朝向P的光线。

7.10.1 衰减

在物理上,根据平方反比法则,光强度作为距离的函数减弱。也就是说,远离光源距离d处的光强度由下式给出:

I(d)=I0d2

其中 I0 是距离光源距离 d=1 处的光强度。但是,这个公式并不总是给出完美的结果。因此,我们不再担心物理准确性,而是为艺术家/程序员提供了一些更为一般的公式,使艺术家/程序员能够控制一些参数(即艺术家/程序员用不同的参数值进行实验,直到他对结果满意为止)。用来衡量光照强度的典型公式是:
I(d)=I0a0+a1d+a2d2

我们调用a0,a1a2 衰减参数,并由艺术家或程序员提供。例如,如果实际上希望光强随反距离减弱,则设a0=0,a1=1,a2=0 。如果希望跟距离的平方成反比,则设a0=0,a1=0,a2=1

考虑衰减情况则有:

LitColor=A+kdD+ksSa0+a1d+a2d2

请注意,衰减不会影响环境条件,因为环境条件用于模拟已反弹的间接光源。

7.10.2 范围

对于点光源,我们包含一个额外的范围参数。距离光源的距离大于该范围的点不接收来自该光源的任何光。此参数对于将灯光定位到特定区域很有用。尽管衰减参数随着距离而减弱了光强度,但是能够明确地定义光源的最大范围仍然是有用的。范围参数对于着色器优化也很有用。正如我们将很快看到的那样,在我们的着色器代码中,如果点超出范围,那么我们可以尽早返回并跳过具有动态分支的光照计算。范围参数不会影响平行光源,这种光源可以模拟很远的光源。

7.11 聚光灯

聚光灯的一个很好的物理例子是手电筒。从本质上讲,聚光灯的位置为Q,瞄准方向为d,并通过一个圆锥体发射光线(见图7.20)。

7-20
图7.20 聚光灯具有位置Q,瞄准方向d,并通过角度为ϕmax 的锥体辐射光。

为了实现聚光灯,我们开始与点光源一样:光矢量由下式给出:
L=QP||QP||

其中P是点的位置,Q是聚光灯的位置。从图7.20可以看出,当且仅当 -L和d之间的角度ϕ 小于锥角ϕmax 时,P在聚光灯锥体内部(并因此接收光)。此外,聚光灯锥体内的所有光线不应该是相等的强度;圆锥中心的光线应该是最强的,当ϕ 从0增加到ϕmax 时,光线强度应该消失。

那么我们如何将强度衰减控制为ϕ 的函数呢?我们又如何控制聚光灯锥的大小呢?那么,我们可以玩与反射的镜面反射相同的方法。也就是说,我们使用这个函数:

kspot(ϕ)=max(cosϕ,0)s=max(L·d,0)s

所以聚光方程就像点光方程一样,除了我们乘以聚光因子来根据光点在哪里相对于聚光锥来标定光强度:
(eq. 7.5)LitColor=kspot(A+kdD+ksSa0+a1d+a2d2)

Note:通过比较公式7.4和7.5,我们看到聚光灯比点光源更昂贵,因为我们需要计算kspot因子并乘以它。类似地,通过比较公式7.3和7.4,我们看到点光比定向光更昂贵,因为需要计算距离d(实际上这非常昂贵,因为距离涉及平方根操作),并且我们需要划分总的来说,定向灯是最便宜的光源,其次是点光源。聚光灯是最昂贵的光源。

7.12 实现

在LightHelper.h中,我们定义了以下结构来表示我们支持的三种灯。

struct Directional Light
{
    DirectionalLight() { ZeroMemory(this, sizeof(this)); }
    XMFLOAT4 Ambient;
    XMFLOAT4 Diffuse;
    XMFLOAT4 Specular;
    XMFLOAT3 Direction;
    float Pad; // Pad the last float so we can
        // array of lights if we wanted.
};
struct Point Light
{
    PointLight() { ZeroMemory(this, sizeof(this)); }

    XMFLOAT4 Ambient;
    XMFLOAT4 Diffuse;
    XMFLOAT4 Specular;

    // Packed into 4D vector: (Position, Range)
    XMFLOAT3 Position;
    float Range;

    // Packed into 4D vector: (A0, A1, A2, Pad)
    XMFLOAT 3 Att;
    float Pad; // Pad the last float so we can set an
        // array of lights if we wanted.
};
struct SpotLight
{
    SpotLight() { ZeroMemory(this, sizeof(this)); }

    XMFLOAT4 Ambient;
    XMFLOAT4 Diffuse;
    XMFLOAT4 Specular;

    // Packed into 4D vector: (Position, Range)
    XMFLOAT3 Position;
    float Range;

    // Packed into 4D vector: (Direction,Spot)XMFLOAT3 Direction;
    float Spot;

    // Packed into 4D vector: (Att, Pad)
    XMFLOAT 3 Att;
    float Pad; // Pad the last float so we can set an
        // array of lights if we wanted.
};

1. Ambient:环境发出的环境光
2. Diffuse:环境发出的漫射光
3. Specular:镜面反射
4. Direction:光的方向
5. Position:光的位置
6. Range:光的范围。超出该范围的点没有光照。
7. Attenuation:以控制光强随距离下降的格式(a0,a1,a2)存储三个衰减常数。
8. Spot:用来控制聚光灯锥的指数。

我们将在下一节讨论“pad”变量和“打包”格式的必要性。LightHelper.fx文件定义了镜像这些文件的结构:

struct DirectionalLight
{
    float4 Ambient;
    float4 Diffuse;
    float4 Specular;
    float3 Direction;
    float pad;
};
struct PointLight
{
    float4 Ambient;
    float4 Diffuse;
    float4 Specular;
    float3 Position;
    float Range;
    float 3 Att;
    float pad;
};
struct SpotLight
{
    float4 Ambient;
    float4 Diffuse;
    float4 Specular;

    float3 Position;
    float Range;
    float3 Direction;
    float Spot;
    float3 Att;
    float pad;
};

7.12.2 结构包装

上一节定义了HLSL结构,我们在常量缓冲区中实例化这样的结构:

cbuffer cbPerFrame
{
    DirectionalLight gDirLight;
    PointLight gPointLight;
    SpotLight gSpotLight;
    float3 gEyePosW;
};

在应用程序级别,我们实例化镜像结构。我们希望在一次调用中将轻实例设置为效果变量,而不是单独设置每个数据成员。一个结构实例可以被设置为一个效果变量实例以下功能:

ID3DX11EffectVariable::SetRawValue(void *pData,UINT Offset, UINT Count);
// Example call:
DirectionalLight mDirLight;
mfxDirLight->SetRawValue(&mDirLight, 0, sizeof(mDirLight));

但是,因为这个函数只是复制原始字节,所以如果我们不小心的话会导致很难发现错误。具体而言,我们需要注意的是C ++不遵循与HLSL相同的包装规则。

在HLSL中,发生结构填充以使得元素被压缩成4D向量,限制了单个元素不能跨越两个4D向量分裂。考虑下面的例子:

//HLSL
struct S
{
    float3 Pos;
    float3 Dir;
};

如果我们必须将数据打包到4D向量中,您可能会认为它是这样完成的:

vector 1: (Pos.x, Pos.y, Pos.z, Dir.x)
vector 2: (Dir.y, Dir.z, empty,empty)

然而,这将元素方向分割成两个4D向量,这是HLSL规则所不允许的 - 元素不允许跨越4D向量边界。因此,它必须像这样打包:

vector 1: (Pos.x, Pos.y, Pos.z,empty)
vector 2: (Dir.x, Dir.y, Dir.z,empty)

现在假设我们的镜像C ++结构是这样定义的:

// C++
struct S
{
    XMFLOAT3 Pos;
    XMFLOAT3 Dir;
};

如果我们不注意这些包装规则,只是盲目地调用和复制字节,我们会得到的第一种情况是:

vector 1: (Pos.x, Pos.y, Pos.z, Dir.x)
vector 2: (Dir.y, Dir.z, empty, empty)

因此,我们必须定义我们的C ++结构,以便元素根据HLSL包装规则正确地复制到HLSL结构中; 我们使用“pad”变量。让我们再看几个HLSL如何打包的例子。

struct S
{
    float3 v;
    float s;
    float2 p;
    float3 q;
};

该结构将被填充,数据将被打包成三个4D向量,如下所示:

vector 1: (v.x, v.y, v.z, s)
vector 2: (p.x, p.y, empty, empty)
vector 3: (q.x, q.y, q.z, empty)

这里我们可以把标量s放在第一个向量的第四个分量中。然而,我们不能在矢量2的剩余槽中拟合所有的q,所以q必须得到它自己的矢量。
最后结构体如下:

struct S
{
    float2 u;
    float2 v;
    float a0;
    float a1;
    float a2;
};

将被填充和打包,如下所示:

vector 1: (u.x, u.y, v.x, v.y)
vector 2: (a0, a1, a2, empty)

数组的处理方式不同。从SDK文档中,“数组中的每个元素都存储在一个fourcomponent向量中”。例如,如果您有一个float2的数组:float2 TexOffsets[8];你可能会认为两个float2元素将被打包到一个float4插槽中,如上面的例子所示。但是,数组是个例外,前者相当于:float4 TexOffsets[8];因此,从C ++代码中,您需要设置一个由8个XMFLOAT4组成的数组,而不是由8个XMFLOAT2组成的数组才能正常工作。 每个元素浪费两个存储浮点数,因为我们真的只想要一个float2数组。 SDK文档指出,您可以使用强制转换和附加地址计算指令来提高内存使用效率:
float4 array[4];
static float2 aggressivePackArray[8] = (float2[8])array;

7.12.3 定向光源的实现

下面的HLSL函数输出给定材质,定向光源,表面法线的点的颜色,以及从表面点亮到眼睛的单位矢量:

// Defined in LightHelper.fx.
// Equation 7.3
void ComputeDirectionalLight(Material mat, DirectionalLight L,
float3 normal, float3 toEye,
out float4 ambient,
out float4 diffuse,
out float4 spec)
{
// Initialize outputs.
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// The light vector aims opposite the direction the light rays travel.
float3 lightVec = -L.Direction;
// Add ambient term.
ambient = mat.Ambient * L.Ambient;
// Add diffuse and specular term, provided the surface is in
// the line of site of the light.
float diffuseFactor = dot(lightVec, normal);
// Flatten to avoid dynamic branching.
[flatten]
if(diffuseFactor > 0.0f)
{
float3 v = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
spec = specFactor * mat.Specular * L.Specular;
}
}

使用了以下内在HLSL函数:点,反射,功率和最大值,它们分别是矢量点乘积函数,矢量反射函数,幂函数和最大值函数。大部分HLSL内部函数的描述可以在附录B中找到,以及其他HLSL语法的快速入门。有一点需要注意的是,当两个向量与运算符*相乘时,乘法是以分量方式完成的。

Note:在PC上HLSL功能总是内联; 因此,函数或参数传递没有性能开销。

7.12.4 点光源的实现

下面的HLSL函数输出给定材料点,点光源,表面位置,表面法线以及从表面点亮到眼睛的单位矢量的点亮颜色:

// Defined in LightHelper.fx.
// Equation 7.4
void ComputePointLight(Material mat, PointLight L, float3 pos,
float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
// Initialize outputs.
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// The vector from the surface to the light.
float3 lightVec = L.Position - pos;
// The distance from surface to light.
float d = length(lightVec);
// Range test.
if(d > L.Range)
return;
// Normalize the light vector.
lightVec /= d;
// Ambient term.
ambient = mat.Ambient * L.Ambient;
// Add diffuse and specular term, provided the surface is in
// the line of site of the
light.
float diffuseFactor = dot(lightVec, normal);
// Flatten to avoid dynamic branching.
[flatten]
if(diffuseFactor > 0.0f)
{
float3 v = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
spec = specFactor * mat.Specular * L.Specular;
}
// Attenuate
float att = 1.0f / dot(L.Att, float3(1.0f, d, d*d));
diffuse *= att;
spec *= att;
}

7.12.5 聚光灯的实现

以下HLSL函数输出给定材质,聚光源,表面位置,表面法线以及从表面点亮到眼睛的单位矢量的点亮颜色:

// Defined in LightHelper.fx.
// Equation 7.5
void ComputeSpotLight(Material mat, SpotLight L,
float3 pos, float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
// Initialize outputs.
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// The vector from the surface to the light.
float3 lightVec = L.Position - pos;
// The distance from surface to light.
float d = length(lightVec);
// Range test.
if( d > L.Range )
return;
// Normalize the light vector.
lightVec /= d;
// Ambient term.
ambient = mat.Ambient * L.Ambient;
// Add diffuse and specular term, provided the surface is in
// the line of site of the light.
float diffuseFactor = dot(lightVec, normal);
// Flatten to avoid dynamic branching.
[flatten]
if(diffuseFactor > 0.0f)
{
float3 v = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
spec = specFactor * mat.Specular * L.Specular;
}
// Scale by spotlight factor and attenuate.
float spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
// Scale by spotlight factor and attenuate.
float att = spot / dot(L.Att, float3(1.0f, d,
d*d));
ambient *= spot;
diffuse *= att;
spec *= att;
}

7.13 例子

在我们的第一个照明演示中,我们将有三个灯同时激活:定向,点光源和聚光灯。定向灯保持固定,点光源围绕地形转动,聚光灯随照相机移动,瞄准照相机正在寻找的方向。照明演示构建了上一章中的“Waves”演示。效果文件在下面的章节中给出,它使用了§7.10中定义的结构和函数。

7.13.1 Effect 文件

#include "LightHelper.fx"
cbuffer cbPerFrame
{
DirectionalLight gDirLight;
PointLight gPointLight;
SpotLight gSpotLight;
float3 gEyePosW;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
Material gMaterial;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
float3 toEyeW = normalize(gEyePosW - pin.PosW);
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// Sum the light contribution from each light source.
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLight,
pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
ComputePointLight(gMaterial, gPointLight,
pin.PosW, pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
ComputeSpotLight(gMaterial, gSpotLight,
pin.PosW, pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
float4 litColor = ambient + diffuse +spec;
// Common to take alpha from diffuse material.
litColor.a = gMaterial.Diffuse.a;
return lit Color;
}
technique11 LightTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}

7.13.2 C++ Application Code

7-21
图7.21 “照明”演示的屏幕截图

照明计算需要表面法线。 我们在顶点级定义法线, 然后将这些法线插入三角形的像素中,以便我们可以对每个像素进行光照计算。 而且,我们不再指定顶点颜色。 相反,表面颜色是通过应用每个像素的照明方程来生成的。 我们的输入布局描述如下所示:

D3D11_INPUT_ELEMENT_DESC vertexDesc] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12,
D3D11_INPUT_PER_VERTEX_DATA, 0}
};

在应用程序类中,我们定义了三个灯光和两个材质。

DirectionalLight mDirLight;
PointLight mPointLight;
SpotLight mSpotLight;
Material mLandMat;
Material mWavesMat;

它们在构造函数中初始化:

LightingApp::LightingApp(HINSTANCE hInstance)
{
/* ...Irrelevant code omitted... */
// Directional light.
mDirLight.Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
mDirLight.Diffuse = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mDirLight.Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mDirLight.Direction = XMFLOAT3(0.57735f, -0.57735f, 0.57735f);
// Point light--position is changed every frame to animate
// in UpdateScene function.
mPointLight.Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
mPointLight.Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
mPointLight.Specular = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
mPointLight.Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
mPointLight.Range = 25.0f;
// Spot light--position and direction changed every frame to
// animate in UpdateScene function.
mSpotLight.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
mSpotLight.Diffuse = XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f);
mSpotLight.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
mSpotLight.Att = XMFLOAT3(1.0f, 0.0f, 0.0f);
mSpotLight.Spot = 96.0f;
mSpotLight.Range = 10000.0f;
mLandMat.Ambient = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
mLandMat.Diffuse = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
mLandMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
mWavesMat.Ambient = XMFLOAT4(0.137f, 0.42f, 0.556f, 1.0f);
mWavesMat.Diffuse = XMFLOAT4(0.137f, 0.42f, 0.556f, 1.0f);
mWavesMat.Specular = XMFLOAT4(0.8f, 0.8f, 0.8f, 96.0f);
}

使用多个灯光时,必须注意不要让灯光过度饱和。 因此,您需要尝试使用环境,漫反射和镜面反射来获得正确的平衡。 实验的衰减和范围也是必要的。观察聚光灯发出黄色漫射光。 有时很容易忘记使用彩色灯,但它们可以用于各种效果。 例如,如果您使用定向光源来模拟阳光,并且太阳正在设置明亮的橙色,则可以调整发光颜色以发出橙色光,以使场景对象具有橙色色调。 当探索一艘外星飞船时,微妙的蓝色灯光可以工作,红灯可以传达紧急情况。
如前所述,点光源和聚光灯是动画的; 这是在pdateScene方法中完成的:

void LightingApp::UpdateScene(float dt)
{
/* ...Irrelevant code omitted... */
// Convert Spherical to Cartesian coordinates.
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
mEyePosW = XMFLOAT3(x, y, z);
// Build the view matrix.
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
/* ...Irrelevant code omitted ... */
// Circle light over the land surface.
mPointLight.Position.x = 70.0f*cosf(0.2f*mTimer.TotalTime());
mPointLight.Position.z = 70.0f*sinf(0.2f*mTimer.TotalTime());
mPointLight.Position.y = MathHelper::Max(
GetHillHeight(mPointLight.Positi
on.x, mPointLight.Position.z), -3.0f) + 10.0f;
// The spotlight takes on the camera position and is aimed in the
// same direction the camera is looking. In this way, it looks
// like we are holding a flashlight.
mSpotLight.Position = mEyePosW;
XMStoreFloat3(&mSpotLight.Direction,
XMVector3Normalize(target - pos));
}

点光源基本上沿着xz平面的圆形轨迹,但总是在地面或水面上行驶。 聚光灯定位在眼睛上,瞄准与眼睛相同的方向; 这使得它看起来像观众握着像手电筒一样的光。
最后,灯光和材质在渲染之前设置为效果:

void LightingApp::DrawScene()
{
/* ...Irrelevant code omitted... */
// Set per frame constants.
mfxDirLight->SetRawValue(&mDirLight, 0, sizeof(mDirLight));
mfxPointLight->SetRawValue(&mPointLight, 0, sizeof(mPointLight));
mfxSpotLight->SetRawValue(&mSpotLight, 0, sizeof(mSpotLight));
mfxEyePosW->SetRawValue(&mEyePosW, 0, sizeof(mEyePosW));
/* ...Irrelevant code omitted... */
// Set land material (material varies per object).
mfxMaterial->SetRawValue(&mLandMat, 0, sizeof(mLandMat));
/* ...Render land... */
// Set wave material (material varies per object).
mfxMaterial->SetRawValue(&mWavesMat, 0, sizeof(mWaves
Mat));
/* ...Render waves... */
}

7.13.3向量计算

因为我们的地形曲面由函数y = f(x,z)给出,我们可以直接使用微积分来计算法向量,而不是使用第7.2.1节中描述的正常平均技术。为此,对于曲面上的每个点,我们通过取偏导数在+ x和+ z方向上形成两个切向量:

Tx=1,fx,0Tz=0,fz,1

这两个向量位于曲面点的切平面上。 交叉乘积然后给出法向量:
n=Tz×Tx=ijk0fz11fx0=,fx,0
发布了7 篇原创文章 · 获赞 7 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章