适合图形渲染建模的配置(基础渲染系列一)

本文重点内容:

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

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

3、变换矩阵

4、创建简单的相机投影

译注:从原创作者博客转为公众号文章非常复杂,我需要先将原文翻译一遍,然后在公众号再排版一遍。公众号编辑十分不方便,尤其是原作者的代码风格、图片格式、数学公式、动图、视频、引用Tips等等都需要二次导入和格式转换。加上原作者每篇的内容非常长,编辑起来非常耗时,非常累。

另外,我对比了一下使用源码引用和截图在公众号的阅读体验,觉得截图的体验要好于源码引用。截图既能保留原作者源码风格,又能在手机上有良好的阅读体验。

代码的黄色部分,是指在原有代码基础上变化的部分,完整源码会在后台通过回复关键字获取。

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

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

适合图形渲染建模的配置(基础渲染系列一)(1)

1 空间可视化

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

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

适合图形渲染建模的配置(基础渲染系列一)(2)

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

适合图形渲染建模的配置(基础渲染系列一)(3)

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

适合图形渲染建模的配置(基础渲染系列一)(4)

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

适合图形渲染建模的配置(基础渲染系列一)(5)

(缩小立方体预置)

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

适合图形渲染建模的配置(基础渲染系列一)(6)

适合图形渲染建模的配置(基础渲染系列一)(7)

(Transformations Grid)

2 Transformations

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

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

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

适合图形渲染建模的配置(基础渲染系列一)(8)

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

适合图形渲染建模的配置(基础渲染系列一)(9)

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

适合图形渲染建模的配置(基础渲染系列一)(10)

为什么要在Update获取组件?

这样就可以在保持播放模式的同时使用Transform组件,并立即看到结果。

为什么使用List而不是数组?

GetComponents方法的最直接的版本只是返回一个包含请求类型的所有组件的数组。 这意味着每次调用都会创建一个新数组,在本例中是每次Update。 替代版本具有列表参数。 这样做的好处是它将把组件放到列表中,而不是创建一个新的数组。

但在我看来,这不是一个关键的优化,但是当你需要经常获取组件时,使用list是个好习惯。

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

适合图形渲染建模的配置(基础渲染系列一)(11)

2.1 转换

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

适合图形渲染建模的配置(基础渲染系列一)(12)

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

适合图形渲染建模的配置(基础渲染系列一)(13)

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

适合图形渲染建模的配置(基础渲染系列一)(14)

适合图形渲染建模的配置(基础渲染系列一)(15)

(变换位置)

2.2 缩放

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

适合图形渲染建模的配置(基础渲染系列一)(16)

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

适合图形渲染建模的配置(基础渲染系列一)(17)

适合图形渲染建模的配置(基础渲染系列一)(18)

(调整缩放)

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

适合图形渲染建模的配置(基础渲染系列一)(19)

(修改组件顺序)

2.3 旋转

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

适合图形渲染建模的配置(基础渲染系列一)(20)

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

适合图形渲染建模的配置(基础渲染系列一)(21)

(绕着Z轴的2D旋转)

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

适合图形渲染建模的配置(基础渲染系列一)(22)

