前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础渲染系列(一)图形学的基石——矩阵

基础渲染系列(一)图形学的基石——矩阵

作者头像
放牛的星星
发布2020-07-10 17:11:23
4.8K0
发布2020-07-10 17:11:23
举报
文章被收录于专栏:壹种念头壹种念头

本文重点内容:

1、创建一个立方体构建的Grid网格

2、支持缩放、位移、旋转

3、变换矩阵

4、创建简单的相机投影

译注:从原创作者博客转为公众号文章非常复杂,我需要先将原文翻译一遍,然后在公众号再排版一遍。公众号编辑十分不方便,尤其是原作者的代码风格、图片格式、数学公式、动图、视频、引用Tips等等都需要二次导入和格式转换。加上原作者每篇的内容非常长,编辑起来非常耗时,非常累。 另外,我对比了一下使用源码引用和截图在公众号的阅读体验,觉得截图的体验要好于源码引用。截图既能保留原作者源码风格,又能在手机上有良好的阅读体验。 代码的黄色部分,是指在原有代码基础上变化的部分,完整源码会在后台通过回复关键字获取。

这是基础渲染课程系列的第一部分,主要涵盖变换矩阵相关的内容。如果你还不清楚Mesh是什么或者怎么工作的,可以转到Mesh Basics 相关的章节去了解(译注:Mesh Basics系列皆已经翻译完毕,但与本系列主题关联不大,讲完4个渲染系列之后,再放出来)。这个系列会讲,这些Mesh是如何最终变成一个像素呈现在显示器上的。

该示例使用Unity5.3.1(译注:实测2018.4版本没有问题)。

1 空间可视化

你已经知道什么是Mesh网格以及如何在场景中对其进行定位了。但是这种定位实际上是如何完成的呢?着色器如何知道在哪里绘制?当然,我们可以仅依靠Unity的transform组件和着色器来完成所有工作,但是如果你想获得完全控制权,那么了解实际发生的底层原理则至关重要。

为了完全理解此过程,最好创建自己的实现。移动,旋转和缩放网格是通过操纵其顶点的位置来完成的。这属于空间上的变换,因此要在实际中看到它,我们必须使空间可见。可以通过创建用“点”组成的3D网格来实现。点可以是任何预制件。

创建一个点,实际上就是实例化预制件,确定其坐标并为其赋予独特的颜色。

网格最明显的形状是一个立方体,所以让我们开始吧。我们将其以原点为中心,因此变换(尤其是旋转和缩放)相对于网格立方体的中点。

我将使用默认的立方体作为预制对象,将其缩放为一半大小,以便在它们之间留出空间。

(缩小立方体预置)

创建一个网格对象,添加我们的组件,并连接预制件。进入播放模式时,将会以我们对象的本地原点为中心出现方格。

(Transformations Grid)

2 Transformations

理想情况下,我们应该能够对Grid应用任意数量的转换。 以及各种不同类型的转换,但为了和Unity的理解一致,将只限制在位置,旋转和缩放上。

如果我们为每个Transform创建一个组件类型,就可以按照所需的任何顺序和数量将它们添加到Grid对象中。 而且,尽管每个Transform的细节都不同,但它们都需要一种方法将自己应用于空间点。

让我们为所有的Transform组件创建一个可以继承的基类。 它是一个抽象类,这意味着它不能直接使用。 给它一个抽象的Apply方法,具体的转换组件将使用它来完成其工作。

将此类组件添加到网格对象后,就必须以某种方式检索它们,以便将其应用于所有网格点。我们将使用通用List来存储对这些组件的引用。

现在我们可以添加一个Update方法来检索Transform,然后遍历整个网格并转换所有点。

为什么要在Update获取组件? 这样就可以在保持播放模式的同时使用Transform组件,并立即看到结果。

为什么使用List而不是数组? GetComponents方法的最直接的版本只是返回一个包含请求类型的所有组件的数组。 这意味着每次调用都会创建一个新数组,在本例中是每次Update。 替代版本具有列表参数。 这样做的好处是它将把组件放到列表中,而不是创建一个新的数组。 但在我看来,这不是一个关键的优化,但是当你需要经常获取组件时,使用list是个好习惯。

