【转】模型视图矩阵和投影矩阵(webgl笔记)

这段时间在做游戏3D场景生成和2D/3D切换的时候对模型视图矩阵和投影矩阵很头疼,直到看到这篇文章才豁然开朗,这篇文章讲的非常详细易懂,特地转来。

可以关注我的个人主页:http://alanzjl.sinaapp.com

最近在学习WebGL技术的过程中,我补充了一些原本了解甚少的计算机图形学知识。如果有同学和我一样,没有系统学过计算机图形学就接触了3D图形编程,而对不少略为艰深的概念有困惑,希望这些笔记能够帮助你。

模型矩阵

我们必须考虑,当空间中点的位置会发生变化的时候,其座标如何变化。考虑三种基本的变换:平移、旋转和缩放。

“变换”的含义就是,将点的初始位置的座标P映射到平移、旋转、缩放后的位置座标P’,即:

clip_image002

平移变换是最简单的变换:

clip_image004

旋转变换有一些复杂,先看在二维平面上的旋转变换:

clip_image006

很容易得到:

clip_image008

矩阵形式的表达更加简洁,后面大多使用这种形式:

clip_image010

推广到三维空间中:

点绕z轴旋转:

clip_image012

点绕x轴旋转:

clip_image014

点绕y轴旋转:

clip_image016

绕指定的任意轴旋转变换是由几个绕座标轴旋转变换和平移变换效果叠加而成的,后文会有详细叙述。

缩放变换也比较简单:

clip_image018

总结一下:平移变换,变换后点座标等于初始位置点座标加上一个平移向量;而旋转变换和缩放变换,变换后点座标等于初始位置点座标乘以一个变换矩阵。

clip_image020

clip_image022

clip_image024

齐次座标这天才的发明,允许平移变换也表示成初始位置点座标左乘一个变换矩阵的形式。齐次座标使用4个分量来表示三维空间中的点,前三个分量和普通座标一样,第四个分量为1。

clip_image026

平移变换巧妙地表示为:

clip_image028

旋转变换(以绕z轴旋转为例)和缩放变换相应为:

clip_image030

clip_image032

综上,在齐次座标下三种基本变换实现了形式上的统一,这种形式的统一意义重大。

clip_image034

clip_image022[1]

clip_image024[1]

矩阵有一个性质:

clip_image036

考虑一个点, 先进行了一次平移变换,又进行了一次旋转变换,结合上面矩阵的性质,可知变换后的点P’为:

clip_image038

旋转矩阵和平移矩阵的乘积R·T也是一个4×4的矩阵,这个矩阵代表了一次平移变换和一次旋转变换效果的叠加;如果这个点还要进行变换,只要将新的变换矩阵按照顺序左乘这个矩阵,得到的新矩阵能够表示之前所有变换效果的叠加,将最初的点座标左乘这个矩阵就能得到一系列变换后最终的点座标,这个矩阵称为“模型矩阵”。一个模型矩阵乘以另一个模型矩阵得到的还是一个模型矩阵,表示先进行右侧模型矩阵代表的变换,再进行左侧模型矩阵代表的变换这一过程的效果之和,因此模型矩阵的乘法又可以认为是闭合的。

模型矩阵之所以称之为“模型矩阵”,是因为该矩阵与点的位置没有关系,仅仅包含了一系列变换的信息。而在三维世界中,一个模型里所有的顶点往往共享同一个变换,对应同一个模型矩阵,比如抛在空中的一个木块,运转机器的一个齿轮。

之前说到,考虑一个物体绕指定轴旋转,如以下这个变换:绕着过顶点(x,y,z)方向为(a,b,c)的轴旋转角度θ,利用多个变换的叠加构建绕任意轴旋转的变换矩阵。

