webgl自学笔记——矩阵变换

这章主要探讨矩阵,这些矩阵代表了应用在我们场景上的变换,允许我们移动物体。然而在webGL api中并没有一个专门的camera对象,只有矩阵。好消息是使用矩阵来取代相机对象能让webgl在很多复杂动画中拥有更高的灵活性。

第四章中主要内容:

1、了解场景从3d世界到二维屏幕所经历的变换

2、学习仿射变换

3、将矩阵匹配到ESSL uniforms变量中

4、了解Model-View矩阵和透视投影矩阵

5、构造法线变换矩阵

6、创建一个相机对象使用它来旋转3d场景

WebGL中并没有一个可以操控的相机对象。然而我们可以假设我们渲染在canvas上的场景就是我们的相机捕获的。这章中我们将学习用4x4矩阵来表达一个相机对象。

每次我们移动我们的相机时,都需要根据相机的新位置跟新相机中的对象。为了达到目的,我们需要系统的处理每个顶点来将变换应用上去,以便产生新的视点坐标位置。类似的,我们需要保证对象的发现和光照方向在相机移动后仍能保持一致。所以我们需要分析两种变换:顶点和法线

webgl场景中的物体在我们的屏幕看到他们之前会经历多种不同的变换。每一个变换都用4x4的矩阵来表达。如何将只有xyz属性的顶点与4x4矩阵相乘?简要回答是将我们的元组增加一个维度,每一个顶点现在拥有4个维度了,这种坐标表达方式成为齐次坐标。

Homogeneous coordinates

齐次坐标系在任何计算机图形程序中都是关键的一部分。它使得我们能够用4x4矩阵来表达仿射变换(旋转、缩放、切变、平移)和投影变换。在齐次坐标系中,顶点拥有四个部分:x、y、z、w。前三个部分是顶点在欧几里得空间中的坐标。第四个部分是透视部分。所以四元组(x, y, z, w)将我们带到了一个新的空间:投影空间。

齐次坐标系使得我们可以在一中特殊的方程组中求出解,这个方程组中每一个方程都表示一个与系统中其他直线平行的直线。我们知道在欧几里得空间中,对这种方程组是无解的,因为他们没有交点。然而在投影空间中,这种方程序有解,这些直线将会在无穷远处相交。这实际上代表了他们的透视部分的值是0.关于这种观点的一个很好的物理类比就是想象一下所有的平行铁路都会来视线的终点处相交。

从齐次坐标系变换到欧几里得坐标系是很容易的。

反过来,从欧几里得空间到投影空间,我们只需要加上第四维并将它设置为1.

从齐次坐标系到欧几里得坐标系,我们除以w即可;从欧几里得坐标系到齐次坐标系我们将第四维设置为1.w=0的其次坐标代表一个在无穷远处的点。

另一个关于齐次坐标系需要知道的事情是,顶点有一个w=1的齐次坐标,向量的齐次坐标w=0.所以在Phong顶点着色器中,法线向量的处理如下:

vNormal = vec3(uNMatrix * vec4(aVertexNormal, 0.0));

顶点的处理如下:

//Transformed vertex position

vec4 vertex = uMVMatrix * vec4(aVertexPosition, 1.0);

下面来看一下我们的顶点在呈现在屏幕前需要经历的一系列变换:

Model transform

我们从对象坐标系来开始我们的分析。顶点坐标在这个空间中被指定(类似矢量切片)。然后如果我们想移动或者旋转对象,我们会使用一个矩阵来编码这些变换。这个矩阵被称为 模型矩阵 。一旦我们用模型矩阵来乘以我们对象中的顶点,我们将会获得新的顶点坐标。这些新的顶点决定物体在我们的3d世界中的位置。

在对象坐标系中,每个物体都可以自由定义它的原点然后指定它的顶点相对于原点的位置;在世界坐标系中,原点被所有的物体所共享。世界坐标系允许我们知道物体相对于其他物体的位置。通过模型变换我们可以决定对象在3d世界中的位置。

View transform

接下来的变换是视点变换,将坐标系的原点转换到视点。视点是我们的眼睛或者相机相对于世界原点的位置。换句话说,视图变换是通过视点坐标来切换世界坐标。这个变换可以用视点矩阵来编码。我们通过模型变换后的顶点坐标来乘以这个矩阵。这个操作的结果是得到原点是视点的一堆顶点坐标。我们将在这个坐标系中来操作我们的相机。