通过获取原始坐标,然后应用每个变换来完成每个点的变换。 但不能依靠每个点的实际位置,因为已经对它们进行了变换,并且我们不想在每个帧上累积变换。

2.1 转换

我们的第一个具体组成部分是Transform,这是最简单的。因此,创建一个扩展了Transformation的新组件,并将其位置用作局部偏移。

现在,编译器将报错说没有提供Apply的具体版本,所以我们给它一个吧。只需将所需位置添加到原始点即可。

现在,你可以将位置转换组件添加到我们的网格对象中。这让我们可以移动“点”,而无需移动实际的网格对象。我们所有的转换都发生在对象的局部空间中。

(变换位置)

2.2 缩放

接下来是缩放转换。它与位置处理方式几乎相同,只是比例分量被乘而不是被添加到原始点。

也把该组件添加到我们的网格对象中。现在我们也可以缩放网格。请注意,我们仅调整网格点的位置,因此缩放不会更改其可视化效果的大小。

(调整缩放)

一次操作中尝试执行定位和缩放。 你会发现比例尺也会影响位置。 发生这种情况是因为我们首先重新定位空间,然后对其进行缩放。但Unity的transform组件是反过来实现的,所以,我们也应该调整下脚本执行的顺序,这可以通过重新排序组件来完成。 通过每个组件右上角齿轮图标下的弹出菜单移动它们。

(修改组件顺序)

2.3 旋转

第三种变换类型是旋转。比前两个要困难一些。我们从一个新组件开始,该组件将返回没有变化的点。

那么旋转该如何实现呢? 它需要限制自己绕单个轴(Z轴)旋转。 围绕该轴旋转点就像旋转一个轮子。 由于Unity使用左手坐标系,因此在Z轴正方向观看时,正向旋转会使车轮逆时针旋转。

(绕着Z轴的2D旋转)

一个点旋转时会发生什么变化呢? 最简单的考虑点位于半径为一个单位的圆(单位圆)上的点。 最直接的点对应于X和Y轴。 如果将这些点旋转90°,则总是以0、1或-1结束。

(将(1,0)和(0,1)分别旋转90和180度)

第一步之后,点(1,0)变为(0,1)。 下一步将其设置为(−1,0)。 然后是(0,-1),最后回到(1,0)。

如果我们从点(0,1)开始,则与之前的序列相比,我们仅领先一步。

我们从(0,1)到(−1,0)到(0,−1)到(1,0)再返回。 因此,我们的点的坐标经历了循环0、1、0,-1。 他们只是有不同的起点而已。

如果改为以45°增量旋转怎么办? 这将产生位于XY平面对角线上的点。 由于到原点的距离没有变化,因此我们必须以(±√½,±√½)形式的坐标结束。 这将我们的周期扩展为0,√½,1,√½,0,-√½,-1,-√½。 如果不断减小步长,则最终会出现正弦波。

(正弦和余弦)

在我们例子里,从(1,0)开始,正弦波与y坐标匹配。 余弦与x坐标匹配。 这意味着我们可以将(1,0)重新定义为(cos z,sin z)(cosz,sinz)。 同样,我们可以将(0,1)替换为(-sin z,cos z)(-sinz,cosz)。

因此,我们首先计算围绕Z轴所需旋转的正弦和余弦。提供以度为单位的角度,但是正弦和余弦使用弧度,因此必须进行转换。

什么是弧度? 像度数一样,它们可以用作旋转的量度。 使用单位圆时,弧度与您沿其圆周行进的距离匹配。 由于圆周的长度等于圆半径的2π倍,因此1个弧度等于π/ 180度。 在这里你还可以看到π的定义。 它是圆的周长与其直径之比。

很高兴我们找到了一种旋转(1,0)和(0,1)的方法,但是旋转任意点呢? 好吧,这两点定义了X和Y轴。 我们可以将任何2D点(x,y)分解为 xX + yY。 没有任何旋转,它等于x(1,0)+ y(0,1),实际上的确是(x,y)。 但是当旋转时,我们现在可以使用x(cos Z,sin Z)+ y(-sin Z,cos Z)并得到正确旋转的点。 你可以将其视为缩放点,使其落在单位圆上,旋转然后再缩小。 压缩成一个坐标对,它变成(xcosZ-ysinZ,xsinZ + ycosZ)。

