现代计算机图形学 —— 着色(Shading)
现代计算机图形学
着色(Shading)
Blinn-Phong Reflection Model
漫反射项(Diffuse Reflect)
数学公式
其中
高光项(Specular Term)
产生原因
当物体绝对光滑时,光线打在在其表面呈现镜面反射。
因此相对光滑的物体,光线打到其表面,反射光分布在镜面反射方向周围,此时观察方向与镜面反射方向接近,就可以看见高光,因此 高光项(Specular Term) 取决于我们的 观察方向(view direction),这是根据 经验 所得的结果。
Blinn-Phong模型
Blinn-Phong模型很巧妙的修改了上述判断方式:
V 向量是否与镜面反射方向接近 <==> V 与 l 的 半程向量 是否与原法向相近
数学公式
其中
而
效果展示
根据Blinn-Phong模型创建测试项目,调整
环境光项(Ambient Term)
产生原因
光线在场景中经过复杂的反射,最终从四面八方发射并打在物体表面,因此称其 环境光。但是要真的添加环境光,需要十分庞大的计算量,这不利于游戏的实时渲染。
而 Blinn-Phong模型 对环境光进行了简化:它将环境光视为恒定的,帮助我们给 物体未能被光线直射而产生的阴影部分 添加恒定的颜色,弥补我们忽略的环境反射光。
数学公式
在这里,Blinn-Phong模型对环境光的计算做一个大胆的猜想:
其中
如果需要精确计算环境光,需要用到 全局光照 的知识,这些后续再谈。
Blinn-Phong模型整体效果
将上述几项光相加,即可得到物体最后的渲染效果。
整合所有的数学公式:
着色频率(Shading Frequencies)
着色频率对Shading的影响
开篇先提一个问题,是什么造成了下面三幅图的着色效果的不同?
观察这三个物体的边缘,可以发现这三个物体是相同的,只是因为 着色频率(Shading Frequencies)的不同导致他们的渲染精度发生了变化,下面我们来分别介绍三种着色频率。
Flat Shading
Flat Shading 是以每个三角形面的法向量为基准,渲染整个三角形面的方法。
该方法在三角形面较少的的时,会导致渲染精度较差。
Gourand Shading
Gourand Shading 是以每个三角形顶点的法向量为基准,先渲染每个顶点后,再用插值的方式渲染整个物体。
该方法相比于 Flat Shading 拥有较好的渲染精度,但是需要考虑如何计算每个顶点的法向量。
计算顶点法向量的方式
当三角形构成的物体表示的是一个圆,那么我们可以根据圆的切面还原出顶点的法向量,这是最理想的情况。
但实际情况是,三角形构成的物体表示的可能是一个复杂图形,此时我们可以通过 平均顶点周围面的法向量,近似得出顶点的法向量,该方法得到的向量还是比较准确的。
Phong Shading
Phong Shading 是以每个三角形面的法向量为基准,先用插值的方法得到每个像素的法向量,最后逐一渲染每个像素。该方法有着最好的渲染精度,但是开销过大,且随着三角形面数增多,其优势也逐渐变小。
需要注意的是,Phong Shading 不等于 Blinn-Phong模型,前者是渲染频率相关方法,后者是一个着色模型。
插值法获得像素法向量
插值法 是通过已知的、离散的数据点,在范围内推求新数据点的方法。因此我们可以使用插值法,求得两个已知法向量的顶点之间,所有像素的法向量。
不要忘记,在求得每个像素的法向量后,需要对其进行 标准化(normalize),也就是将其化为 单位向量。
简单纹理映射
前言
前面我们学习 Blinn-Phong 模型时,都提到了一个关键参数 —— 反射系数。该参数在一定程度上影响着物体反射光线的能力,我们因此能够看见五彩缤纷的世界。
但是在现代计算机图形学中,这个参数要如何进行记录呢?如果我们将该参数记录到3D模型上,当然可以正确的进行光线反射的模拟。但是,当我们想要更换模型的材质时,将会非常的麻烦:因为反射系数记录在模型上,这意味着你需要额外做一个只有材质不同的模型,这非常的麻烦。因此就有了 纹理 这一产物。
纹理简介
纹理 通常是一个 $11$ 的矩形图片,其中记录着一个物体表面的各种参数,包括他的材质,颜色,以及最重要的 *反射系数,等。3D模型上的每一个顶点,都对应其纹理矩形上的一个点,改点参数会在渲染时映射到模型的顶点上。只要有了对应的纹理,物体就可以正确的依照纹理渲染到我们的屏幕空间中。
有了纹理后,当我们需要为一个3D模型更换表面的材质,只需要更换其对应纹理即可。在游戏开发中,这样的开发模式很好的减少了美术老师的工作量。
三角形的重心坐标
使用原因
通过介绍,你应该已经知道纹理“是什么”了,接下来就应该讲“怎么做”—— 如何将纹理映射到3D模型的顶点。
但是在真正讲纹理映射前,我们来讲讲最基础的知识点 —— 三角形的重心坐标。
通过简介我们应该已经知道,纹理是一张 相对连续 的图片,而3D模型上的顶点对应纹理上的一个 离散的点。但是模型顶点不是连续的,我们应该如何填充其三角形中的空白呢?
首先想到的方法就是 插值。可是三角形并不像 正方形
或是 圆形
一样,能够方便的做两点之间的线性插值,那应该怎么办?这时就可以用到 三角形的重心坐标。
定义
三角形的重心坐标规定,三角形中的任意一点,可以通过三角形的三个顶点 A、B、C 表示,其表示方式为:
其中
像(
使用重心坐标进行纹理映射
根据三角形重心坐标的定义,我们可以发现只要知道了三角形顶点坐标,就可以表示三角形中的任意一点。这不就是我们需要的吗?
将屏幕空间上像素的中点(
由于三角形的重心坐标会在进行变换(Transform)之后改变,因此不能使用屏幕空间中的重心坐标进行纹理映射。需要将屏幕空间中的坐标变换为原模型中的3D坐标,再进行纹理映射。前面如此复杂的纹理映射操作就是这么来的。
纹理过小/纹理放大
纹理放大产生的问题
当3D模型对应的 纹理过小 时,为了能将纹理完全覆盖对应模型,我们就需要将纹理放大。这是图形学中普遍存在的问题—— 纹理分辨率不足 问题。
但是 纹理放大 会出现很多问题,例如同一个纹理采样点,可能会覆盖多个像素采样点。
应对方法
此时我们有多种方法应对,例如:Nearest(直接采用当前纹理样本点的信息)、Bilinear(双线性插值最近四个纹理样本点的信息)、或是Bicubic(双三次插值算法)
可以很明显的看到,使用Nearest方法产生了比较严重的锯齿现象,可见这种方法虽然操作简单但效果不佳。因此这里我们详细讲述 Bilinear 算法。
Bilinear(双线性插值算法)
简单介绍
既然直接使用当前的纹理采样点作为参数(Nearest)行不通,那我们就换一种方法。相信如果你看到这里,应该可以很快的想到一个我们经常会用到的方法 —— 插值。
对啊,插值不就好了!既然直接用现有的参数行不通,那我们就使用采样点附近的纹理样本点的参数作线性插值,应该可以得到一个相对准确的数据,Bilinear算法也是这么来的。
Bilinear算法选择离采样点 最近的四个纹理样本点 进行 线性插值,采样点会根据插值后的参数进行着色(Shading)
实际操作
如何 选取插值需要的纹理样本点 在上面已经提到过了,接下来要解决的是如何进行插值。
在 一维坐标系 上进行插值,只需要一步即可:
在 二维坐标系 上进行插值:
- 首先需要进行 两次一维线性插值 得到两个辅助点:
- 接着根据两个辅助点做线性插值:
如此反复,3D物体上的所有采样点都可以通过插值获得相对准确的纹理渲染信息,有效的避免了锯齿的产生。查看Bilinear效果图可以发现,对比Nearest方法,它对低分辨率纹理的着色效果还是不错的。
纹理过大
纹理过大产生的问题
前面说到 纹理过小 会产生 纹理分辨率不足 的问题,那么 纹理过大 会不会也产生一系列的问题呢?答案是肯定的。
仔细看上图会发现,当我们对一个纹理非常大的物体进行 点采样(Point sampled),物体离摄像机较远的位置产生了摩尔纹,离摄像机较近的位置产生了严重的锯齿现象。
那么为什么会出现这种情况,按照我们之前对纹理过小产生问题的分析结果,纹理足够大,分辨率足够高,不应该是图像更清晰吗?
在上图中,带阴影的黑色框代表着一个像素所能覆盖的纹理范围,如果我们取 蓝色的像素中心点 所对应的 纹理样本点 进行着色,所得到的图像显然不对:远处和近处都出现了 采样率不足 的情况。因此我们会看见近处出现 锯齿,而远处出现 摩尔纹,这与我们之前 采样率不足导致走样 的说法相同。
使用超采样(MSAA)处理
我们可以采用之前反走样的方式 —— 超采样(MSAA)方法对图像进行处理,可以看到效果还是不错的。
可是在计算机中这样的作法实在是太繁琐了:我们需要在像素内部进行额外的512次采样,经过平均计算之后才能得到结果。这在实际运用中是行不通的,我们需要一个更加快速的解。其实仔细分析一下问题产生的原因,从根源出发,我们或许能够的到问题的解决办法。
在一个像素需要进行渲染的时候,都需要对该点对应的纹理进行查询,查询的返回值应该是像素所 覆盖的纹理样本点的均值,此时如果我们直接返回像素中点对应的纹理参数,就会出现走样问题。
讲到这里你应该知道了,这就是一个 范围查询 的算法问题。如何快速的对一个区域内进行范围查询,是截至目前仍然在研究的问题。
想要快速的进行范围查询,我们可以预先将已经查询好的数据保存起来,等真正进行范围查询时,返回对应的值即可。这就是我们后面要讲到的 Mipmap。
Mipmap
简介
Mipmap 最大的作用就是允许 快速的、近似的、正方形的 范围查询(注意着三个特点)。
我们可以先来看一下他在计算机中树如何存储的。
可以发现他在分辨率上是逐层递减的,且前一层所占空间是后一层的四倍,它就像一个金字塔,层数越高,横截面积越小。根据等比数列的求和公式算出,Mipmap所占的存储空间仅为原图的
Mipmap的使用方法
介绍完Mipmap,我们回到着色本身,到底应该如何判断像素处于Mipmap的哪一个层级,并获取正确的纹理信息呢?
我们可以使用屏幕空间中 相邻采样点的纹理坐标 估计 纹理的被覆盖面积
可以发现,图中一块像素的覆盖范围 并不是一个正方形,因此使用Mipmap作为该点的纹理纹理信息还是不够准确,只能是对当前实际纹理信息的近似。
如果我们对求得的 Mipmap层级参数
渲染效果并不是很理想,不同深度区域之间的分割线十分的明显。
由于参数
此时还是得请出我们的老朋友 —— 插值。使用插值法,对与
其具体步骤是:首先获取采样点在最近的两个 Mipmap层级 上进行 Bilinear双线性插值 后的结果,接着对两个结果进行线性插值运算,所得结果就是当前采样点的纹理信息。
最终效果
可以看到使用上述插值法,计算落在 Mipmap层级 之间的采样点对应的纹理信息,
存在的缺陷
首先来看一下,使用 Mipmap 后能否改善我们最初因为 纹理过大 而产生的问题。
好像近处物体的锯齿现象改善了,但是远处的物体虽然没有了摩尔纹,但是颜色又糊成了一团,这是为什么?
这就是Mipmap本身的缺陷。Mipmap 只有在近似正方形的范围查询下表现良好,如果出现了长条状的范围查询,其也会发生 走样 的现象,表现在图片上就是色彩糊作一团。
要解决这个问题,就需要使用 各项异性过滤(Anisotropic Filtering)来对 Mipmap 进行优化。
各项异性过滤(Anisotropic Filtering)
使用各项异性过滤后,对于在水平或竖直方向上的长条状范围查询,我们可以查询 Ripmap 中被压缩的图像部分,可以在一定程度上缓解 Mipmap 对远处物体进行着色时产生的走样问题。
使用各项异性后,图像的着色效果 显著提高,远处物体的细节部分恢复:
当然相对于Mipmap,各向异性Ripmap的 缺点 也是显著的:
- Mipmap所占空间仅为原图的
,而Ripmap所占空间为原图的 倍,空间开销增大 - Ripmap对斜方向上的长条状查询依旧没有办法,因此只是 部分优化 Mipmap