实现OpenGL渲染器原理篇(四)——透视投影

最近在Github上复现了一个渲染器render的项目:
Github链接:tiny render

我希望在博客上可以记录自己的学习过程,博客主要分为两大类:《原理篇》和《语法篇》。
原理篇则主要讲的是——实现渲染器过程中所需要的算法知识。
语法篇则主要讲的是——实现渲染器过程中使用到的C++的语法知识。


一、The goal

在之前的3篇博客中,大家通过简单地忽略z坐标来渲染正投影模型。
也就是说,我们之前的目标是渲染一个正投影模型
今天的目标是如何画透视图

The goal

我们现在完成透视图的绘制,最终可以完成全3D的渲染


二、2D几何

2.1 线性转换

平面上的线性变换可以由相应的矩阵表示。
如果我们取一个点(x,y),那么它的变换可以写成下面这样:

变换1

最简单的(不是退化的)变换是单位矩阵,它一点都不会动

变换2

矩阵的对角系数沿坐标轴缩放。

让我来说明一下,如果大家进行以下转换

变换3

然后,白色物体(一个角被切掉的白色正方形)将变成黄色
红色和绿色线段分别给出了与x和y对齐的单位长度向量:

白色object

我们为什么要用矩阵来解决问题呢?因为它很方便。

首先,在矩阵形式中我们可以像这样表示整个物体的变换

变换矩阵

在此表达式中,变换矩阵与上一个矩阵相同,但是2x5矩阵就是我们的方形物体的顶点

我们简单地将所有顶点放在数组中,然后将其乘以转换矩阵,即可获得转换后的对象

真正的原因是这样: 大部分人经常希望通过许多连续的转换来转换对象。

想象一下,在你的源代码中写了这样的转换函数——

vec2 foo(vec2 p) 
    return vec2(ax+by, cx+dy);
vec2 bar(vec2 p)
    return vec2(ex+fy, gx+hy);
[..]

for (each p in object)
{
    p = foo(bar(p));
}

这段代码对对象的每个顶点执行两次线性转换,我们通常以百万计的次数来计算这些顶点。

连续进行数十次的转换并不少见,从而导致数以千万计的操作是非常昂贵的。

在矩阵形式中,我们可以对所有的变换矩阵进行预乘,并对我们的对象进行一次变换。

对于只有乘法的表达式,我们可以在所需的地方加上括号,可以吗?

大家应该知道,事实上,矩阵的对角系数是沿着坐标轴缩放的。

让我们考虑下面的转换:

对角系数

以下就是它对物体的作用

白色物体变换

通过观察上图,大家可以发现,它是沿着x轴的简单剪切

另一个对角元素则是沿y轴剪切空间。因此,在一个平面上有两个基本的线性变换缩放和剪切

当然,看到这里,大家或许会反应: 那旋转呢?

事实证明,任何旋转(绕原点的旋转)都可以表示为三个剪切的组合动作——

这里白色对象被转换为红色对象,然后转换为绿色对象,最后转换为蓝色对象(白色换红色的时候是x轴的移动,然后红色换绿色的时候是y轴的移动,绿色换为蓝色则又是x轴的移动):

旋转

但是这些都是复杂的细节,为了简单起见,可以直接编写一个旋转矩阵(大家都还记得预乘技巧吗?):

旋转矩阵

大家可以将矩阵以任何顺序相乘,但请记住,矩阵的乘法是不可交换的

image.png

这下讲得通了: 先剪切一个物体,然后旋转它,与先旋转它,然后剪切它是不一样的!

先剪切一个物体,后旋转它,与先旋转它,后剪切它是不一样的!

上面两张图可以进行比较,大家可以看看。


三、2D仿射变换

因此,平面上的任何线性变换都是比例变换和剪切变换组合
这意味着我们可以做任何我们想做的线性变换,而原点,则永远不会移动

是的,平移不是线性的。大家可以试着在完成线性部分后加上平移

加号左边则是线性转换部分,加号右边则是平移

上面这个表达式也真的很酷,它告诉我们可以旋转缩放剪切平移

但是我们需要注意:我们的目标是组合多个转换

下面的表达式则是两个转换的组合(记住,我们需要组合许多这样的转换):

括号中的左边是线性转换,右边是平移部分。下来对得到新的括号中的坐标,再进行第2轮线性变换,然后第2轮的平移变换