首先将顶点(x,y,z)平移到原点,绕x轴旋转角度p使指定的旋转轴在x-z平面上,绕y轴旋转角度q使指定的旋转轴与z轴重合,绕指定旋转轴(也就是z轴)旋转角度θ,绕y轴旋转角度-q,绕x轴旋转角度-p,将顶点平移到向量(x,y,z),p和q的值由方向(a,b,c)决定。综上,变换矩阵为:

clip_image040

因此在处理围绕非座标轴旋转的模型时,根据指定的旋转参数可以直接按照上述公式生成按照指定轴旋转的旋转矩阵,参加模型矩阵的构建。

齐次座标还有一个优点,能够区分点和向量:在普通座标里,点和向量都是由三个分量组成的,表示位置的点座标(2,3,4)和表示方向的向量(2,3,4)没有区别。而在齐次座标中,第四个分量可以区分它们,点座标的第四个分量为1,而向量座标第四个分量为0。比如,平移一个点是有意义的,能够得到平移后的点座标;而平移一个向量是没有意义的,方向不会因为平移而改变。

clip_image042

clip_image044

以上,我们已经了解到模型矩阵可以存储一个模型空间位置变化的信息,在生成三维动画每一帧的过程中,我们首先计算每个模型的模型矩阵,然后将最初的模型的每一顶点座标都左乘该模型矩阵,得到这一帧表示的时刻(模型已经经过多次变换)该模型每一顶点的座标。上面说的“帧”并不狭义地指屏幕的两次刷新时间的短暂间隔中屏幕上呈现的图像,而是指在这幅图像所描绘的整个三维空间的这个瞬间的所有顶点的位置。

来看个具体的例子:一个绕z轴匀速螺旋匀速上升的立方体,在某一帧中(即在这一帧对应的时刻t下),其向z轴正方向平移的长度和绕z轴旋转的角度分别为:

clip_image046

clip_image048

则模型矩阵(注意上文齐次座标下的基本变换矩阵)为:

clip_image050

产生这一帧时,只需要计算一次模型矩阵,再将立方体中8个顶点座标分别左乘该矩阵,就可以得到经过变换后8个顶点的座标。当一个模型顶点数量增加到上百甚至上千个,模型变换的步骤数也增加到几十步时,模型矩阵的作用就很明显了:如果没有齐次座标(也当然没有模型矩阵),对每个顶点都需要一步一步地变换:平移的时候加上一个向量,旋转的时候左乘一个矩阵,才能得到变换后的顶点座标;而模型变换只需要计算一次模型矩阵(当然也是一步一步的),然后每个顶点左乘模型矩阵就可以直接得到变换后的座标了。

视图矩阵

在模型矩阵中,我们关心的是空间中的点在经历变换后在世界座标系下的位置。事实上,我们更加关心空间中的点相对于观察者的位置。最简单的方案是将观察者置于原点处,面向z轴(或x轴、y轴)正半轴,那么空间中的点在世界座标系下的位置就是其相对于观察者的位置。

观察者的位置和方向会变化,看上去就好像整个世界的位置和方向发生变化了一样,所以解决的方案很简单,将世界里的所有模型看作一个大模型,在所有模型矩阵的左侧再乘以一个表示整个世界变换的模型矩阵,就可以了。这个表示整个世界变换的矩阵又称为“视图矩阵”,因为他们经常一起工作,所以将视图矩阵乘以模型矩阵得到的矩阵称为模型视图矩阵。模型视图矩阵的作用是:乘以一个点座标,获得一个新的点座标,获得的点座标表示点在世界里变换,观察者也变换后,点相对于观察者的位置。

视图矩阵同样也可以分为平移、旋转和缩放,视图矩阵是将观察者视为一个模型,获得的观察者在世界中变换的模型矩阵的逆矩阵。

观察者平移了(tx,ty,tz),视图矩阵如下,可以看出如果将视图矩阵看作整个世界的模型矩阵,相当于整个世界平移了(-tx,-ty,-tz)。

clip_image042[1]

观察者绕z轴旋转了角度θ,视图矩阵如下,相当于整个世界绕z轴旋转了-θ度。