Projection transform

下面的操作被称为投影变换。这个操作决定了多大的视点空间将被渲染和它将如何被匹配到计算机屏幕上。这个区域被称为视锥体,它由六个平面来定义(近平面、远平面、上底面、下底面、左平面、右平面),如下所示:

这六个平面被编码在透视矩阵中。任何坐落于视锥体外面的顶点在应用投影变换后将被裁剪,并在后续的处理中被抛弃。经过投影变换后的空间成为裁切空间。(透视矩阵由视锥体产生,经过透视矩阵变换后的空间称为裁切空间,顶点的w坐标可能不在是1)

视锥体的形状和范围决定了从3d视点空间到2d屏幕的投影类型。如果远平面和近平面拥有相同的维度,视锥体将产生一个正射投影。否则将产生透视投影,如下图所示:

目前为止我们仍然是在齐次坐标系中工作,所以裁切坐标有四个部分:x、y、z、w。裁剪是通过比较x、y、z与齐次坐标w。如果其中任何一个大于+w或者小于-w,那么这个顶点位于视锥体外并被抛弃。

Perspective division

一旦决定了多大的视域空间将被渲染,视锥体将被匹配到近平面来为了产生一张2d图片。近景面的内容是将被渲染到我们计算机屏幕的内容。

不同的操作系统和显示设备会有不同的机制来渲染2d信息到屏幕上。为了保证所有情况下的健壮性,webgl提供一个独立于任何硬件的中介坐标系统。这个空间被称为归一化设备坐标(NDC)。

归一化设备坐标通过将获得的裁剪坐标除以w分量获得。这也就是为什么这一步叫做透视除法。另外要记住当你除以齐次坐标后,我们从投影空间转到了欧几里得空间,所以NDC坐标只有三个分量。在NDC坐标中,x y坐标代表了你的顶点在归一化2d屏幕上的位置,而z坐标编码了深度信息,即物体相对于近景面和远景面的位置。尽管在这里我们工作在2d屏幕上,我们仍然保持着深度信息。这允许webgl后面基于物体到近景面的距离来决定如何显示物体的叠盖关系。 当使用归一化设备坐标后,深度信息被编码在了z分量上。

透视除法将视锥体变换精一个立方体中,原点在立方体的中心,最小坐标是(-1, -1, -1)最大坐标是(1, 1, 1).而且z轴的方向是相反的。如下所示:

Viewport transform

最后NDC被匹配到视口坐标。这一步将这些坐标映射到屏幕上的可视区域。在webgl中,这个区域由html5的canvas提供,如下图所示:

与之前的步骤不同,视口变换不是由矩阵变换产生的。在这里我们使用webgl的viewport函数。

Normal transformations法线变换

当顶点被变换后,法线向量也需要被变换,以便他们能够指向正确的方向。我们可以考虑用模型视图矩阵来做这件事,但是存在一个问题:模型视图矩阵并不一定能够保持发现的垂直性。

这个问题发生在单轴缩放或者切变的矩阵中。在我们的例子中,我们有一个经过延y轴缩放的三角形。我们能够看到在应用完模型视图矩阵后,法线N'不在是该表面的法线。所以我们要重新计算法线矩阵。

Calculating the Normal matrix

如果两个向量垂直,他们的点乘积是0.

N*S = 0

S是通过表面两个不同的顶点构造的一个向量。

M作为模型视图矩阵。我们可以使用M来变换S:

S' = MS

我们想要找到一个矩阵K来允许我们做类似的变换。对于法线N:

N' = KN

变换后仍然会保持N'与S'的垂直性:

N'*S' = 0

然后将N'和S'替换:

(KN)*(MS) = 0

由于向量可以表示成1x3的矩阵,所以点乘可以表示为对一个向量矩阵的转置与第二个向量相乘:

(KN)T(MS) = 0

矩阵相乘的转置等于矩阵的转置逆序相乘:

NTKTMS = 0

矩阵相乘满足结合律,所以:

NT(KTM)S = 0

因为N*S=0,所以NTS = 0.所以这代表(KTM)=I,因为只有标准单位对角矩阵才使得NI=N;

综上结论:

  1. K是能够保持法线向量与表面向量垂直的正确的变换矩阵。我们将K成为法线矩阵。
  2. K由模型视图矩阵的逆矩阵转置获得
  3. 我们需要使用K来乘以法线向量以便他们能够跟被变换后的表面保持垂直。

