首先明确一下Shading的定义:即对不同的物体应用不同的材质

Blinn-Phong模型

定义

当有一根光线打到物体表面某个点时,光线会被均匀地反射到各个不同的方向上去


此图的意思为:

  • $\vec{n}$:法线,垂直于平面的方向(Surface normal)
  • $\vec{v}$:观察方向(Viewer direction)
  • $\vec{l}$:光照方向(Light direction)

PS:以上向量都是单位向量,只表示方向

兰伯特(Lambert)余弦定理

$cos\theta = \vec{l} \cdot \vec{n}$

不同角度的物体反射的光不同

光照衰减


我们认为光照是辐射状向外扩散的,每个阶段都能形成一个类似于球壳的东西,球的表面积为$4\pi r^2$,假设最内圈的球壳上,能量(光照)强度为$I$,此时它的总能量为$I * 4\pi R^2$(R = 1),根据能量守恒(不考虑传播时的损耗),最外圈的能量和内圈相等,由此可推出

漫反射(Diffuse Reflection)



$cos\theta = \vec{n} \cdot \vec{l}$ 余弦表示Shading Point单位面积接收到了多少能量,是负数时表示是从后面射过来的,没有物理意义,因此 max(0, n * l)

$k_d$:漫反射系数,等于1时表示这个点完全不吸收能量,是最亮的,等于0时表示这个点所有能量都被吸收了,是最黑的

PS:不管从哪个方位观察Shading Point,结果都应该是一样的,漫反射与观察方向无关

高光(Sepcular Term)

什么时候能看到高光:观察的方向和镜面反射的方向接近的时候

PS:当观察方向和镜面反射方向接近的时候,其实就说明这个法线方向半程向量接近


$cos\alpha$ 越接近1表示向量越接近

$k_s$ : 镜面反射系数

$p$ 是指数,为了避免 $cos\alpha$ 衰减得太慢,导致 $\vec{n}$ 和 $\vec{h}$ 夹角大(例如 45° 时)也有比较大的高光(假如期望 20° 和 30° 时就无高光了), $p$ 在Blinn-Phong模型里一般使用100 - 200

环境光照(Ambient Term)

假设:任何一个点接收到来自环境的光永远都是相同的

  • $L_a$ : reflected ambient light
  • $k_a$ : 环境光照系数
  • $I_a$ : 环境光照强度(某一种颜色)

PS: 环境光照是一个常数,其与 $\vec{v}$ 和 $\vec{n}$ 都无任何关系

总结


着色频率(Shaing Frequencies)



根据着色频率不同,着色的效果也不同

  • 以三角形平面为单位着色(Flat shading)
  • 以顶点为单位着色(Gouraud shading):求出每个顶点的法线,只有知道三角形三个顶点的法线,就能根据插值求出三角形内任一顶点的法线
  • 以像素为单位着色(Phong shading):对于每一个四边形或者三角形,顶点求出法线,然后把这些法线的方向在三角形内部进行插值,就得到了任一像素都有它自己的法线方向

PS:当几何体面数足够复杂时,逐面的效果不一定比逐顶点差

在复杂的图形中,一个顶点的法线方向通常是将相邻四个面的面积进行加权平均得到四个面的平均法向量

渲染管线(Graphics(Real-time Rendering) Pipeline)


渲染管线的基本流程:

  • 输入3D空间中一系列的点
  • 处理顶点,将顶点变换到2维平面
  • 将顶点相连,组成一个一个三角形
  • 对每个fragment进行采样,如果没使用MSAA之类的超采样,此处可以将一个fragment看成一个像素
  • 着色
  • 输出

注:

  • mvp变换是在Vertex Processing中进行的
  • 涉及到对每个像素进行采样,判断是否在三角形内,这一步就是在Rasterization中进行的,即光栅化
  • 当光栅化后产生一系列的fragment或者像素的时候,要判定它是否可见(深度测试),是在Fragment Processing中进行的
  • Shaing在Vertex Processing或者Fragment Processing中都有可能进行,取决于如果是逐点那就位于Vertex Processing中,而逐像素则位于Fragment Processing

纹理映射(Texture Mapping)

任何一个三维物体表面都是二维的

纹理就是一张图,纹理是有坐标系的,被称为uv坐标系,通常认为uv都在0-1之内

一个纹理可以使用很多次,不同的位置也可以映射到相同的纹理的位置上面,纹理本身也可以做到无缝衔接

重心坐标(Barycentric Coordinates)

重心坐标是为了做三角形内部的插值,三角形顶点上可以定义多种不同的属性,为了在三角形内部做平滑过渡


在三角形ABC的平面内,任何一点都可以表示为三个顶点ABC的线性组合,如果是在三角形内的话那 $\alpha$,$\beta$, $\gamma$ 都是大于0的


接着即可使用重心坐标插值的方法计算出各个属性在各个点的值,包括但不限于 position, color, normal, texture。

