首先是对上次课的一些补充:

shading课程介绍了很多东西,从最开始的布林-冯反射模型,到后来的采样频率,实时渲染管线,纹理映射,重心坐标,纹理反走样,今天我们要继续做纹理的应用。

Applications of Textures

在现在的GPU中,纹理可以被视作一块内存加一个范围查询的结合,可以被应用的地方也很多:环境光照(环境贴图)

右图:犹他茶壶。

如果把环境贴图全部存储在一个物体上,就可以再利用这个物体去渲染别的物体,比较生动的例子就是材质球。

但这样也会带来问题:

将球上的纹理展开成图后,会发现图顶部和底部的部分会产生扭曲现象。

如何处理呢?我们可以将球上的纹理转换到立方体上:我们从球心出开始连线到球上的每一个点后,继续延长到包裹他的立方体表面,然后将球体上的纹理信息转移到对应的立方体的表面,于是这样就实现了纹理的转移而且解决了扭曲现象。

展开后就得到了这样的图,需要一些额外的计算。

纹理还可以影响着色,比如说凹凸纹理的应用:众所周知纹理不仅仅可以存储颜色光照信息,我们给他存储高度的信息后就可以实现不同像素之间高低有致的效果。更具体地说其实是在不改动各个三角形的前提下,去干扰三角形法线的属性来“伪装”具体的几何表现。

如图所示:原本光滑的曲线收到凹凸贴图的影响,被告知法线需要发生变化。那么具体如何变化呢?

问题变成了如何求凹凸贴图上任意一点的法线,我们只需要先求导得到切线再将切线逆时针旋转九十度后归一化即可得到法线的方向向量。在结果上等同于(-dp,1);

二维也类似,我们分别求两个方向的偏微分后分别交换两个方向的偏微分后加个负值正则化即可。

另一种做法类似的叫做位移贴图,同样是告诉我们的三角形的高度进行变化,不同的是这一次的纹理确实是要求三角形进行位置上的变化。(凹凸贴图只是伪装了一个高度发生变化的情况,实际上的物体的高度是完全没有变化的,如左图)。不过位移贴图对于三角形的细节要求更高,无法适用于很粗糙的模型。

三维的纹理可以应用于体积的渲染,比较常见的例子是医院里的核磁共振成像,我们扫描人体的某个部分后,返回的信息就可以返回一个三维信息,这部分三维信息我们就视作三维的纹理,因为这部分信息就像纹理一样可能记录了很多信息。

Geometry

我们大体上将几何分为两类:隐式的几何和显式的几何。

隐式几何即并不给出具体的物体而只是给出关系方程来描述一个几何物体,如我们的球面方程就是一种隐式几何;而显式几何需要具体给出如顶点,面等物体来描述几何。

一些几何物体只用隐式几何很难表达出来,比如这样一个环体。

但是判断点与几何的关系时,隐式几何就非常方便。

显式几何的表示方法:直接给出或者使用参数的映射关系方法给出。

比如这个等式,虽然也是用关系式的方式表达,但是我们只需要将所有的u和v遍历一遍就可以得到这个几何体,所以这其实就是一种映射关系的表示(用u,v存储),也被认为是显式几何。

说白了,隐式几何更方便判断,显式几何更直观,但是二者都是互有优劣。

也就是说,没有最佳的几何表示方法。

要对症下药,以下是一些更多的几何表示方法补充:

用数学公式表示。

用不同的几何体通过与或非(交集,并集,互斥)等逻辑运算表达其他的几何体。

距离函数:去描述任何一个点到这个表面的最小距离。

如果不是用距离函数进行融合的话,如第一行所示,一个1/3黑与一个2/3黑的方形融合后只能得到一个1/3黑与2/3半黑的图形,而使用距离函数的话,1/3的距离函数为负数的一个方形与一个2/3距离函数为负数的方形融合之后可以得到一个一半距离函数为负数的方形。(其实我还是没有很懂,先把视频里说的东西复制粘贴一下)

水平集,概念上来说等同于等高线。

分型,与递归差不多一个概念。

Curves

一些在CG领域中的显式几何的表达:

点云:

把物体看作一堆缝隙小到看不见的点的集合就是点云。

理论上只要点云的密度足够大,就可以把任何物体表示出来,一般三维扫描等应用的结果都是点云。

多边形的面:

在图形学中应用得最广泛的显式几何,将各种物体拆为各种三角形面与四边形面的集合,一般研究连接关系。

.obj文件格式:

一个容纳了顶点,法线,纹理坐标与连接关系的文本文件,定义一个用三角形组成的网格。

接下来就是正儿八经的曲线的介绍:

贝塞尔曲线

用一堆点与一系列条件来定义贝塞尔曲线:如图所示,要求该曲线的起点必须是p0而终点必须是p3,且在p0和p3的切线方向必须为p1与p2,于是我们就可以得到一条唯一的曲线。

具体如何画出一条贝塞尔曲线呢?