将旋转组件添加到网格,并将其作为中间转换。 这意味着我们首先缩放,然后旋转,最后重新定位,这也是Unity的Transform组件所做的。 当然,目前仅支持围绕Z旋转。 稍后我们将处理其他两个轴。

(所有的三个转换效果)

3 完全体的旋转

现在,我们只能绕Z轴旋转。 为了提供与Unity变换组件相同的旋转支持,我们还必须启用围绕X和Y轴的旋转。 孤立地绕这些轴旋转的实现就类似于绕Z旋转,但同时绕多个轴旋转则变得更加复杂。 为了解决这个问题,我们可以使用更好的方法来写下旋转数学。

3.1 矩阵

从现在开始,我们将垂直而不是水平地写入点的坐标。用

的写法代替(x,y)。同样的使用

代替(xcosZ−ysinZ,xsinZ+ycosZ)。这样阅读更加容易一些。请注意,x和y因子最终排列在垂直列中,表示一个2D乘法。 实际上,我们执行的乘法是

这是矩阵乘法。2 x 2矩阵的第一列表示X轴,第二列表示Y轴。

(用2D的矩阵定义X和Y轴)

通常,将两个矩阵相乘时,在第一个矩阵中逐行,在第二个矩阵中逐列。 结果矩阵中的每个项是一行的项总和乘以一列的相应项之和。 这意味着第一矩阵的行和第二矩阵的列必须具有相同数量的元素。

(2个2X2的矩阵相乘)

结果矩阵的第一行包含行1×列1,行1×列2,依此类推。 第二行包含第2行×第1列,第2行×第2列,依此类推。 因此,它具有与第一矩阵相同的行数和与第二矩阵相同的列数。

3.2 3D旋转矩阵

到目前为止,我们有一个2 x 2矩阵,可用于绕Z轴旋转2D点。

但我们实际上使用的是3D点。所以我们尝试乘法

因为矩阵的行和列长度不匹配。所以我们必须把我们的旋转矩阵增加到3乘3,以包含第三维空间。如果我们用零来填充它会发生什么?

结果的X和Y分量是正常的,但Z分量始终为零。 那是不对的。 为了保持Z不变,我们必须在旋转矩阵的右下角插入1。 这么做才是对的,因为第三列表示Z轴,即

如果我们一次对所有三个维度都使用此技巧,那么最终将得到一个矩阵,其对角线为1,其他任何地方为0。 这被称为单位矩阵,因为它不会改变与之相乘的关系。 它就像一个过滤器,使所有内容保持不变。

3.3 为X和Y做矩阵旋转

使用我们找到的绕Z轴旋转的相同方式,我们可以得出绕Y轴旋转的矩阵。首先,X轴从

开始,逆时针旋转90°后,变为

这意味着旋转的X轴可以用

来表示。Z轴在其后方相距90°,因此为

Y轴保持不变,从而完成了旋转矩阵。

最后旋转矩阵使X保持不变,并以类似方式调整Y和Z。

3.4 统一旋转矩阵

我们的三个旋转矩阵每个绕单个轴旋转。 为了将它们结合起来,我们必须一个接一个地应用。 让我们先绕Z旋转,然后绕Y旋转,最后绕X旋转。但其实我们可以这样做:首先将Z旋转应用于我们的点,然后将Y旋转应用于结果,然后将X旋转应用于该结果。

同样我们也可以将旋转矩阵彼此相乘。这将产生一个新的旋转矩阵,该矩阵将立即应用所有三个旋转。让我们展示下Y×Z。

结果矩阵的第一项是

整个矩阵需要大量的乘法运算,但是许多部分最终都为0,可以丢弃。

现在再来展示X × (Y × Z) ,这会得到我们最终要的矩阵。

乘法顺序重要吗? X乘以 X×(Y×Z)=(X×Y)×Z的顺序无关紧要。 你最终得到一个不同的中间步骤,但最终结果却相同。 但是,在此方程式中对矩阵重新排序确实会改变旋转顺序,会产生不同的结果。 因此X×Y×Z≠Z×Y×X 在这方面,矩阵乘法不同于单数乘法。 Unity的实际轮换顺序为ZXY。

