引言:
不知道是不是有人好奇空间中的场景模型是怎么渲染成屏幕上的( image /pixels)呢?-如下图
想要对这个过程有所了解的话有必要学习一下实时渲染管线(Real-time Rendering Pipeline)[1]—如下图。在我的上一篇文章-现代计算机图形学基础二:Rasterization[2]中,我已经详细的阐述了实时渲染管线的前半部分即光栅化的过程:空间中的场景模型经过各种空间变换(Model, View, Projection transforms),把空间的物体转换到单位立方体里面来(cuboid to “canonical” cube [-1, 1]3,然后通过视口变化矩阵(Viewport transform matrix)把单位立方体映射到屏幕空间(Canonical Cube to Screen),此时我们需要把屏幕空间的三角形离散化成一个个像素。到这里我们就可以将一个三角形画在屏幕上了,但是有人就会问了实际场景中那么多不同空间三角形的组成,形状不同而且他们距离相机的距离是不一样的,那么如何把这些三角形画在屏幕上并且保证他们的遮挡关系是正确的(近处的物体要遮住远处的物体)。这个问题就涉及到可见性问题或者是遮挡问题了。这里我们解决的办法是深度测试(Z-Buffer Visibility Tests)。
在完成Rasterization后,我们就需要进行着色Shading(Fragment Processing)-如下图,从图中看到着色也可以发生在顶点着色阶段(Vertex Processing),这是因为考虑到不同的着色频率,在现代GPU可编程的渲染管线中可以通过自己写Shader来控制顶点和像素如何着色。
因此本篇文章就来给大家继续介绍Shading这一部分,主要从基本着色模型(Blinn-Phong Reflectance Model)和纹理映射两个方面来讲述。
渲染三维对象的图像时,模型不仅应具有适当的几何形状,而且还应具有所需的视觉外观[3]。决定渲染物体的外观的第一步是选择一个渲染模型(shading model)用来描述物体的颜色应该怎样变化,这里我们主要讲解的是一个简单的着色模型-(Blinn-Phong Reflectance Model)。所谓的Reflectance Model就是指光线与材质如何进行相互作用以及光线如何反射的。
我们先看下面的一张图,图中的右上角有一个光源照亮了一些茶具,我们基本可以看到每个茶杯有一些不一样的地方,比如有一个镜面反射高光(Specular highlights)还有一个颜色变换不是很明显的部分-漫反射部分(Diffuse reflection),最后一个就是杯子的背面那我们可能以为它接收不到右上角的光源应该是黑的,但是图中显示有颜色这是怎么回事呢?这会因为他接收的不是直接光照而是间接光照(Ambient lighting),简单的来说可以理解为有一些光线打在了墙上然后又反射到地板再反射到茶杯的背面,当然这一部分比较复杂我们在后面的光线追踪中继续展开。
到现在我们已经知道Blinn-Phong着色模型(Blinn-Phong Reflectance Model)有三个部分:镜面反射高光(Specular highlights)、漫反射部分(Diffuse reflection)、间接光照(Ambient lighting)。我们首先介绍比较简单的漫反射部分(Lambertian (Diffuse) Shading)。
我们知道一个点光源发射的能量是恒定的I(能量守恒,不考虑能量损失),这就意味着在每个球的表面他们到达的能量是一样的为I,由于球的表面是与r2的有关,因此球表面的每个shading point能够到达的能量是I/r2,但是到达的能量每个shading point接受多少呢?这就取决于cos θ = l • n(其实也很好理解,假设光照与法线n平行那就是全部接受cos θ=1,如果是垂直那么就没有接收的cos θ=0)。最后就是要考虑不同的shading point的材质(kd)不一样。这样我们就可以得到公式Ld = kd (I/r2) max(0, n · l)-如下图(这个公式用来反映漫反射到底能看到多少能量也就是物体的明亮程度)。
在Lambertian (Diffuse) Shading-Ld = kd (I/r2) max(0, n · l)公式中,我们可以进一步知道漫反射是和视角view没有关系的因为公式中不涉及到V的方向,这也符合漫反射的常识无论从哪个角度看漫反射的同一个shading point结果都应该是一样的。然后解释一下为什么要对cos θ = l • n取max,这是因为如果光线从shading point的反面打过来是负数没有意义的。这样一来我们就可以很好地理解Lambertian (Diffuse) Shading-Ld = kd (I/r2) max(0, n · l)公式。
我们从下面一组图石膏球的漫反射:我们可以看到图片最右边的石膏球中的一部分表现很亮这是因为光线与法线平行那么cos θ =1则表面的接受的能量较大较亮,而侧面的光线与法线垂直那么cos θ =0则表面的接受的能量较小较暗。然后我们还可以发现从左到右kd越来越大的话也会越来越亮。
然后我们需要把高光项加进去(类似镜面反射-Specular Term),那我们怎么判断是否能看到高光呢?这个时候Blinn-Phong模型就发现只要判断入射光线与View的角平分线(也成为半程向量)是否与法线n接近,然后按照上面的能量接收思路我们就可以得到Ls =ks(I/r2) max(0, n · h)p。
这里我们可以发现高光项公式比漫反射公式多了一个p(高光指数),我们知道漫反射的点乘Dot确实能反映两个向量是否接近但是他们的敏感度不够,我们通常认为高光的角度差在3-5度里面,一般需要把p(高光指数)要设置到100-200。如下图
最后我们再加上一个全局光照项就可以得到着色模型-(Blinn-Phong Reflection Model)
到这里我们已经理解了着色模型,这个时候我们考虑的是任何一个着色点shading point,那么下一步自然而然就是对所有点做一遍着色操作,那么整个场景的明亮程度就能看得见了。
着色频率可以大概分为对三角形着色(flat shading),对每个顶点着色(Gouraud shading),对每个像素着色(Phong shading)。
三角形着色比较好算因为只要算它的法线n就可以了,而顶点着色的法线怎么算呢其实在虚幻引擎或其他建模软件中一般顶点中是已经记录好了定点的法线的,当然有很多方法可以计算定点的法线介绍一个简单但是实用的方法:平均加权(如下图)
求出了每个顶点的法线后那么我们怎么对像素着色呢,这就要利用到重心坐标做插值求每个像素的法向量。这个我们后面的纹理映射部分会详细介绍这个重心坐标。
了解完了着色频率以后,我们自然而然的就会思考一个问题什么时候用三角形着色什么时候用像素着色呢?这个问题就要看场景模型的精度了,我们知道三角面越来越多的话它的着色效果会越来愈好(如下图)。这里需要说的是像素着色不一定就比三角形着色效果好。这是因为如果模型的三角面特别多多于像素的时候显然是三角面着色好(现在有的模型都有数以十亿的三角面片而屏幕上的像素也就是不到4K-4096×2160)
上面我们介绍了Blinn-Phong Reflection Model这个光照模型,其中针对每个着色点( shading point)它本身有一些属性,这个属相可以简单理解为纹理映射来的(也可以简单的理解为Blinn-Phong 模型中的kd系数)。我们看下面场景中ball上不同的地方有不同的颜色,有的地方还有五角星。我们知道它们共用一套光照模型,只是它们的漫反射k_d系数不一样,那么我们自然而然的希望在物体的不同位置定义它们对应的不同属性,这里就引入了纹理映射。
纹理映射 (Texture Mapping) 是一种将物体空间坐标点转化为纹理坐标,进而从纹理上获取对应点的值,以增强着色细节的方法。纹理映射(纹理划管线——The Texturing pipeline)[4]有以下四个步骤:投影映射(Projector And Mapping)、变换函数(Corresponder Function)、纹理采样(Texture Sampling)(如下图)
(1)投影映射(Projector And Mapping)
主要包括Projector 和 UV Mapping两种方法,将矩形地图纹理均匀贴到球表面的投影函数称之为:Spherical 形式(如下图)
Projector 只适用于简单情况,对于更复杂的几何体贴图,往往需要用到 UV Mapping:用于将 3 维模型中的每个顶点与 2 维纹理坐标一一对应。 UV map 则需要建模师精心制作(如下图)
在实时渲染中,通常是将 uv 坐标保存在顶点信息中,在三角形内使用时,通过插值的方式得到每个片元(fragment)具体的 uv 坐标,再从纹理中采样获得对应的值(这样就可以获得三角形内部的平滑变化值)。
有人可能又会好奇怎么通过三个顶点的UV来插值呢?这就涉及到重心坐标(Barycentric Coordinates)
(2)变换函数(Corresponder Function)
在对 uv 进行合理变换之后,其范围都落到了 [0,1],再分别乘以纹理实际的宽高,可得到纹理坐标。比如 uv 坐标为 [0.25,0.3],纹理的宽高为 [256,256],那么相乘可得到 [64,76.8],出现了小数如何根据纹理坐标得到对应的纹理值?这就涉及到下面的纹理采样。
(3)纹理采样(Texture Sampling)
屏幕上的一个单元称之为像素(Pixel),而纹理上的一个单元称之为纹素(texel)。如果渲染物体的尺寸与纹理尺寸一致都是 512*512,那么在获取每个 pixel 对应 texel 值时,刚好就能得到整数的纹理坐标,只需要每个 texel 逐个读取即可,不用考虑什么采样与重建[4]。
a、放大(Magnification)—双线性插值(Bilinear Interpolation)
但如果我们要渲染的一个模型是高清4K,而纹理贴图是256*256,那么这个时候纹理太小就会拉大(Texture Magnification ),这个时候如果采用最邻近法插值就会产生马赛克既视感。这是因为如果投影映射后非整数的像素点如果采用最临近的纹理值,由于模型像素点太多纹理纹素少,这个时候3*3的一个像素可能会对应同一个纹素,这个时候会产生一个马赛克既视感。
我们当然想要一个更好的效果,这就要让像素对应的纹素值有一个平滑的过度,因此我们采用了双线性插值来优化它(Bilinear Interpolation)。什么是双线性插值呢?
了解双线性插值之前我们需要了解一下线性插值,如果我们要描述一个在(v0,v1)之间的点x,我们可以用lerp(x, v0, v1) = v0 + x(v1 − v0)来表示,这就是线性插值。双线性插值就是利用两次水平线性插值再来一次垂直线性插值(主要是考虑最邻近四个点的纹理值,也可以先两次垂直再来一次水平)。
双线性插值通常会给出很好的结果并且它的消耗成本不高,效果如下图:
(b)缩小(Minification)——Mipmap
将高分辨率的纹理贴到低分辨率的模型上,就会产生锯齿(Jaggies)和摩尔纹(Moire)。这是为什么呢?这是因为近处像素覆盖纹理上的区域较小而远处像素覆盖的区域很大,一个像素对应一片很大的纹理范围的话自然而然的就说明纹理的信号变化过快而采样点只有一个,这就是典型的高频信号采样不足。
我们自然而然可以用MSAA和Supersampling来抗锯齿,但是代价太高了。那我们走另一条完全不一样的路径,我们知道产生锯齿的原因是我们采样了,如果我们不采样立刻就知道每个像素对应那片区域的纹理平均值是多少我们自然而然地解决了锯齿,这就涉及到Mipmap(我们知道点查询(Point Query)可以用双线性插值,而范围查询 Range Query的话可以采用Mipmap)。
这种方法很流行,其思想为:建立一系列不同尺寸的多级纹理,在纹理采样时,计算对应的细节级别,再用三线性插值 (Trilinear interpolation) 进行混合。首先是 Mipmap 的建立——Mipmap 中的多级纹理尺寸逐个减半,然后是Mipmap的使用——要使用 Mipmap,就要先确定用里面哪个级别的纹理去采样,也就是求出 d 值。