首先来介绍de Casteljau算法:如图所示,三个点生成的贝塞尔曲线就叫做二次贝塞尔曲线,我们要画出一条贝塞尔曲线,只需要知道任何在一个范围在(0,1)的t对应的点,这样的点全部组合起来就可以得到我们的贝塞尔曲线。

如何得到这个点呢?我们已经有三个点,那么对应的就是两条线段,假设我们想知道t=1/3的点,我们分别在两条线段1/3处取点。

这两个点又可以组合成一条线段,我再在这条线段上的1/3处取点,这时候我们得到了唯一的一个点,这个点就是我的贝塞尔曲线在t=1/3的点。

穷举所有的t就可以得到我们完整的贝塞尔曲线了。

即使点数增加也一样,我们通过同样的做法总能得到唯一的贝塞尔曲线。

整个过程就可以用这样一个图来表示:我们不断地减少点数直至1。

直观地解释后,我们又能否通过代数运算的方式来表示呢?

我们要得到的一个点,是从所有已知的点不断地插值得到的:

写出来的话结果大概就是这样。看这个算式是不是很眼熟呢?是的,这就是一个描述二项分布的多项式的展开式,总结来说就是这样:

其中涉及到了一个伯恩施坦多项式:

而能够写出我们贝塞尔曲线的代数表达后,我们就可以不把点限制在易于理解的二维了,而是把这些点都放置在空间中,依然可以得到我们的贝塞尔曲线。

贝塞尔曲线的性质:

一定通过起点和终点(画的时候就规定好的),对于超过二阶的贝塞尔曲线,起点的切线一定是沿着第二个点,终点的切线一定沿着倒数第二个点;构成贝塞尔曲线的点在经过仿射变换(线性变换和平移变换)后仍然组成相同的贝塞尔曲线;贝塞尔曲线一定在控制点组成凸包内。

什么是凸包呢?

简单地说,凸包就是能够包裹住所有控制点的最小的凸多边形。

逐段的贝塞尔曲线。

当贝塞尔的阶数很高时,虽然我们依然可以按照上述方法画出贝塞尔曲线,但是这样很麻烦。

于是人们想:可不可以将控制点分为几段,我们分别画这几段的贝塞尔曲线后再把这几段贝塞尔曲线进行融合呢?

比如说:我们定义四个控制点就是一段贝塞尔曲线。这样我们就可以得到好几段风格迥异但是分别都是光滑的曲线,然后我们需要将这几段曲线也光滑地融合:根据之前的贝塞尔曲线的性质可知:起点和终点的切线方向都是由起点与第二个点以及终点与倒数第二个点决定的,那么我们能否让不同曲线的起点和终点的切线方向一致实现光滑地融合呢?

两段贝塞尔曲线如果满足第一段的终点是第二段的起点的条件的话,我们将这种连续称为C0级连续。

而如果这两段贝塞尔曲线的控制点的其中一段的终点是另一段的起点且两段曲线离这个终点(起点)分别最近的点与这个终点(起点)共线且反向,距离相同,那么我们认为这两个贝塞尔曲线C1级连续。

一些别的曲线:

样条:要求一条连续的曲线通过一系列给定的点,简单地说就是控制下的曲线就是样条。

最常用的B样条:算是一种对贝塞尔曲线的扩展:贝塞尔曲线有着牵一发而动全身的性质:任意改变一个控制点的位置都会导致整个曲线发生变化,而B样条就避免了这样的事,局部性更优于贝塞尔曲线。

Surfaces

介绍完曲线接着介绍一下曲面:

既然都有贝塞尔曲线了,自然也少不了贝塞尔曲面。且生成原理也差不多:假设我们有4*4=16个控制点,我们先水平方向分别运用算法得到四条贝塞尔曲线,然后我们再假设这四条贝塞尔曲线上不同于水平方向的t的另一个相同t的四个点就是一条新的贝塞尔曲线的控制点,就可以得到一个新的贝塞尔曲线,这样全部扫描一遍后就可以得到一个新的贝塞尔曲面了~

就像这样

我们还可以将两个不同的t具象为坐标轴的横纵轴,这样就可以具体地得到贝塞尔曲面上的每一个具体的点。

Mesh Operations

讲解完贝塞尔曲线与曲面的内容后,我们来讲讲具体的几何操作。

图上的三种操作分别对应:网格数量的增加,网格的简化与网格的正则化。

首先是细分:

所谓的细分,首先是需要更多三角形,其次是要让三角形的位置发生变化。

比如Loop细分(Loop在此处并不是循环的意思,而只是发明者姓Loop):

Loop细分只需要将原本的一个三角形分别连接三条边的中点,然后一个三角形就会变成四个三角形。接着就是要更改各个顶点的位置:

要更新到哪里呢?以上图为例,两个三角形分别细分成了八个三角形后,我们假设要更改图中的白点,他位于被两个三角形共享的一条边上,这条被共享的边的两个顶点称为A和B,而另外两个顶点称为C和D,于是我们将白点更新到3/8*(A+B)+1/8*(C+D)的位置上。