现在我们有了这个矩阵,可以看到如何构建旋转结果的X,Y和Z轴。

(3个轴任意旋转)

4 矩阵转换

如果我们可以能够将三个旋转方向组合到一个矩阵中,是否还可以将缩放,旋转和重新定位也组合到一个矩阵中?如果我们可以将缩放和重新定位表示为矩阵乘法,那么答案是肯定的。

缩放矩阵很容易构造。取单位矩阵并缩放其分量。

但是我们如何支持重新定位呢? 这不是对三个轴的重新定义,而是一个偏移量。 因此,我们无法用现在拥有的3 x 3矩阵表示它。 我们需要另外一列来包含偏移量。

但是,这是无效的,因为矩阵的行长已变为4。因此,我们需要在点上添加第四个组件。 当此分量与偏移量相乘时,它应该为1。我们想要保留该1值,因此可以在进一步的矩阵乘法中使用它。 这会导致一个4×4矩阵和一个4D点。

因此,我们必须使用4 x 4转换矩阵。 这意味着缩放和旋转矩阵会获得额外的行和列,其中右下角的数字为0,而数字为1。 我们所有的点都得到第四坐标,该坐标始终为1。

4.1 齐次坐标

我们可以理解第四个坐标吗?它代表什么有用的东西呢?我们现在知道给它赋予值1可以实现点的重新定位。如果其值为0,则偏移量将被忽略,但缩放和旋转仍会发生。

可以缩放和旋转但不能移动的东西。那不是点,而是向量,代表一个方向。

所以

代表一个点,而

表示向量。这概念很有用,因为这意味着我们可以使用相同的矩阵来变换位置,法线和切线。

如果当第四个坐标得到的值不是0或1时会发生什么呢? 好吧,不应该有这种情况发生。 或实际上,它没有区别。 我们现在正在使用齐次坐标。 这个想法是,空间中的每个点都可以用无限数量的坐标集表示。 最直接的形式使用1作为第四坐标。 通过将整个集合乘以任意数字,可以找到所有其他选择。

因此,要获得欧几里得点(实际的3D点),请将每个坐标除以第四个坐标,然后将其丢弃。

当然,当第四个坐标为0时,这是行不通的。这些点被定义为无限远。这就是为什么它是表现为方向的。

4.2 使用矩阵

我们可以使用Unity的Matrix4x4结构执行矩阵乘法。从现在开始,我们将使用它来执行转换,而不是之前的方法。

将一个抽象的只读属性添加到Transformation中以检索转换矩阵。

它的Apply方法不再需要抽象。将仅获取矩阵并执行乘法。

请注意,Matrix4x4.MultiplyPoint具有3D矢量参数。 假定缺少的第四坐标为1。它还负责从齐次坐标转换回欧几里得坐标的工作。 如果是要乘以一个方向而不是一个点,则可以使用Matrix4x4.MultiplyVector。

现在,具体的转换类必须将其Apply方法更改为Matrix属性。 首先是PositionTransformation。Matrix4x4.SetRow方法提供了一种方便的方式来填充矩阵。

接下来是ScaleTransformation。

对于RotationTransformation,逐列设置矩阵会更方便,因为这与我们现有的代码匹配。

4.3 组合矩阵

现在,让我们将这些Transform矩阵合并为一个矩阵。将一个Transform矩阵字段添加到TransformationGrid。

我们将在每次Update时更新此转换矩阵。这需要先获取第一个矩阵,然后将其与所有其他矩阵相乘。确保它们以正确的顺序相乘。

现在,网格不再调用Apply,而是自己执行矩阵乘法。

这种新方法效率更高,因为我们曾经分别为每个点创建每个Transform矩阵,然后分别应用它们。 现在,我们一次创建一个统一的转换矩阵,并将其重新用于每个点。Unity使用相同的技巧把每个对象层次结构简化为一个Transform矩阵。

对我们而言,我们可以使其变得更加高效。 所有变换矩阵都具有相同的底行[0 0 0 1]。 知道了这一点,我们就可以忽略该行,而跳过0的计算和最后的转换除法。Matrix4x4.MultiplyPoint4x3方法就是这么做的。 但是,我们不会使用该方法,因为有一些有用的转换会改变底部的行。