clip_image044[1]

观察者在三个方向等比例缩小了s倍,视图矩阵如下,相当于整个世界放大了s倍。

clip_image052

观察者缩小的情形可能会引起困惑:如果人和猫咪的眼睛在同一个位置,人看到的世界和一只猫咪看到的世界应当是一样尺寸的,这和上述视图矩阵的情形矛盾;但是直觉告诉我,如果你喝了缩小药水,你应该会觉得整个世界在膨胀,就像视图矩阵所表现的那样。解答是这样:如果在计算机上模拟观察者喝了缩小药水的情形,在屏幕上看到整个世界是膨胀的,因为在那个虚拟的三维空间中,计算机屏幕这个“窗口”也随你(观察者)缩小。

视图矩阵实际上就是整个世界的模型矩阵,这给我一点启发:一个模型可能由多个较小的子模型组成,模型自身有其模型矩阵,而子模型也有自己的局部模型矩阵。考虑一辆行驶中的汽车的轮胎,其模型视图矩阵是局部模型矩阵(描述轮胎的旋转)左乘汽车的模型矩阵(描述汽车的行驶)再左乘视图矩阵得到的。

投影矩阵

模型视图矩阵的作用是确定某一帧中,空间里每个顶点的座标,而投影矩阵则将这些顶点座标映射到二维的屏幕上,即:

clip_image054

最主要的有两种投影方式,正射投影和透视投影。前者用于精确制图,如工业零件侧视图或建筑物顶视图,从屏幕上就可以量测平行于屏幕的线段长度;后者用于模拟视觉,远处的物体看上去较小。下图中,空间中的同一个矩形,正射投影后仍然是矩形,而透视投影后则变成了梯形。

正射投影(投影面和相机空间):

clip_image056

透视投影(投影面和相机空间):

clip_image058

三维世界的显示中,屏幕模拟了一个窗口,你透过这个窗口观察“外面”的世界。你的屏幕是有边缘的(除非你有一个球形的房间,内壁全是屏幕),因此你仅仅能观察到那个世界的一部分,即相机空间。相机空间的左、右、上、下边界是受限于屏幕的边缘,同时也设定前、后边界,因为你很难看清太近或太远的东西。在正射投影中,相机空间是一个规则的立方体,而在透视投影中则是一个方台体。

三维模型可能在不同的显示器上展现,因此投影的过程中不该将显示器参数加入进来,而是将空间中的点投影到一个规范的显示器中。另外,透视投影中的z值并不是毫无用处,它可以用来表示顶点的“深度”:如果三维空间中的两个不同顶点投影到平面上时重合了,那么将显示深度较浅的顶点。

定义一个规范的视窗区域(CCV),为xyz都处在区间[-1,1]之间的边长为2的立方体。x和y座标值用来线形拉伸到到实际屏幕上,而z值存储了“深度”。而投影的过程就是将三维空间中的点从相机空间映射到CCV中。

正射投影非常简单,直接将矩形的相机空间线形压缩到CCV中即可。采取顶视图,相机空间的左右边界为:

clip_image060

clip_image062

简单的线形成比例关系:

clip_image064

clip_image066

推广到y轴和z轴:

clip_image068

相机空间中的点经过正射投影矩阵左乘后得到的点都在CCV之中:

clip_image070

透视投影相对较为复杂,同样用顶视图考虑x座标的情况:

clip_image072

clip_image074

clip_image076

clip_image078

转化为齐次的方式:

clip_image080

推广到y轴:

clip_image082

透视投影矩阵的第三行不是我们关心的内容,只要保证不同顶点投影前后的点座标的第三个分量z和z’的大小关系不变就可以。

透视投影矩阵尾行是(0,0,1,0),这样就将计算得到的座标的第四个分量赋值为z而不是1。将相机空间左乘投影矩阵后的结果不是一个CCV空间,如果你将这个空间画出来,会发现其仍然是一个方台形。这时进行“透视除法”,将上一步得到的点座标化为第四个分量为1的标准齐次座标:

clip_image084

然后我们直接取齐次座标中的x’和y’值,并将其线形映射到屏幕上,比如点(0,0)出现在屏幕中央,点(-1,1)出现在屏幕左上角。

WebGL

WebGL中对于模型视图矩阵和投影矩阵的操作依赖于第三方库,比如Oak3D或glMatrix,WebGL本身不支持(或者说不限制)任何对模型视图矩阵和投影矩阵的操作。

WebGL是在浏览器端运行的,所以使用JavaScript编程。下面的代码来自www.hiwebgl.com翻译的LearningWebGL.com的WebGL教程。

以glMatrix库为例:

// 新建空模型视图矩阵

var mvMatrix = mat4.create();

// 将矩阵设置为单位阵

mat4.identity(mvMatrix);

// 平移和旋转

mat4.translate(mvMatrix, [-1.5, 0.0, -8.0]);

mat4.rotate(mvMatrix, degToRad(45), [0, 1, 0]);

将矩阵设置为单位阵相当于说:“这个矩阵表示什么都还没做(平移、旋转、缩放)呢”,事实上,任意点座标乘以单位矩阵都只能得到自己,正说明“什么都没做”。

平移矩阵的函数mat4.translate()做的仅仅是将mvMatrix左乘一个平移矩阵而已。

旋转矩阵的函数mat4.rotate()也许比较复杂,它做的是上面我们讨论过的“围绕任意轴旋转”的问题,这个函数默认使用“本地轴”,即过所有平移效果累加后的那一点的轴,参数向量[0,1,0]是轴的指向,因此上面的函数调用处理了一个围绕本地y轴的旋转。

// 新建空投影矩阵

var pMatrix = mat4.create();

// 初始化投影矩阵

mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

投影矩阵不会因为场景里模型的位置变化或观察者的移动而变化(当然如果你想模拟观察者戴眼镜的过程你可能要考虑),故而投影矩阵只需要一次初始化就够了。初始化需要给出相机空间的前、后、左、右、上、下边界,很容易从函数调用里传入的参数推知:包括前、后边界,相机空间的宽高比和水平视场角。

如果你使用脚本调试工具监测矩阵对象mvMatrix和pMatrix,就会发现他们仅仅是有16个元素的Float32Array对象而已,你完全可以亲自处理它。

值得一提的是glMatrix库的函数大多不返回处理后的矩阵,在将矩阵作为参数传入时已经给了函数修改矩阵的权利,很少的情况下需要会写这样的代码(但其他的库不一定这样):

xMatrix = matX.operate();

使用库函数或自力更生处理完矩阵后,通过着色器程序传递到着色器中(着色器程序是JavaScript脚本里的概念,而着色器是用其他脚本语言编写的在显卡中运行的逻辑,这些不在本文的讨论范围内):

// 设置着色器程序

……

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");

……

// 将模型视图矩阵和投影矩阵传入着色器

gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);

然后看看着色器里的代码,这是用x-shader类型的脚本语言写的:

void main(void){

……

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

……

}

可以看到屏幕上点座标由初始点座标左乘模型视图矩阵,再左乘投影矩阵得到的。对于较复杂的场景,我猜测可能需要重新编写着色器,将模型矩阵和视图矩阵拆开处理。

综上所述,模型视图矩阵和投影矩阵是三维计算机图形学的基石。关于这两个矩阵的知识虽然不是进行3D图形编程的必须,但是至少能够帮助我们更好地了解那些库函数在做些什么,或者自己直接操作矩阵对象。

OpenGL矩阵推导——模型视图变化