大家需要明白一点,即使仅仅是单一的合成物,看起来也会很难看,那么如果后面添加更多的东西,看起来则会变得更糟


四、齐次坐标

好,下来带大家看看什么叫黑科技

想象一下,我们要将一列纵行一行横行添加到我们的转换矩阵中(因此使其变为3x3),并将一个始终等于1的坐标附加到将要转换的向量上:

上图中,左边3×3则是转换矩阵,3×1则是要转换的向量,等式右边则是结果

如果我们将转换矩阵乘以加了1后的矢量,我们将得到一个新的向量(最后一个元素是1),如上图右边所示。而元素1上面的其他两个分量表达的形状恰好是我们想要的!

大家需要明白一点,在2D空间中,平行平移不是线性的

因此,我们将2D嵌入到3D空间中(只需将1添加到第3个元素即可)。

这意味着我们的2D空间是建立在3D空间中的平面z = 1的基础上的。

然后,我们执行线性3D变换并将结果投影到我们的2D物理平面上。

平行平移不是线性的,但是传递路径是简单的。

那么大家简单地想想,如何将3D投影回2D平面呢?只需用前两个元素向量除以第三个向量分量即可:

将3D投影回2D平面

五、不要除以零!

OK,让我们一起先回顾以下传递路径

  • 我们将2D嵌入3D,方法是将它置于平面z=1内
  • 我们在3D中做任何我们想做的事;
  • 对于要从3D投影到2D的每个点,我们在原点和要投影的点之间绘制一条直线,然后找到其与平面z = 1的交点

在此图像中,我们的2D平面为品红色,点(x, y, z)投影到了点(x / z, y / z, 1)上(回顾一下上面说的3个步骤):

从3D投影到2D

假设有一条垂直的直线穿过点(x,y,1)。那么点(x,y,1)会投影在哪里?

没错,在(x, y)上

3D空间点(x, y, 1)投影到2D平面上的点(x, y, 0)上

现在我们沿着直线向下,例如,3D坐标点(x, y, 1/2)会投影到2D坐标点(2x, 2y, 0)上:

3D空间点投影到2D空间上对应的点

不要停,我们继续点(x, y, 1/4)变成了点(4x, 4y, 0)

同上的栗子

如果我们继续这个过程趋近于z=0,那么其投影将沿着(x,y)方向上,离原点越来越远

换句话说,点(x,y,0)会投影到(x,y)方向上一个无限远的点上。

没错!那这个是个什么呢?很简单,一个向量而已!

大家需要记住一点,齐次坐标是可以区分向量和点的。

我想问问大家,如果一个程序员写了vec2(x,y),那么它是一个向量还是一个点
答案可以这样讲:

  • 在齐次坐标中,z = 0的所有事物都是向量,其余均为

注意:

  • vector + vector = vector.
  • vector - vector = vector.
  • point + vector = point.

六、复合转换

正如我之前所说的,大家在这个过程中应该能够积累数十个转换

让我们想象一下,如果我们需要围绕点(x0,y0)旋转一个对象(2D),我们需要怎么做呢?

可以查查公式呀,可以手算,都可以的!

因为大家知道如何围绕原点旋转,如何平移。这些就是所有的我们需要的了!

大家可以将点(x0, y0)平移到原点旋转反平移,完成:

平移旋转反平移

在3D中,动作序列会更长一些,但思路是一样的:我们需要知道一些基本的转换,在它们的帮助下,我们可以表示任何组合的动作


七、Touch一下神奇的3×3矩阵的底排

让我们将以下转换应用于我们的标准方形对象

新的转换

大家回想一下,原始对象是白色的,单位轴向量是红色和绿色的。

原始对象是白色的,单位轴向量是红色和绿色的

下面是转换后的对象

image.png

在这里,另一种魔法(白色!)发生了

大家还记得我们之前ybuffer区的练习吗?

接下来大家只需要做同样的事就可以了:将2D对象投影到x=0的垂直线上。

让我们对规则再进行一些加强:大家必须使用中心投影,我们的相机是位于点(5,0)的,并且是指向原点的

为了找到投影,我们需要跟踪相机和要投影的点之间的直线(黄色线),并找到与屏幕线(白色垂直线)的交点