关于旧的顶点被替换到哪里呢?这与这个顶点相连的三角形多少有关系,或者说旧顶点的位置变换的权重与自己相连的三角形的数量相关。如果一个顶点与众多三角形相连,那么他的位置变换肯定受临边三角形的位置影响大,反之则更看重自己原来的位置。

Loop细分的问题在于只适用于三角形网格,而遇到其他多边形的网格就需要新的细分方法。

首先定义了非四边形与奇异点(度不为4的点都叫奇异点)

我们在每个面都添加一个新的顶点,在每条边也添加一个中点,然后将这些顶点全部连接起来:

和上图比较能够发现:原本的非四边形都消失了,奇异点增加了原本的非四边形的个数,也就是说可以理解成:经过一次这样的变换后,每一个原来的非四边形都变成了一个奇异点。

有具体的算式计算,如上图所示,分为在面上的点与在边上的点以及顶点。

在讲解完网格细分后,我们来到网格简化:不改变原形状的前提下,尽可能减少网格的元素个数。

如上图所示,三万个三角形和三千个三角形在近处还是能明显看出差异,可假如放远了看,其实很难看出差别。

具体如何实现网格简化呢?首先提出边坍缩的方法:

边坍缩的概念其实很简单:我们将原来一条边的两个点捏在一起,这样原来的边就消失了。

问题是我们具体要捏哪条边呢?如何度量哪条边更重要呢?我们引入了一个与机器学习中L2距离概念相似的度量标准:二次误差度量:一个点与所有与他有关联的面的距离的平均和最小。于是我们有了二次误差度量后,我们将原来的边坍缩成一个点,然后根据这个度量将这个点放置到对原图形影响最小的位置,得到坍缩这条边的最小的二次误差度量。我们对每条边挨个进行计算,就可以得到最合适的坍缩的边。

可问题在于,我们将一条边坍缩后,会不可避免地影响其他边,那么这些其他的边坍缩后的二次度量误差也会发生变化,这时候我们又要重新分别计算各个边坍缩的指标...其实不难从上述过程看出,我们适合这种实时更新二次度量误差最小值的数据结构,就是所谓的优先队列,或者叫堆。

Shadow

讲解了几何中的这么多东西后,在开始后续的光线追踪前,我们需要讲解清除如何在光栅化中生成阴影。

为何之前说着色无法处理阴影?

其实说白了shading针对的情况是光的总和,高光,环境光或是别的什么光,总归是需要光才能着色。可是阴影是没有光照的情况,这就在shading的业务范围之外。

生成阴影的关键点在于:不在阴影的点一定能同时被光源和相机看到。

值得一提的是:经典的shadow mapping只能处理点光源。

shadow mapping主要分两步进行:

第一步:我们假设在光源处有一个虚拟的摄像机,它记录从光源出发的能够看到的所有点的深度。

第二步,通过真正的摄像机观察各个点,反推出各个点到光源的深度。

接着只需要比较两个观察视角里的相同的点的深度,如果两个点深度相同证明这个点既可以被光源看到也可以被摄像机看到,反之则证明这个点就处在阴影之中。

shadow map的问题:只能生成硬阴影且只适用于点光源,阴影质量对shadow map的分辨率要求较高,精度问题难以解决。

在这里区分一下所谓的硬阴影与软阴影:硬阴影就是指边缘非常锐利的阴影,而软阴影则反之。

为什么有这两种阴影之分呢?说到底还是光源的大小问题,针对点光源,并不会产生软阴影,因为他的大小太小。

作业四

作业四还是比较简单的,我们只需要画出四个点以及他们的贝塞尔曲线即可:

cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) 
{
    // TODO: Implement de Casteljau's algorithm
    std::vector<cv::Point2f> temp;
    std::vector<cv::Point2f> c_p(control_points);
    while(c_p.size() > 1) 
    {
        for(int i=0; i<c_p.size()-1; ++i) 
        {
            float distX = c_p[i+1].x - c_p[i].x;
            float distY = c_p[i+1].y - c_p[i].y;
            temp.push_back(cv::Point2f(c_p[i].x+t*distX, c_p[i].y+t*distY));
        }
        c_p.clear();
        for(auto x:temp) 
        {
            c_p.push_back(x);
        }
        temp.clear();
    }

    return c_p[0];
}


void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) 
{
    // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's 
    float t = 0.0f;
    for(int i=0; i<1000; ++i) 
    {
        t += 0.001f;
        // recursive Bezier algorithm.
        cv::Point2f point = recursive_bezier(control_points, t);
        window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
     }
}

为什么先写了作业四而不是作业三呢?因为作业三好麻烦QAQ,后续我会复习到目前为止所学的所有内容以及四次(虽然其实是五次)作业的详解(逐行分析的那种),然后到那篇文章我们再把第四次作业的代码也好好地解析一下。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