在三维编程中,模型视图变换是从三维世界到二维屏幕中一个很重要的变换,但是这个变换往往很多人都不太理解,要么是事而非。而这方面的文章不是太少就是讲的太浅没有真正的理解模型视图变换,本人在这个过程中曾经走过很多歪路,不过好在最终在自己的不懈努力下终于降伏了这只猛虎。本人就以自己的理解,通过矩阵推导过程一步一步来了解模型视图变化,最后通过两个OpenGl的程序来进一步理解模型视图矩阵。先从一个基本的模型视图—透视投影变换讲起。

透射投影是将相机空间中的点从视锥体(frustum)变换到规则观察体(Canonical View Volume 以下简称CVV)中,待裁剪完毕后进行透视除法的行为。

透视投影变换是令很多刚刚进入3D图形领域的开发人员感到迷惑乃至神秘的一个图形技术。其中的理解困难在于步骤繁琐,对一些基础知识过分依赖,一旦对它们中的任何地方感到陌生,立刻导致理解停止不前。

主流的3D APIs 都把透射投影的具体细节进行了封装,从而只需一个函数便可生成一个透射投影矩阵比如gluPerspective(),使得我们不需要了解其算法便可实现三维到二维的转化,然而实事是,一些三维图形或游戏开发人员遇到一些视图矩阵的问题往往会不知所措,比如视景体裁剪。

以下部分内容是从别处那转过来的,主要感谢Twinsen和一个叫丁欧南的高中生。

透视投影变换是在齐次座标下进行的,而齐次座标本身就是一个令人迷惑的概念,这里我们先把它理解清楚。齐次座标

对于一个向量v以及基oabc clip_image086

可以找到一组座标(v1,v2,v3),使得

v = v1 a + v2 b + v3 c1

而对于一个点p,则可以找到一组座标(p1,p2,p3),使得

po = p1 a + p2 b + p3c 2

从上面对向量和点的表达,我们可以看出为了在座标系中表示一个点(如p),我们把点的位置看作是对这个基的原点o所进行的一个位移,即一个向量——p– o(有的书中把这样的向量叫做位置向量——起始于座标原点的特殊向量),我们在表达这个向量的同时用等价的方式表达出了点p:

p = o + p1 a + p2 b + p3 c (3)

(1)(3)是座标系下表达一个向量和点的不同表达方式。这里可以看出,虽然都是用代数分量的形式表达向量和点,但表达一个点比一个向量需要额外的信息。如果我写出一个代数分量表达(1, 4, 7),谁知道它是个向量还是个点!

我们现在把(1)(3)写成矩阵的形式:

clip_image088

这里(a,b,c,o)是座标基矩阵,右边的列向量分别是向量v和点p在基下的座标。这样,向量和点在同一个基下就有了不同的表达:3D向量的第4个代数分量是0,而3D点的第4个代数分量是1。像这种这种用4个代数分量表示3D几何概念的方式是一种齐次座标表示。

齐次座标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射(线性)几何变换。——F.S. Hill, JR

这样,上面的(1, 4, 7)如果写成(1,4,7,0),它就是个向量;如果是(1,4,7,1),它就是个点。

下面是如何在普通座标(OrdinaryCoordinate)和齐次座标(Homogeneous Coordinate)之间进行转换:

从普通座标转换成齐次座标时,

如果(x,y,z)是个点,则变为(x,y,z,1);

如果(x,y,z)是个向量,则变为(x,y,z,0)

从齐次座标转换成普通座标时,

如果是(x,y,z,1),则知道它是个点,变成(x,y,z);

如果是(x,y,z,0),则知道它是个向量,仍然变成(x,y,z)

以上是通过齐次座标来区分向量和点的方式。从中可以思考得知,对于平移T、旋转R、缩放S这3个最常见的仿射变换,平移变换只对于点才有意义,因为普通向量没有位置概念,只有大小和方向,这可以通过下面的式子清楚地看出:

clip_image090

而旋转和缩放对于向量和点都有意义,你可以用类似上面齐次表示来检测。从中可以看出,齐次座标用于仿射变换非常方便。