(将(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,-√½。 如果不断减小步长,则最终会出现正弦波。

适合图形渲染建模的配置(基础渲染系列一)(23)

(正弦和余弦)

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

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

适合图形渲染建模的配置(基础渲染系列一)(24)

什么是弧度?

像度数一样,它们可以用作旋转的量度。 使用单位圆时,弧度与您沿其圆周行进的距离匹配。 由于圆周的长度等于圆半径的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)。

适合图形渲染建模的配置(基础渲染系列一)(25)

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

适合图形渲染建模的配置(基础渲染系列一)(26)

(所有的三个转换效果)

3 完全体的旋转

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

3.1 矩阵

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

适合图形渲染建模的配置(基础渲染系列一)(27)

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

适合图形渲染建模的配置(基础渲染系列一)(28)

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

适合图形渲染建模的配置(基础渲染系列一)(29)

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

适合图形渲染建模的配置(基础渲染系列一)(30)

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

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

适合图形渲染建模的配置(基础渲染系列一)(31)

(2个2X2的矩阵相乘)

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

3.2 3D旋转矩阵

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

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

适合图形渲染建模的配置(基础渲染系列一)(32)

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

适合图形渲染建模的配置(基础渲染系列一)(33)

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

适合图形渲染建模的配置(基础渲染系列一)(34)

适合图形渲染建模的配置(基础渲染系列一)(35)

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

适合图形渲染建模的配置(基础渲染系列一)(36)

3.3 为X和Y做矩阵旋转

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

适合图形渲染建模的配置(基础渲染系列一)(37)

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

适合图形渲染建模的配置(基础渲染系列一)(38)

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

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

适合图形渲染建模的配置(基础渲染系列一)(39)

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

适合图形渲染建模的配置(基础渲染系列一)(40)

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

适合图形渲染建模的配置(基础渲染系列一)(41)

3.4 统一旋转矩阵

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

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

结果矩阵的第一项是

适合图形渲染建模的配置(基础渲染系列一)(42)

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

适合图形渲染建模的配置(基础渲染系列一)(43)

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

适合图形渲染建模的配置(基础渲染系列一)(44)

乘法顺序重要吗?

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

Unity的实际轮换顺序为ZXY。

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

适合图形渲染建模的配置(基础渲染系列一)(45)

适合图形渲染建模的配置(基础渲染系列一)(46)

(3个轴任意旋转)

4 矩阵转换

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

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

适合图形渲染建模的配置(基础渲染系列一)(47)

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

适合图形渲染建模的配置(基础渲染系列一)(48)

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

适合图形渲染建模的配置(基础渲染系列一)(49)

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

4.1 齐次坐标

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

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

所以

适合图形渲染建模的配置(基础渲染系列一)(50)

代表一个点,而

适合图形渲染建模的配置(基础渲染系列一)(51)

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

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

适合图形渲染建模的配置(基础渲染系列一)(52)

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

适合图形渲染建模的配置(基础渲染系列一)(53)

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

4.2 使用矩阵

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

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

适合图形渲染建模的配置(基础渲染系列一)(54)

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

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

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

适合图形渲染建模的配置(基础渲染系列一)(55)

接下来是ScaleTransformation。

适合图形渲染建模的配置(基础渲染系列一)(56)

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

适合图形渲染建模的配置(基础渲染系列一)(57)

4.3 组合矩阵

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

适合图形渲染建模的配置(基础渲染系列一)(58)

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

适合图形渲染建模的配置(基础渲染系列一)(59)

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

适合图形渲染建模的配置(基础渲染系列一)(60)

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

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

5 投影矩阵

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

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

适合图形渲染建模的配置(基础渲染系列一)(61)

将其添加为最终转换。

适合图形渲染建模的配置(基础渲染系列一)(62)

(相机投影最终结果)

5.1 正交相机

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

适合图形渲染建模的配置(基础渲染系列一)(63)

适合图形渲染建模的配置(基础渲染系列一)(64)

适合图形渲染建模的配置(基础渲染系列一)(65)

适合图形渲染建模的配置(基础渲染系列一)(66)

(正交投影)

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

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

5.2 透视摄像机

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

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

适合图形渲染建模的配置(基础渲染系列一)(67)

适合图形渲染建模的配置(基础渲染系列一)(68)

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

适合图形渲染建模的配置(基础渲染系列一)(69)

适合图形渲染建模的配置(基础渲染系列一)(70)

(透视投影)

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

适合图形渲染建模的配置(基础渲染系列一)(71)

适合图形渲染建模的配置(基础渲染系列一)(72)

(焦距)

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

适合图形渲染建模的配置(基础渲染系列一)(73)

适合图形渲染建模的配置(基础渲染系列一)(74)

适合图形渲染建模的配置(基础渲染系列一)(75)

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

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

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

本系列会在Unity社区、知乎、个人公众号:壹种念头 进行连载发布。公众号会首发,欢迎关注。

适合图形渲染建模的配置(基础渲染系列一)(76)

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页