WebGL implementation WebGL的实现方式

现在我们来看一下我们是如何在webgl中实现顶点和法线的变换。下面的图展示了我们在理论上所学到的和理论与webgl实践的关系:

在webgl中,我们应用于物体上坐标来得到视口坐标的五个变换被组织在三个矩阵和一个webgl方法中:

1、模型视图矩阵将模型变换和视点变换组织在一个矩阵中。当我们通过这个矩阵来乘以我们的顶点,我们就得到的视点坐标

2、法线矩阵通过将模型视图矩阵求逆并转置得到。这个矩阵被用来乘以法线向量以便用来计算光照

3、透视矩阵用来组织投影变换和透视除法,然后我们就得到了NDC设备归一化坐标。

最后我们使用gl.viewport来讲NDC坐标匹配到视口坐标。

gl.viewport(minX, minY, width, height);

视口坐标的原点在canvas的左上角。

JavaScript matrices

glmatrixwebgl本身没有提供方法来实现矩阵操作。webgl做的所有工作就是提供一种将矩阵传递给着色器的方式。所以我们需要使用一个JavaScript类库来让我们拥有操作矩阵的能力。这里使用这个类库。

Mapping JavaScript matrices to ESSL uniforms

由于模型视图矩阵和透视矩阵在一次渲染过程中不会改变,所以我们将他们做uniforms类型传递给着色程序。比如,如果我们要讲场景中的一个物体做平移操作,我们需要用平移操作后的新坐标来绘制所有物体。将所有的物体画在新位置是在一个渲染步骤中完成的。

然而,在渲染被(drawArrays或drawElements)调用之前,我们需要确保着色器拥有一个更新后的矩阵版本。具体步骤如下:

  1. 拿到一个指向该uniform的JavaScript引用

var reference= getUniformLocation(Object program, String uniformName)

  1. 利用该引用来将矩阵传递给着色器

gl.uniformMatrix4fv(WebGLUniformLocation reference, bool transpose,float[] matrix);其中matrix是一个JavaScript变量

uniformMatrix[2 3 4]fv(reference, transpose, matrix)能够通过reference来加载 2x2, 3x3, 4x4的浮点数据矩阵到uniform型着色器变量中。reference是WebGLUniformLocation型的变量。为了实际操作的简便,它通常是int数字。按照规范,transpose的值必须是false。matrix总是浮点类型。matrix通常是4,9,16个元素的按照列向量形式的数组。其中matrix参数也可以是Float32Array类型。这是一种JavaScript类型数组,使用二进制来存储数据,能够提升性能。

Working with matrices in ESSL

在如下着色器代码中:

gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

将一个计算值传递给了之前定义的gl_Position变量。gl_Position将包含着色器最近处理的顶点的裁剪坐标。这里应该记住着色过程是并行的,每一个顶点将会被一个顶点着色器的实例所处理。

为了获得给定顶点的裁切坐标,我们需要先乘以模型视图矩阵然后是投影矩阵。为了达到这个目的我们由左到右来计算(矩阵乘法不满足交换律)。

另外,注意我们需要包含齐次坐标来增强aVertexPosition属性。因为我们通常在欧几里得空间中来定义我们的图形。幸运的是ESSL允许我们通过增加确实部分来创建一个四维向量vec4。因为模型视图矩阵和透视矩阵都是在齐次坐标系中被描述的。

The Model-View matrix

模型视图矩阵允许在我们的场景中实现仿射变换。仿射是一个数学名词来描述一种对物体应用变换后不会改变物体结构的变换。

其中:

m13 m14 m15代表平移

m4 m8 m12总是0,m16总是1;

The Camera matrix

webgl中没有专门的相机对象,我们假想的相机对象由一个4x4矩阵所表达。

假定我们的相机坐落于世界的原点,朝向Z轴的负方向。关于相机的运动问题我们分成两个步骤来看: 相机的平移和相机的旋转

(本书中使用的是列向量来做讲解,而glmatrix中使用的是行向量)

The Camera matrix is the inverse of the Model-View matrix

相机矩阵跟模型视图矩阵为互逆关系。

根据程序想要达到的效果不同,相机类型也分为两种类型:轨道相机和跟踪相机。