此外,对于一个普通座标的点P=(Px, Py, Pz),有对应的一族齐次座标(wPx, wPy,wPz, w),其中w不等于零。比如,P(1, 4, 7)的齐次座标有(1, 4, 7, 1)、(2, 8, 14, 2)、(-0.1, -0.4, -0.7, -0.1)等等。因此,如果把一个点从普通座标变成齐次座标,给x,y,z乘上同一个非零数w,然后增加第4个分量w;如果把一个齐次座标转换成普通座标,把前三个座标同时除以第4个座标,然后去掉第4个分量。

由于齐次座标使用了4个分量来表达3D概念,使得平移变换可以使用矩阵进行,从而如F.S. Hill, JR所说,仿射(线性)变换的进行更加方便。由于图形硬件已经普遍地支持齐次座标与矩阵乘法,因此更加促进了齐次座标使用,使得它似乎成为图形学中的一个标准。

简单的线性插值

线性插值我举的是丁欧南的温度计的例子:

已知有一破温度计(何以谓破?刻度之间间距虽平均,但间距或大于或小于标准值,谓之破),当其插入0 0C水里时显示为50C,当其插入1000C的沸水中时显示为900C,问:当实际水温为500C时此破温度计显示的值是多少?

解:因刻度均匀,所以刻度之间的比例与好温度计相同,由此:设显示的数为T,

(90-T)/(T-5)=(100-50)/(50-0) 解出T=47.5 0C.

结论:由一个数域(如题目中的好温度计两个端点[0,100])映射到另一个数域(如题目中的破温度计的两个端点[5,90])时,如果两个数域都是线性(就如题目中暗示的刻度平均),那么它们对应点成比例(比如50和47.5这一对端点).

这道题的应用是把一组座标映射到另一个范围,这将在介绍NDC(Normalized Device Coordinate,归一化的设备座标)时用到.

透视投影变换

好,有了上面两个理论知识,我们开始分析这次的主角——透视投影变换。这里我们选择OpenGL的透视投影变换进行分析,其他的APIs会存在一些差异,但主体思想是相似的,可以类似地推导。经过相机矩阵的变换,顶点被变换到了相机空间。这个时候的多边形也许会被视锥体裁剪,但在这个不规则的体中进行裁剪并非那么容易的事情,所以经过图形学前辈们的精心分析,裁剪被安排到规则观察体(CanonicalView Volume, CVV)中进行,CVV是一个正方体,x, y, z的范围都是[-1,1],多边形裁剪就是用这个规则体完成的。所以,事实上是透视投影变换由两步组成:

1) 用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中。

2) CVV裁剪完成后进行透视除法(一会进行解释)。

clip_image092

我们一步一步来,我们先从一个方向考察投影关系。

clip_image094

上图是右手座标系中顶点在相机空间中的情形。设P(x,z)是经过相机变换之后的点,视锥体由eye——眼睛位置,np——近裁剪平面,fp——远裁剪平面组成。N是眼睛到近裁剪平面的距离,F是眼睛到远裁剪平面的距离。投影面可以选择任何平行于近裁剪平面的平面,这里我们选择近裁剪平面作为投影平面。设P’(x’,z’)是投影之后的点,则有z’ = -N。通过相似三角形性质,我们有关系:

clip_image096

同理,有

clip_image098

这样,我们便得到了P投影后的点P’

clip_image100

从上面可以看出,投影的结果z’始终等于-N,在投影面上。实际上,z’对于投影后的P’已经没有意义了,这个信息点已经没用了。但对于3D图形管线来说,为了便于进行后面的片元操作,例如z缓冲消隐算法,有必要把投影之前的z保存下来,方便后面使用。因此,我们利用这个没用的信息点存储z,处理成:

clip_image102

这个形式最大化地使用了3个信息点,达到了最原始的投影变换的目的,但是它太直白了,有一点蛮干的意味,我感觉我们最终的结果不应该是它,你说呢?我们开始结合CVV进行思考,把它写得在数学上更优雅一致,更易于程序处理。假入能够把上面写成这个形式:

clip_image104

那么我们就可以非常方便的用矩阵以及齐次座标理论来表达投影变换:

clip_image106

其中clip_image108

哈,看到了齐次座标的使用,这对于你来说已经不陌生了吧?这个新的形式不仅达到了上面原始投影变换的目的,而且使用了齐次座标理论,使得处理更加规范化。注意在把clip_image110 变成clip_image112 的一步我们是使用齐次座标变普通座标的规则完成的。这一步在透视投影过程中称为透视除法(Perspective Division),这是透视投影变换的第2步,经过这一步,就丢弃了原始的z值(得到了CVV中对应的z值,后面解释),顶点才算完成了投影。而在这两步之间的就是CVV裁剪过程,所以裁剪空间使用的是齐次座标clip_image110[1] ,主要原因在于透视除法会损失一些必要的信息(如原始z,第4个-z保留的)从而使裁剪变得更加难以处理,这里我们不讨论CVV裁剪的细节,只关注透视投影变换的两步。

矩阵clip_image114

就是我们投影矩阵的第一个版本。你一定会问为什么要把z写成

clip_image116

有两个原因:

1) P’的3个代数分量统一地除以分母-z,易于使用齐次座标变为普通座标来完成,使得处理更加一致、高效。

2) 后面的CVV是一个x,y,z的范围都为[-1,1]的规则体,便于进行多边形裁剪。而我们可以适当的选择系数a和b,使得clip_image116[1] 这个式子在z = -N的时候值为-1,而在z = -F的时候值为1,从而在z方向上构建CVV。

接下来我们就求出a和b:clip_image118

这样我们就得到了透视投影矩阵的第一个版本:

clip_image120

使用这个版本的透视投影矩阵可以从z方向上构建CVV,但是x和y方向仍然没有限制在[-1,1]中,我们的透视投影矩阵的下一个版本就要解决这个问题。

为了能在x和y方向把顶点从Frustum情形变成CVV情形,我们开始对x和y进行处理。先来观察我们目前得到的最终变换结果:

clip_image121

我们知道-Nx / z的有效范围是投影平面的左边界值(记为left)和右边界值(记为right),即[left, right],-Ny / z则为[bottom, top]。而现在我们想把-Nx / z属于[left, right]映射到x属于[-1, 1]中,-Ny / z属于[bottom, top]映射到y属于[-1, 1]中。你想到了什么?哈,就是我们简单的线性插值,你都已经掌握了!我们解决掉它:

clip_image123

则我们得到了最终的投影点:

clip_image125

下面要做的就是从这个新形式出发反推出下一个版本的透视投影矩阵。注意到clip_image121[1]clip_image110[2] 经过透视除法的形式,而P’只变化了x和y分量的形式,az+b和-z是不变的,则我们做透视除法的逆处理——给P’每个分量乘上-z,得到

clip_image127

而这个结果又是这么来的:

clip_image129

则我们最终得到:

clip_image131

上面是一般情况,我们要把它变成特殊性版本,即gluPerspective,它是一种左右对称的投影形式,因此我们从对x和y进行插值的那一步来看:

那一步来看:

clip_image133

销掉两边的1/2,得到:

clip_image135

则我们反推出透视投影矩阵:

clip_image137

这就是gluPerspective的投影矩阵了。