重心坐标有一个问题:在投影的情况下是不能保证重心坐标不变的,因此计算投影前的三角形重心坐标,应该结合深度,也就是取三维空间里面的坐标,不能在投影之后的三角形里面做,如果已经投影的则使用逆变换转换回去,简而言之,你原本的属性定义在几维的点上,就用几维中的坐标。

应用纹理(Applying Textures)

首先,对于屏幕上每一个采样点 $(x, y)$ ,我们使用上面刚提到的重心坐标去插值出其 $(u, v)$ 坐标(三角形三个顶点本来就有属于自己对应的 $(u, v)$ ),再使用这个 $(u, v)$ 坐标去在纹理中查询到对应的 texcolor ,此时,我们可以认为纹理定义的就是漫反射的系数 $k_d$ ,然后就可以用取到的这个 texcolor 直接去代替这个 $k_d$ ,就相当于把这张图贴在了物体表面了。

应用纹理时出现的问题

纹理的放大(Texture Magnification)

出现原因

这是当纹理太小的时候发生的,假如渲染出了一面4k分辨率的墙,需要去应用一个只有256x256分辨率的纹理,那么一些点查询出来的 $uv$ 肯定是个非整数的值,接着会被四舍五入为整数,一个范围内很多 pixel 都会被映射到一个 texel (纹理上面的点) 上面去,也就被拉大了。

解决办法

双线性插值(Bilinear interpolation)



通过最近的四个点做出插值:

  • 每个 texel 之间的距离为1
  • 已知 $s$ 和 $t$ 的大小
  • 接着就可以使用线性插值(图中的 $lerp$ )插值出两个辅助点 $u_0$,$u_1$ 的位置
  • 最后使用两个辅助点就可以插值出我们要求的 $(x, y)$ 的位置

双三次插值(Bicubic):周围16个点做3次插值


纹理太大了(Texture Magnification(hard case))

出现原因

和第一想法相反,纹理太大了也会出问题,而且严重性甚至会高于纹理太小导致的问题


可以很直观的看出,纹理太大造成的直接后果就是走样(远处的摩尔纹和近处的锯齿)



从左到右,每一个像素大小映射到纹理上面的区域依次变大(透视),前面两个像素覆盖区域很小时,我们可以近似认为这个像素的值这个覆盖纹理区域的平均值,这是没问题的。

但是当覆盖区域很大时(接近地平线的点),再理所当然认为像素对应的这个中心点的值就是覆盖区域的平均值的话,这是显然错误的。

当一个纹理特别大的时候,一个像素内部就有可能包含很大一块纹理,这一块纹理是一直在变化的,也就是我们采样的频率跟不上信号变化的频率,导致了信息的丢失,因此我们就不能这样简单的去采样。

解决办法

超采样(点查询)

例如作业2已经实现过的MSAA,但是他是及其消耗性能的,甚至需要MSAA 512x才能解决的话,这种性能消耗是绝大部分机器不能忍受的

Mipmap(范围查询)

Mipmap有三大特性:fast , approx , square ,也就是说,Mipmap可以做快的,近似的(也就是不是很准确),只有正方形的范围查询


Mipmap实际上就是从一张图生成一系列图,下图可以很直观看出,Mipmap实际上的额外存储量是原本大小的 $1/3$ ,虽然画得有点不标准,但是可以很清晰看出,长宽除2后无限递减下去,得到的面积之和肯定是原面积的 $1/3$



计算要查询的区域在Mipmap的第几层:

  • 首先我们要去获得要查询的点的相邻点(图中是上和右)
  • 接着将这三个点都映射到纹理上面去,得到对应的 $uv$ 坐标
  • 计算出和相邻点的距离 $L$(取较大者),可以将 $L$ 看作对应到 $uv$ 坐标上每个 texel 正方形的边长大小
  • 接着求出Mipmap的层级 $D$,假如正方形为1x1,那么肯定就是第0层,假如正方形为4x4,那么就是递减两次后会边长1x1的像素,所以此时为第2层

那此时我们又遇到一个问题,万一求出了的 $D$ 没有落在整数上面怎么办呢,此时就会导致在层级变化的时候没有一个很好的平滑过渡,这时我们又可以想到——插值


如何去做这个三线性插值:

  • 将求出了的 $D$ 相邻的两个整数层级上的双线性插值求出来
  • 接着再到层与层直接做一个线性插值

Mipmap的局限性



可以看到,远处的地方Overblur(过度模糊了),已经丢失了信息,这是因为Mipmap只能做一个正方形的近似查询以及三线性插值的平均。

此时我们需要:各向异性过滤


各项异性过滤是在保存Mipmap之外,同时保存了只压缩一根轴的情况,也就是可以用长条矩形去查询

对于上面这张图来说,屏幕空间的 pixel 对应到纹理空间中会边长长条状甚至斜边,各向异性过滤对于长条状矩阵是有很好的效果的,但是总共的内存开销是原本的三倍

此外还有一种EWA过滤也可以解决Overblur

把任意不规则的形状,拆成很多不同的圆形去覆盖这个不规则的形状,每一次去查询一个圆形,多次查询之后也就能覆盖这个形状了(性能的代价)