各向异性
各向异性(anisotropy)与均向性相反,是指在不同方向具有不同行为的性质,也就是其行为与方向有关。如在物理学上,沿着材料做不同方向的量测,若会出现不同行为,通常称该材料具有某种“各向异性”,这样的材料表面称为各向异性表面( anisotropic surface);
各向异性反射是指:各向异性表面反射光的一种现象。在生活中我们经常见到各向异性光照效果,例如光滑的炊具上的扇面光斑。
由于材质有组织的细微凹凸结构的不同,各向异性也分为基本的三种类型
- 线性各向异性;
- 径向各向异性;
- 圆柱形各向异性,实际上线性各向异性,单被映像为圆柱形。
折射
光在真空中的速度 $c$ 与在透明介质中的速度 $v$ 之比,称之为该介质的绝对折射率,简称折射率。光在真空中的折射率等于 1,通常我们认为光在空气中的折射率也近视为 1。
$$n=\dfrac{c}{v}$$
折射率较大介质的称为光密介质,折射率较小的介质称为光疏介质。
Snell
Snell 定律描述光线从一个介质传播到另一个介质时,入射角、折射角和介质折射率的关系。假设光线从空气射入水面,入射角度为$θ_i$ ,空气对光线的折射率为 $n_i$ ,折射率角度为$θ_t$ ,水对光线的折射率为 $n_t$ ,则存在:
$$sinθ_in_i=sinθ_in_t$$
通过 snell 定律,我们可以根据入射光的方向向量求取折射光的方向向量
反射
Fresnel
光线照射到透明物体上时,一部分发生反射,一部分进入物体内部并在介质交界处发生折射,被反射和折射的光通量存在一定的比率关系,这个比率关系可以通过 Fresnel 定律进行计算。
根据 Fresnel 定律计算得出的数据称为 Fresnel 系数。fresnel 系数分为 fresnel 反射系数和 fresnel 折射系数, 通常我们所说的 fresnel 系数指“反射系数”。
一个完整的 fresnel 公式依赖于折射率、消光率和入射角度等因素。
$$F=f_0+(1-f_0)(1-V•H)^5$$
$f_0$ 为入射角度接近 $0$(入射方向靠近法向量)时的 Fresnel 反射系数, $V$ 是指向视点的观察方向,$H$ 为半角向量。随着入射角趋近 90,反射系数趋近 1,即擦地入射时,所有入射光都被反射。
当入射角度接近 $0$ 时的 fresnel 反射系数的计算方法:
$$f_0=\dfrac{(n_i-n_t)^2}{(n_i+n_t)^2}$$
求出 fresnel 反射系数后,用 $1$ 减去该系数,就得到了折射系数,所以当入射角度接近 $0$ 时的 fresnel 折射系数的计算方法为:
$$f_{t0}=1-f_0=\dfrac{4n_1n_2}{(n_i+n_t)^2}$$
由此得fresnel 反射系数的计算公式为:
$$F\approx \dfrac{(n_i-n_t)^2}{(n_i+n_t)^2}+\dfrac{4n_1n_2}{(n_i+n_t)^2}*(1-V•H)^5$$
由于frensnel反射系数计算公式比较消耗时间,所以通常在程序中使用入射角接近 0 时的fresnel 系数。如果对于计算精度不高,可以简化公式:
$$F\approx(1-V•H)^4$$
色散
色散分为正常色散和反常色散,通常我们所说的色散都是指反常色散,即,对光波透明的介质,其折射率随着波长的增加而减小。
环境贴图
环境贴图( Environment Mapping, EM)也称为反射贴图( Reflection Mapping),用于模拟光滑表面对周围场景的映射效果,在一副图片上展现周围的环境。
使用环境贴图,是为了模拟光滑表面对周围场景的映射效果。光滑表面对周围场景的映射, 是由从场景出发的光线投射到光滑表面上然后被反射到人眼所形成的视觉效果。
从视点发射一束射线到反射体上的一个点,然后这束射线以这个点为基准进行反射, 并根据反射光线的方向向量检索环境图像的颜色。这就是环境贴图算法的基本思想。
环境贴图算法的步骤如下:
- 首先根据视线方向和法向量计算反射向量;
- 然后使用反射向量检索环境贴图上的纹理信息;
- 最后将该纹理信息融合到当像素颜色中;
1 | // 顶点着色器 |
透明
简单透明
简单透明光照模型不考虑透明物体对光的第二次折射、次表面散射,以及光在穿越透明物体时的强度衰减,只是简单的使用颜色调和的方法,即我们最终所看到的颜色,是物体表面的颜色和背景颜色的叠加。光强公式如下:
$$i=(1-t)i_b+ti_a$$
$t为透明物体的不透明度$,$i_a$为光源直接照射到 点上产生的反射光强,$i_b$为视线穿过透明体与另一个物体相交处的光强。
通过透射光方向计算透射光强,首先需要进行光线和空间物体的求交运算,以确定透射光的来源,这是非常消耗时间的,如果真的这样做,其实就是演变为光线跟踪算法了。为了保证实时性,在实际使用中,通常是根据入射光方向向量和法向量求取折射光方向,然后根据折射光方向检索环境纹理上的颜色值作为ib 。
1 | struct VertexIn |
复杂透明
光射入透明物体时会发生一次反射和折射,光从透明物体内射出时,又会发生一次反射和折射。 透明光照的简单模型实际上只是通过计算了第一次反射和折射,近似的模拟光透效果。
投影
投影纹理映射
投影纹理映射( Projective Texture Mapping),用于映射一个纹理到物体上,就像将幻灯片投影到墙上一样。该方法不需要在应用程序中指定顶点纹理坐标,实际上, 投影纹理映射中使用的纹理坐标是在顶点着色程序中通过视点矩阵和投影矩阵计算得到的,通常也被称作投影纹理坐标(coordinates in projective space)。而我们常用的纹理坐标是在建模软件中通过手工调整纹理和 3D 模型的对应关系而产生的。
投影纹理映射的目的是将纹理和三维空间顶点进行对应, 这种对应的方法好比“将纹理当作一张幻灯片,投影到墙上一样”。
投影纹理映射有两大优点:其一,将纹理与空间顶点进行实时对应,不需要预先在建模软件中生成纹理坐标;其二,使用投影纹理映射,可以有效的避免纹理扭曲现象。
投影纹理映射的流程是“根据投影机(视点相机)的位置、投影角度,物体的坐标,求出每个顶点所对应的纹理坐标,然后依据纹理坐标去查询纹理值”,也就是说,不是将纹理投影到墙上,而是把墙投影到纹理上。投影纹理坐标的求得,也与纹理本身没有关系,而是由投影机的位置、角度,以及3D模型的顶点坐标所决定。
根据顶点坐标获得纹理坐标的本质是将顶点坐标投影到 NDC 平面上,此时投影点的平面坐标即为纹理坐标。如果你将当前视点作为投影机,那么在顶点着色程序中通过 POSTION 语义词输出的顶点投影坐标,就是当前视点下的投影纹理坐标没有被归一化的表达形式。
顶点投影过程是顶点从模型坐标空间转换到世界坐标空间,然后从世界坐标空间转换到视点空间,再从视点空间转换到裁剪空间,然后投影到视锥近平面,经过这些步骤,一个顶点就确定了在屏幕上的位置。纹理投影过程是将视点当作投影机,根据模型空间的顶点坐标,求得投影纹理坐标的流程。通过比较,可以发现这两个流程基本一样,唯一的区别在于求取顶点投影坐标后的归一化不一样: 计算投影纹理坐标需要将投影顶点坐标归一化到$【0,1】$空间中,实现这一步,可以在需要左乘矩阵 normalMatrix , 也可以在着色程序中对顶点投影坐标的每个分量先乘以$\dfrac{1}{2}$然后再加上$\dfrac{1}{2}$。
$$normalMatrix=\left[\begin{matrix}0.5 & 0 & 0 & 0.5 \0 & 0.5 & 0 & 0.5 \0 & 0 & 0.5 & 0.5 \0 & 0 & 0 & 1\end{matrix}\right]$$
所以求取投影坐标矩阵的公式为:
$$texViewProjMatrix=biasMatrix × projectionMatrix × viewMatrix × worldMatrix$$
求得纹理投影矩阵后,便可以使用该矩阵将顶点坐标转换为纹理投影坐标。
$$texViewProjCoordinate=texViewProjMatrix × modelCoordinate$$
使用投影纹理坐标之前,要将投影纹理坐标除以最后一个分量q。到此,你就可以使用所求得的投影纹理坐标的前两个分量去检索纹理图片,从中提取颜色值。
Cg标准函数库中有的纹理映射函数的表达形式为:$tex2DProj(sampler2D tex, float4 szq)$
1 | // 顶点着色器 |
阴影
Shadow Map
Shadow Map 是一种基于深度图(depth map)的阴影生成方法,该方法的主要思想是:在第一遍渲染场景时,将场景的深度信息存放在纹理图片上,这个纹理图片称为深度图;然后在第二次渲染场景时,将深度图中的信息$lenth_1$ 取出,和当前顶点与光源的距离 $lenth_2$ 做比较,如果 $lenth_1$ 小于 $lenth_2$ ,则说明当前顶点被遮挡处于阴影区,然后在片段着色程序中,将该顶点设置为阴影颜色。
depth map
深度图是一张 2D 图片,每个像素都记录了从光源到遮挡物(遮挡物就是阴影生成物体)的距离,并且这些像素对应的顶点对于光源而言是“可见的”。这里的“可见”像素是指,以光源为观察点,光的方向为观察方向,设置观察矩阵并渲染所有遮挡物,最终出现在渲染表面上的像素。
Depth map 中像素点记录的深度值记为 $lenth_1$ ;然后从视点的出发,计算物体顶点 v 到光源的距离,记为 $lenth_2$ ;比较 $lenth_1$ 与 $lenth_2$ 的大小,如果$lenth_2>lenth_1$,则说明顶点 $v$ 所对应的 depth texure 上的像素点记录的深度值,并不是 $v$ 到光源的距离,而是 $v$ 和光源中间某个点到光源的距离,这意味着“ $v$ 被遮挡”。
depth map与shadow texture不是一样东西,depth map保存的是“从视点到物体顶点的距离,通常称为深度值”而shadow texture是将日常所见的阴影保存为纹理图片。
Shadow map 以 depth map 为技术基础,通过比较“光源可见点到光源的深度”和“任何点到光源的深度”来判断点是否被物体遮挡;
实现流程
使用 Shadow Map 技术渲染阴影主要分两个过程:生成 depth map(深度图)和使用 depth map 进行阴影渲染。
depth map生成流程
- 以光源所在位置为相机位置,光线发射方向为观察方向进行相机参数设置;
- 将世界视点投影矩阵 worldViewProjMatrix 传入顶点着色程序中,并在其中计算每个点的投影坐标,投影坐标的 Z 值即为深度值(将 Z 值保存为深度值只是很多方法中的一种)。在片段 shadow 程序中将深度值进行归一化,即转化到【0,1】区间。然后将深度值赋给颜色值( Cg 最的颜色值范围在0-1 之间)。
- 从 frame buffer 中读取颜色值,并渲染到一张纹理上,就得到了 depthmap。注意:在实际运用中,如果遇到动态光影,则 depth map 通常是实时计算的,这就需要场景渲染两次,第一次渲染出 depth map,然后基于 depth map 做阴影渲染。渲染 depth map 的顶点着色程序和片段着色程序分别为:
depth map 中保存的深度值,是衡量“顶点到视点的距离”相对关系的数据,计算深度值的重点在于“保证距离间相对关系的正确性”
1 | // 渲染 depth map 的顶点着色程序 |
阴影渲染流程
- 将纹理投影矩阵传入顶点着色程序中。注意,这个纹理投影矩阵,实际上就是产生深度图时所使用的 worldViewProjMatrix 矩阵乘上偏移矩阵,根据纹理投影矩阵,和模型空间的顶点坐标,计算投影纹理坐标和当前顶点距离光源的深度值 $lenth_2$(深度值的计算方法要和渲染深度图时的方法保持一致)。
- 将 depth map 传入片段着色程序中,并根据计算好的投影纹理坐标,从中获取颜色信息,该颜色信息就是深度图中保存的深度值 $lenth_1$ 。
- 比较两个深度值的大小,若 $lenth_2$ 大于 $lenth_1$ ,则当前片断在阴影中;否则当前片断受光照射。
1 | // 使用 depth map 进行阴影渲染的顶点着色程序 |
优点
Shadow map 方法的优点是可以使用一般用途的图形硬件对任意的阴影进行绘制,而且创建阴影图的代价与需要绘制的图元数量成线性关系,访问阴影图的时间也固定不变。此外,可以在基于该方法进行改进,创建软阴影效果。所谓软阴影就是光学中的半影区域。如果实时渲染软阴影,并运用到游戏中。
缺点
- 阴影质量与阴影图的分辨率有关, 所以很容易出现阴影边缘锯齿现象;
- 深度值比较的精确度和正确性,有赖于 depth map 中像素点的数据精度,当生成深度图时肯定会造成数据精度的损失。要知道,深度值最后都被归一化到 【0,1】 空间中,所以看起来很小的精度损失也会影响数据比较的正确性,尤其是当两个点相聚非常近时,会出现 z-fighting 现象。所以往往在深度值上加上一个偏移量,人为的弥补这个误差;
- 自阴影走样( Self-shadow Aliasing) ,光源采样和屏幕采样通常并不一定在完全相同的位置,当深度图保存的深度值与观察表面的深度做比较时,其数值可能会出现误差,而导致错误的效果,通常引入偏移因子来避免这种情况;
- 这种方法只适合于灯类型是聚光灯( Spot light )的场合。如果灯类型是点光源( Point light)的话,则在第一步中需要生成的不是一张深度纹理,是一个立方深度纹理( cube texture)。如果灯类型是方向光( Directional light)的话,, 则产生深度图时需要使用平行投影坐标系下的 worldViewProjMatrix 矩阵;
Shadow texture
Shadow texture 不但表示阴影贴图,也代表了一种阴影渲染方法。
shadow texture 技术,将生成的阴影图形作为投影纹理来处理,也就是将一张阴影图投影映射到一个物体上(阴影接收体)。这种方法的缺点在于:设计者必须确认哪个物体是遮挡物,哪个物体是阴影接受体,并且不能产生自阴影现象(将一个物体的阴影贴图贴到物体身上,)。
stencil
模板阴影( Stencil Shadow)算法是基于网格几何体和模板缓冲区( stencilbuffer)的阴影渲染算法。其主要优势在于: 不需要高端显卡支持,只需要基本的模板缓冲区即可;阴影精度高,边缘清晰(既是优点,也是缺电),无锯齿现象;光源的类型和位置对性能没有影响,而 shadow map 技术会受到光源类型的影响。因此该算法被广泛的应用于游戏中,目前主流的图形引擎基本
都集成了该算法。
理解模板阴影首先需要理解两个概念:阴影体( shadow volume)和模板缓冲区。实际上模板阴影名称的由来就是因为该算法要基于 stencil buffer 实现。
阴影体
阴影体本质上一个几何区域,用于描述光线被遮挡的空间区域。如三角形 ABC 遮挡了来自光源的光线, 然后在地面上形成了一个投影三角面面 DEF, ABC 与 DEF之间的空间体称为阴影体( shadow volume,也称之为阴影锥)。
模板阴影算法的基本原理是:找出场景中哪些点处于阴影体中,然后将这些点所对应的像素赋予阴影的颜色。很明显该算法有两个关键之处:
- 计算空间点与阴影体的关系(包含或者非包含)。
- 在阴影体中的空间点所对应的像素进行标记,并在绘制的时候对这些标记的像素点绘制暗色调。
计算空间点与阴影体的关系,等价于判断一个像素点所代表的 3D 空间点是否在阴影体中。一般使用经典的直线穿越次数判断算法,即:通过计算视点到该点的连线穿越阴影体面的次数,判断点是否在阴影体中。想象从视点出发射向空间点,如果射线和阴影体完全没有交集,那么点肯定不在阴影体中,该点所对应的像素不必做阴影处理;如果射线与阴影体有交集,并不表示该点一定在阴影体
中,因为射线有可能会射入后再射出。所以只有在射线射入阴影体后,并终止于阴影体中,才表示该点在阴影体内。
图(1)与图(2)都是面向观察者的面,也就是射线射入阴影体的面;图( 3)是背对观察者的面,所以是射线射出阴影体的面。所以计算空间点与阴影体关系, 等价于计算“从视点到空间点的”射线进出阴影体的次数。具体的算法有 Z Pass 算法和 Z Fail 算法。
Z Pass 算法
Z Pass 算法:从视点向空间点引一条射线,当射向进入阴影体时, stencil 加一;当射线离开阴影体时, stencil 减一。如果 stencil 值大于零,则表示该空间点在阴影体中;如果 stencil 值为零,则表示射线进入阴影体和离开阴影体的次数相等,即,空间点不在阴影体中; stencil 值不可能小于零,因为只有进入阴影体才可能离开,不可能存在离开的次数大于进入的次数。stencil 是一个标记值,初始为零,之所以取名为 stencil,是因为该值存放在模板缓冲中
Z Pass 算法实现需要两个缓存器: z buffer(深度缓冲器)和 stencil buffer(模板缓冲器)。算法流程如下
- 开启深度写( Z Enable,允许向 z buffer 中写入值),关闭光源,渲染整个场景,目的是为了获得深度值;这里要关闭光源,是因为这一步的渲染场景完全是为了获得场景像素的深度值,而开启光源只会花更多时间进行光照渲染,但这是不必要的。这也说明了一点:计算阴影时,要两次渲染场景。
- 关闭深度写( Z Disable,不允许向 z buffer 中写入值),只渲染阴影体的正面(面向视线的面),如果深度测试通过则 stencil 值加一。所谓深度测试,即“第一步得到的像素深度值,与渲染阴影体正面得到的深度值进行比较,如果前者小于后者(场景像素更接近视点),则谓之深度测试失败”,反之,如果深度测试通过,则说明场景像素所对应的空间点在阴影体正面之后, stencil 加一,即射线进入阴影体中。
- 渲染阴影体背面(背向观察者的面),深度通过(场景像素深度大于阴影体背面深度),即表示射线出阴影体, stencil 减一;
- 模板值大于零的像素点对应的空间点在阴影体中。现在开启深度写,并开启光源,重新渲染场景,考察每个像素的 stencil 值(存放在 stencil buffer 中),如果大于零,则设置像素颜色值为暗色调。
Z Pass 算法有一个严重的缺点:当视点在阴影体中时,会导致 stencil 值计算错误,因为视点在阴影体中,会使得视线失去一次进入 shadow volume 的机会。
Z Fail 算法
Z Fail 算法弥补了 Z Pass 算法的缺点,即:解决了视点进入阴影体后, z pass失效的问题。 Z Fail 算法的流程为:
- 开启深度写,关闭光源,渲染整个场景,获取深度值;
- 关闭深度写,渲染阴影体背面,深度测试失败(场景像素深度值小于阴影体背面深度值),则 stencil 加一;
- 渲染阴影体正面,深度测试失败(场景像素深度值小于阴影体正面深度值场景像素),则 stencil 减一;
- 最后 stencil 值不为零的像素点处于阴影体中。开启阴影写,开启光源,重新渲染场景,并查看每个像素点的 stencil 值是否为零;如果不为零,则对像素颜色值赋予暗色调。
Z Fail 算法要求阴影体是闭合的(有时候视锥裁剪会导致阴影体是敞开的)。
优点
模板阴影的好处在于:可以精确表现动态光影技术(如果是基于纹理或者像素做阴影计算,会由于精度的原因导致锯齿现象),适用性广;
缺点
不足在于:确立阴影体,需要提前对实体进行物体勾边( objectOutlining),即,计算出物体网格的轮廓,同时渲染的阴影比较硬,无法渲染软阴影