跟踪相机和要投影的点之间的直线(黄色线),并找到与这条黄色线和屏幕线(白色垂直线)之间的交点

现在我用转换后的对象替换原来的对象,但是我没有触及到我们之前画的黄线

用转换后的对象替换原来的对象,但是没有触及到之前画的黄线

如果我们用标准的正交投影红色的物体投影到屏幕上,那么我们会找到完全相同的点

让我们仔细看看转换是如何进行的:所有的垂直部分都还是转换成了垂直部分,但是那些靠近摄像机的部分被拉长了,那些远离摄像机的部分被缩小了。

如果我们正确选择系数(在我们的变换矩阵中是-1/5系数),我们将获得透视(中央)投影的图像


八、是时候开启全3D的渲染了!

基于2D仿射变换,大家可以使用齐次坐标得到3D仿射变换点(x, y, z)增加一维,数值为1,变成(x, y, z, 1),接下来我们将它转换为4D,然后重新投影为3D

举个例子,如果我们进行以下转换:

将3D转换为4D

逆投影为我们提供了以下3D坐标

4D转换为3D

大家先记住这个结果,但先把它放在一边。

让我们回到中心投影的标准定义,没有任何花哨的4D变换

给定一个点P =(x, y, z),我们要将其投影到平面z = 0的面上,相机位于点(0, 0, c)的z轴上:

中心投影

三角形ABC和ODC相似的,这意味着我们可以写下如下的等式:
|AB|/|AC|=|OD|/|OC| => x/(c-z) = x'/c

换句话说,

新的等式1

通过对三角形CPB和CP'D进行相同的推理,很容易找到以下表达式:

新的等式2

这和我们几分钟前放到一边的结果很相似,但是我们通过单矩阵乘法得到了结果。
我们得到了求系数的规律:r = -1/c


九、总结:今日重要的公式

如果我们想要用一个相机计算中心投影

相机位于距离原点为c的Z轴上

然后我们通过增加1把这个点嵌入到4D中(公式1)

然后我们将它与下面的矩阵(公式2左边的矩阵)相乘,并将其反投影到3D中(公式3)。

重要公式截图

我们以某种方式使对象变形,只需忽略其z坐标,我们就可以得到透视图

如果我们要使用z缓冲区,那么自然不要忘记z


学习之初,我整理了每节课中代码的变化,现在看来用处不大,但是删了可惜,大家有需要的可以看看哈~~

这篇博客用到的代码文件的变化是这样的:

  • tgaimage.h

(初始导入)->(新光栅化器+z-buffer)->(初始导入)->(diffuse texture work)

  • tgaimage.cpp

(初始导入)->(新光栅化器+z-buffer)->(初始导入)

  • african_head.obj

(线框渲染)->(新光栅化器+z-buffer)->(matrix class,立方体模型)->(textures)

  • geometry.h

(线框渲染)->(新光栅化器+z-buffer)->(templates)->(投影和viewport矩阵)

  • geometry.cpp

(templates)->(投影和viewport matrices矩阵)

  • main.cpp

(朴素线段追踪)->(线段追踪、减少划分的次数)->(线段追踪:all integer Bresenham)->(线框渲染)->(better test triangles)->(三角形绘制routine)->(背面剔除 + 高洛德着色)->(y-buffer!)->(新光栅化器+z-buffer)->(templates)->(投影和viewport matrices矩阵)

  • model.cpp

(线框渲染)->(新光栅化器+z-buffer)->(线框渲染)->(diffuse texture homework)

  • model.h

(线框渲染)->(diffuse texture homework)

解释一下上述文件括号中的文字——
model.h开始变化,从线框渲染变成了diffuse texture work
model.cpp则是变回了线框渲染,然后又变成了diffuse texture work
main.cpp加入了templates,后面又应用到了投影和viewport matrices矩阵,其中viewport矩阵有多个,所以是matrices。
新增了一个文件geometry.cpp,也是加入了templates,后续也是加入了投影和viewport matrices矩阵
geometry.h也是从光栅格器 + z-buffer变成了templates,最后成为了投影和viewport matrices矩阵
.obj文件的作用此番变成了矩阵类,立方体模型,后面呢,又成为了textures
tgaimage.h/.cpp则均是由光栅格化器 + z-buffer回到了初始导入,但是tgaimage.h第二次加入了diffuse texture homework

其中model时用来test测试的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容