5 投影矩阵

到目前为止,我们一直在将点从3D中的一个位置转换为3D空间中的另一个位置。但是这些点最终如何在2D显示器上绘制呢?这需要从3D空间转换为2D空间。我们可以为此创建一个Transform矩阵!

对相机投影进行新的具体转换。从单位矩阵开始。

将其添加为最终转换。

(相机投影最终结果)

5.1 正交相机

从3D到2D的最直接方法是简单地放弃一个维度。这会将3D空间折叠成一个平面。该平面就像画布一样,用于渲染场景。让我们放弃Z维度试试,看看会发生什么。

(正交投影)

实际上,网格变为2D了。但你仍然可以缩放,旋转和重新放置所有内容,之后会将其投影到XY平面上。这是基本的正交摄影机投影。

我们的原始相机位于原点,并朝正Z方向看。 那我们可以移动它并旋转它吗? 是的,事实上我们已经可以做到了这一点。 移动相机与向相反方向移动世界具有相同的视觉效果。 旋转和缩放也是如此。 因此,尽管有点尴尬,但我们可以使用现有的转换来移动相机。Unity使用矩阵求逆来做同样的事情。

5.2 透视摄像机

正交摄影机很好,但不能像我们看到的那样显示世界。 为此,我们需要一个透视相机。 由于视角的原因,距离较远的事物对我们来说显得较小。 我们可以根据点与相机的距离缩放比例来重现此效果。

将所有内容除以Z坐标。 我们可以用矩阵乘法吗? 是的,通过将单位矩阵的底部行更改为[0,0,1,0]。 这将使结果的第四个坐标等于原始Z坐标。 从齐次坐标转换为欧几里得坐标,然后进行所需的划分。

正交投影的最大区别是点不会直接向下移动到投影平面。 相反,它们会朝着相机的位置(原点)移动,直到撞到切面。 当然,这仅适用于摄像机前面的点。 相机后面的点会被错误地投影。 由于现在我们不会丢弃这些点,因此先通过重新定位确保所有内容都位于相机的前面。 如果不缩放或旋转网格,则5的距离就足够了,否则你可能需要更多。

(透视投影)

原点和投影平面之间的距离也会影响投影。 它的作用就像照相机的焦距。 焦距的越大,视野就越小。 现在,我们使用的焦距为1,可产生90°的视野。 让它可以配置。

(焦距)

由于更大的焦距意味着我们正在放大,有效地增加了终点的比例,因此我们可以采用这种方式进行支持。当我们折叠Z尺寸时,不需要缩放该尺寸。

我们现在有一个非常简单的透视相机。 如果要完全模仿Unity的相机投影,我们还必须处理近距和远距平面。 这将需要投影到立方体而不是平面中,因此深度信息需要保留下来。 再有就是要关心视图纵横比。 另外,Unity的相机朝负Z方向看,还需要取反一些数字。 你可以将所有内容合并到投影矩阵中。 大家可以自己尝试构建。

那么,这一章节的意义何在? 我们很少需要自己构造矩阵,并且绝对不需要构造投影矩阵。 其实最主要是你已经能了解它们的背后发生了什么。 矩阵并不可怕,它们只是将点和向量从一个空间转换到另一个空间。 而且你现在也已经知道了,这就很好了,因为一旦我们开始编写自己的着色器时,你会再次遇到矩阵。

我们将在第2部分“着色器基础知识”中进行此操作。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 空间可视化
  • 2 Transformations
    • 2.1 转换
      • 2.2 缩放
        • 2.3 旋转
        • 3 完全体的旋转
          • 3.1 矩阵
            • 3.2 3D旋转矩阵
              • 3.3 为X和Y做矩阵旋转
                • 3.4 统一旋转矩阵
                • 4 矩阵转换
                  • 4.1 齐次坐标
                    • 4.2 使用矩阵
                      • 4.3 组合矩阵
                      • 5 投影矩阵
                        • 5.1 正交相机
                          • 5.2 透视摄像机
                          相关产品与服务
                          对象存储
                          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档