结论和用法:相机空间中的顶点,如果在视锥体中,则变换后就在CVV中。如果在视锥体外,变换后就在CVV外。而CVV本身的规则性对于多边形的裁剪很有利。OpenGL在构建透视投影矩阵的时候就使用了M的形式。注意到M的最后一行不是(0 0 0 1)而是(0 0 -1 0),因此可以看出透视变换不是一种仿射变换,它是非线性的。另外一点你可能已经想到,对于投影面来说,它的宽和高大多数情况下不同,即宽高比不为1,比如640/480。而CVV的宽高是相同的,即宽高比永远是1。这就造成了多边形的失真现象,比如一个投影面上的正方形在CVV的面上可能变成了一个长方形。解决这个问题的方法就是在对多变形进行透视变换、裁剪、透视除法之后,在归一化的设备座标(NormalizedDevice Coordinates)上进行的视口(viewport)变换中进行校正,它会把归一化的顶点之间按照和投影面上相同的比例变换到视口中,从而解除透视投影变换带来的失真现象。进行校正前提就是要使投影平面的宽高比和视口的宽高比相同。

而r-l和t-b可以分别看作是投影平面的宽w和高h。如果我们不知道right、left、top以及bottom这几个参量,也可以根据视野(FOV – Field Of View)参量来求得。下面是两个平面的视野关系图:

clip_image139

clip_image141

其中,两个fov分别是在x-z以及y-z平面上的视野。如果只给了一个视野,也可以通过投影平面的宽高比计算出来:

clip_image143

用一个视野算出w或者h,然后用宽高比算出h或者w。

我们可以通过一个例子来证明我们矩阵的正确性:

void OpenGlCom::ReSize()

{

RECTrct ;

GetClientRect(m_Hwnd,&rct);

intm_view_width = rct.right- rct.left;

intm_view_height = rct.bottom - rct.top;

glViewport(0,0,m_view_width,m_view_height);

float nearz =5.0;

float farz =80000;

float AspectRatio =float(m_view_height)/ float(m_view_width);

float ViewAngleH =90 * (PI / 180);//90度

float ViewAngleV = atan(tan(ViewAngleH/2) * AspectRatio)* 2;

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

GLfloatm[16];

::ZeroMemory(m,16*sizeof(float));

//////m[8],m[9]=0表示对称视椎体即gluPerspective///////////

m[0] = 1.0 /tan(ViewAngleV / 2);

m[5] = m[0]*AspectRatio;

m[10] = -(farz +nearz) / (farz - nearz);

m[11] = -1;

m[14] = - 2 * farz *nearz / (farz - nearz);

glMultMatrixf(m);

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

}

void OpenGlCom::draw()

{

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

glPushMatrix();

glDepthFunc(GL_LEQUAL);

glEnable(GL_DEPTH_TEST);

glBegin(GL_TRIANGLE_FAN);

glTranslatef(0, 0,10.0f);//往视点方向平移10单位显示更大

glColor3f(0.8f, 0.8f, 0.6f);

glVertex3f(0,0,-20);

glVertex3f(30,20,-40);

glVertex3f(40,10,-30);

glPopMatrix();

}

clip_image145

我们看看在gluPerspective图形有什么变化呢

将ReSize()改成:

{

RECTrct ;

GetClientRect(m_Hwnd,&rct);

intm_view_width = rct.right- rct.left;

intm_view_height = rct.bottom - rct.top;

glViewport(0,0,m_view_width,m_view_height);

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

floatfAspect = float(m_view_width) / float(m_view_height);

gluPerspective(90.0,fAspect, 5.0, 80000.0);

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

}

clip_image147

接下来,再试试看glFrustum()实现的效果:

将ReSize()改成:

{

RECTrct ;

GetClientRect(m_Hwnd,&rct);

intm_view_width = rct.right- rct.left;

intm_view_height = rct.bottom - rct.top;

glViewport(0,0,m_view_width,m_view_height);

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

GLdouble left =-nearz*tan(ViewAngleV/2);

GLdouble right =nearz*tan(ViewAngleV/2);

GLdouble bottom =-nearz*tan(ViewAngleH/2);

GLdouble top =nearz*tan(ViewAngleH/2);

glFrustum(left,right,bottom,top,nearz,farz); glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

}

clip_image149

从三幅图中可以看出图形位置是一样的,这就验证了我们的矩阵是正确的!

发布了31 篇原创文章 · 获赞 14 · 访问量 18万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章