轨道相机达到的目的是以一个世界中心店来做旋转、平移;比如当一个地球全部展现在你面前时,你对地球的旋转、拉近地球距离,这时候就是轨道相机。

跟踪相机的原点在相机位置;以相机为原点进行旋转前进后退等;cs游戏就是典型的跟踪相机模式。

这两种类型的相机在实现起来的差别就是对相机的模型矩阵的平移和旋转的先后顺序不同;轨道相机是先旋转相机的模型矩阵再平移;跟踪相机是先平移相机的模型矩阵,在对平移后的矩阵做旋转。

最终得到一个相机的模型矩阵,对这个相机的模型矩阵求逆就是以相机为原点的模型视图矩阵。

(跟踪相机和轨道相机是可以相互转化的,我们只需要把平移跟旋转的顺序变动一下即可)

Translating the camera in the line of sight

在视域中移动相机;对于轨道相机由于总是指向世界的中心点,所以我们通常使用世界坐标的z轴来改变物体与世界的远近。

而对于跟踪相机,由于他可能指向世界的任何地方,所以我们需要知道在世界坐标系中相机的朝向。

对于行向量来说先旋转后平移,数学上的矩阵操作顺序应该为:vRT;而对于列向量应该是TRv;这是由矩阵的运算法则所决定的。

而在实际编程语言中,通常使用一维数组来存储4x4矩阵的16个元素;所以有行存储和列存储的说法。所谓的行存储和列存储的区分就在于数组的前四个元素存储的是矩阵的第一列还是第一行;表示列的称为列存储,表示行的成为行存储。如下图数组的前四个元素对应矩阵中的第一列,所以是列存储。

当需要对物体或相机做一系列变换时,我们首先要确定矩阵的变换顺序,比如对webgl这种列主序的,先旋转后平移的矩阵操作是:TRv。 确定矩阵顺序后,接下来确定矩阵类库的api调用顺序 。向glmatrix这种类库提供的api,对先旋转后平移这种矩阵操作的实现方式是:

如果初学者要确定类库api的调用顺序,最好看一下类库矩阵运算部分的源码,否则容易绕晕。其实不管类库的api如何设计如果调用,只要保证计算的结果符合数学公式即可。

最后得到一个要传给着色器的最终矩阵,要保证这个一维数组是以列主序的方式存储。

综上来看,一点要保证得到的一维数组的元素在数学上是按照正确的矩阵运算顺序得到的结果。第二要保证传给着色器的一维数组是按照列主序的方式来存储的。

Camera model

相机矩阵中编码了关于相机轴的指向信息。如图所示,左上角的3x3矩阵代表了相机的三个轴:

  1. 第一列代表相机的x轴,我们称为Right 向量
  2. 第二列代表相机的y轴,我们称为up向量
  3. 第三列代表决定了相机可以在哪个矢量方向上来回前后移动。这是相机的z轴,我们称为相机轴。

由于实际中相机矩阵是模型视图矩阵的逆矩阵,包含在相机矩阵中左上角的3x3的旋转矩阵,能告诉我们在世界空间中相机的三个轴的方向。

The Perspective matrix

透视矩阵包含投影变换和透视除法。这两步合起来将一个3d场景转换成一个立方体(后面通过视口变换匹配到2d画布上)。

实际上,透视矩阵决定了相机捕获到的图像中几何物体。在真是世界中,相机镜头会决定最终的图像是如何变形扭曲的。在webgl世界中,我们使用透视矩阵来模拟这个过程。另外在webgl世界中我们可以有另一种非透视的表达:平行投影。

透视矩阵决定了相机的视野,也就是多少3d空间会被相机捕获。视野使用角度作为单位,这个术语通常与视角这个术语交叉使用。

Perspective or orthogonal projection

透视投影能够实现近大远小的效果,它更接近我们的眼睛观察到的真实世界,因为它给我们的大脑一些深度提示信息,所以我们能感到距离感。

相反,平行投影使用平行线,没有近大远小的感觉。所以在平行投影中深度信息被丢失了。

使用glMatrix,我们通过调用mat4.persective或者mat4.ortho来设置透视投影或平行投影矩阵。

Structure of the WebGL examples

webgl app的生命周期:

  1. Configure
  2. Load
  3. Draw

矩阵操作函数:

  1. initTransforms
  2. updateTransforms
  3. setMatrixUniforms
posted @ 2017-08-20 22:22  木的树  阅读(4031)  评论(0编辑  收藏  举报