3D坐标系、矩阵变换、视景体与裁剪

背景
当前3D图形界主要有两个:微软的Direct 3D以及某组织的OpenGL。曾经一度OpenGL几乎占据全部3D图形领域,这在巨人微软面前简直就是屌丝逆袭。曾几什么时候微软搞IDE borland公式倒闭了,后来微软搞浏览器,网景公司解散,员工卷铺盖走人了,也就是说微软搞谁,谁倒霉。直到OpenGL的出现,打破了这一魔咒,在与微软竞争的前期,OpenGL几乎甩了微软几条街,并成为事实上的工业标准。后来在微软的大力绞杀下,OpenGL几乎被彻底赶出了游戏领域,退居高端图形领域。基本上如今是微软的Direct 3D统治游戏领域,而OpenGL则在高端专业图形领域占绝对统治地位。微软仍是微软,OpenGL已经不是之前的OpenGL了,等会。。等会。。这句话咋这么熟悉?想起来了赵本山的小品里说过:你大爷仍是你大爷,你大妈已经不是你5年前的大妈了,为何这么说呢?话说搞OpenGL的那家公司被微软逼疯了,没错。。解散了。。可是OpenGL并无消失,而是交给某开源组织托管、开发与维护了。哎。。。不说了,都是泪啊。。凡是牵扯到微软,那就是一部血泪史啊。。逼疯了无数企业。。可是话有说回来,商场上比尔盖茨是个侩子手而慈善上这家伙也不小气。。大把大把的捐钱。。 程序员

坐标系空间 算法

在OpenGL里面,3D坐标系的X轴自左向右增大,y轴自下向上增大,z轴正方向从屏幕中心指向观察者。
坐标系有如下几种:局部(模型)坐标系、世界坐标系、相机坐标系、屏幕坐标系;对应的矩阵变换则有模型变换、视图(相机)变换、投影变换,其中投影变换分为正视投影、透视投影。而坐标系之间的转换要用到矩阵。世界坐标系至关因而虚拟宇宙,位置固定不变,而局部(模型)坐标系是绘图的一个局部空间,是相对的,相机坐标系是以相机的镜头(或者人的眼睛)来观察物体的视觉空间。在3D中画图是如今局部坐标空间绘制,而后经过矩阵变换转移到世界坐标空间,接着转换到相机空间,而后在投影,最终会在光栅化的二维屏幕上渲染图形。
编程

局部坐标空间又叫模型空间,绘制图形是在模型空间绘制,绘制完成后通过模型变换转换到世界坐标系空间。在OpenGL中渲染三维模型是以图元为最小单位进行渲染的,图元有三角形,四边形等,绝大多数状况下都是以三角形图元渲染。图元如三角形是有3个顶点组成的,那么为何最小图元不是顶点而是诸如三角形呢?这就比如提到一个化学物质人们会说这个物质是由不少原子组成的,而不会说是由电子、中子、原子核组成,由于电子、中子及原子核是一个有机总体;一样三角形图形的三个顶点也一个有机总体。 api

图一 数组

图二 浏览器

上面图片一是层次细节LOD地形网格,513像素X513像素,不过并无达到彻底的分辨率,并且不一样的地方分辨率不一样,此是后话。图片二是用OpenGL加载的一个ms3d格式的三维小汽车,这个小汽车是用3d max制做的3ds文件通过MilkShape 3D转换后获得的ms3d文件。这两个三维模型都是以三角形网格渲染而成的,不一样的是图一没有进行文理贴图,而是以线框的模式渲染的,这样作是为了更好的看到3D渲染的细节;对应的图二则是以平滑模式渲染而且贴有文理。从本质上来讲三维图像的最小单位是顶点,这些顶点以特定的方式送往3D API(典型的如OpenGL 3d api或者D3D api)并以三角形网格的形式进行渲染。固然也能够选择其余多边形如四边形进行渲染,可是三角形渲染最为方便,几乎全部的3D图形都是以三角形为图元进行渲染。从图一能够看出这个地形是有不少小的三角形网格组成的,事实上这个地形网格是以三角形扇的方式组织渲染的。如今咱们看到的这两个图形是在屏幕坐标空间观察的,那么他们的第一站其实就是局部(模型)坐标空间,通过一系列3D流水线最终送往二维屏幕进行光栅化处理和渲染。在局部坐标系的物体有N个顶点组成,若是变换到世界坐标系的话须要对全部顶点作变换,共计N次变换。设物体的任一顶点为(loc_x ,loc_y , loc_z)。这个坐标是在局部坐标系下,要想变换到世界坐标空间须要将局部坐标系的原点移动到对应的位置(world_x,world_y,world_z)处,而且同时移动三维模型的全部顶点坐标。很容易获得最终的坐标:(loc_x+world_x,loc_y+world_y,loc_z+world_z)。如今咱们先来看一幅图片:

 

咱们惊奇的发现这个计算结果正是咱们想要的,没错,你猜对了,在3D图形里面顶点的变换都是经过矩阵完成的,而这个平移变换是最简单的一种矩阵变换,其余的还有旋转、缩放等等。上面的图1、二就是在3D流水线里通过一些列像上述那样的矩阵变换最终才从幕后走向台前展示在你们眼前。由上能够看出每个顶点经历了16次乘法运算、12次加法运算,共计28N次计算,固然在矩阵变换之间可能还有进行了诸如光照、纹理等操做。假如一个三维场景共有100万个顶点,那么就要作2800万次计算,这尚未加上后面的相机变换、投影变换所作的矩阵运算以及光栅化、渲染等操做。由此能够看出来运算量是很是的大。那么显卡能承受如此巨大的运算量吗?后面会提到,3D 图形库(如OpenGL)会在图形进行渲染前将没必要要的顶点裁剪掉,这样就不用渲染他们了,从而节省了GPU的运算量。可是光是依靠3D 图形库的裁剪功能仍是不够,虽然裁剪掉了不需进行渲染的顶点,然而这些顶点已经消耗掉了大量的矩阵运算,尤为是当场景很是细腻的时候也就意味着顶点数目很是巨大。那么有没有方法能够在坐标系统进行矩阵变换前就被提早裁剪掉呢?答案是确定的。举个例子,对于图一中的地形网格来讲,他被渲染到屏幕是有条件的,其一:不在照相机视景体空间内的物体将被忽略不予处理;其二:对于远处的地面以低分辨率渲染,近处则以高分辨率渲染;其三:粗糙的部分以高分辨率渲染,平坦的部分以低分辨率渲染。假若有一个600X600的网格通过测试不在照相机的视景体内,那么这36万个顶点就不用进行后续的大量矩阵运算,而测试所消耗是由8个顶点组成的六面体,这个运算量的消耗无疑是值得的。至于如何进行相机裁剪,在层次细节算法里面将会详细说明。在OpenGL以及D3D里面用户通常不会直接操做矩阵,OpenGL会将用户的函数调用解释为矩阵,好比用户调用gluPerspective(参数。。)时,OpenGL会根据函数的参数设置视图矩阵并与当前的视图矩阵相乘。 函数

世界坐标系空间通过上面的平移操做物体就被移动到世界坐标系,这个坐标系是固定不变的,至关于虚拟宇宙中心。此时物体还不能呈现于屏幕,还要经历九九八十一难才能与观众见面。进入世界坐标系之后,照相机的视点可能不在原点,而且视点可能还不是朝向z轴负方向。此时就要将照相机平移到世界坐标系原点,而且调整方向使相机视点朝向z轴负方向。之因此要这样调整是由于若是相机位于原点并朝向z轴负方向的话会给处理带来极大的方便,至因而什么方便呢,我也不知道,反正是专家说的,至于你信不信,反正我是信了。在说明如何变换到相机空间咱们先来看一下几个矩阵操做。设有世界坐标系空间的某一个点A(world_x,world_y,world_z)分别绕x轴 y轴 z轴旋转angle_x、angle_y、angle_z度到达B点。那么求其旋转后的坐标。 测试

图三 spa

这里为了更好推导将坐标系进行了旋转。 .net

如图三A点绕x轴 旋转angle_x度求其旋转后的坐标。A点绕X轴旋转所造成的平面一定与y轴 z轴所构成的平面平行,所以将A点 B点投射到y_z平面上获得A撇点 B撇点,A撇  B撇点在z y平面上的坐标显而易见就是A点 B点对应的 y z坐标。如图可知C角的大小就是angle_x 即C=angle_x;D点对应的那个角是OA与y轴的夹角。

对于B撇点:

对于A撇点:

将第一个方程式展开获得:

将第二个方程代入第三个方程式获得:

显而易见绕x轴旋转x坐标值天然是不变的,也就是说旋转后顶点坐标为:

(world_x, world_y*cos(angle_x)+world_z*sin(angle_x),-world_y*sin(angle_x)+world_z*cos(angle_x));
下面咱们再看一个矩阵运算:

矩阵一

咱们再一次吃惊的发现这个正是咱们所想要的结果,难道冥冥之中矩阵与3D图形变换有着不解之缘?OpenGL里面矩阵是按照列优先的原则存储于一个一维数组里面,三维顶点不是以三维向量二是以四维向量来表示好比(x,y,z,w)来表示的,w初始默认状况下为1,在变换过程当中w的值会跟着发生变化,而且w也有它的用处,此是后话。咱们来看一个更加通常的矩阵:

在用有特殊含义的字符串来填充相应位置后,咱们会发现。。。没错。。前三行三列刚好表示x y z坐标轴的方向向量,每一列(除了第四列) 的最后一个值默认是0,第四列最后一个值是1,一样在变换过程当中它可能发生变化,它的一个用处是齐次化坐标。那么一开始的矩阵前三列分别是(1,0,0,0) (0,1,0,0) (0,0,1,0)很显然这三个坐标分别表示x y z轴的方向向量(此时局部坐标系的原点与世界坐标系原点重合为(0,0,0)),那么旋转后这三列还能表示三个坐标轴方向向量吗?这个矩阵怎么解释呢?咱们再来看一个图:

 

在上述的矩阵变换中咱们曾经说过绕X轴旋转物体angle_x度,这至关于物体不动把坐标轴绕X轴向相反的方向旋转angle_x度。如图A表示绕X轴旋转的角度,若是以上述变换为例,那么A=angle_x。显然在新的局部坐标系下对应的B C点在原来的坐标系中的坐标分别是
(0,-1*sin(angle_x),1*cos(angle_x))           (0,1*cos(angle_x),1*sin(angle_x)) 再看矩阵一,发现这个正是新的坐标系的三个方向向量,这个方向向量是以原坐标系为参考系得来的。在3D中咱们旋转物体与物体不动旋转坐标系的效果是同样的,只是四维方式的问题而已。在回过头来看矩阵二,咱们发现(translate_x ,translate_y,translate_z)是局部矩阵的原点在原坐标系下的坐标,而(x1,y1,z1)是新的局部坐标系的x轴上的一个点在原坐标系下的坐标,其余的以此类推,局部坐标系的原点定了,三个坐标轴上的点也定了,咱们吃惊的发现局部坐标系在原坐标系为参考的状况下已经描绘出来了。若是。。若是原坐标系是世界坐标系,没错,若是成真的话咱们就通过一系列矩阵变换获得了局部坐标系在世界坐标系中的位置,因而就刻画出来了三维物体的顶点坐标在世界坐标系下的坐标值。
如今还记得咱们一开始的问题吗?你可能已经不记得了,问题是如何将世界坐标系变换到相机坐标系。

图片来自3D游戏编程大师。

如上图相机位置(cam_x,cam_y,cam_z)与y轴夹角为angle_y。若是要变换到相机空间的话首先要将相机平移到原点,结合前面所说的也就是设置矩阵的最后一列translate_x=-cam_x, translate_y=-cam_y, translate_z=-cam_z,而后使相机的镜头绕y轴旋转-angle_y度。前面已经讲过绕X轴旋转angle_x度的矩阵方程,那么绕x轴旋转-angle_x度的方法就是将前述矩阵的角度设置为-angle_x就好了,y轴旋转的也能够以此类推。

相机坐标空间通过上述的矩阵变换已经到达了相机坐标空间,如今到了投影的时刻了,前面定义了相机的位置和方向,可是相机的视野不是无限远的,必须为它制定一个视景体,在视景体内的物体将被投影到视平面,不在视景体内的物体将被丢弃不处理

 

如图即是一个视景体,这个投影是透视投影,所谓透视投影就是给人一中置身于实际场景中的感受,远处的物体显得小,近处的物体显得大。还有一中投影叫正视投影,这个主要用于CAD程序中。三维图形主要使用透视投影。在OpenGL这个视景体能够用api函数gluPerspective(angle,fov,w_div_h,near,far)来定义。这个函数将产生一个透视投影矩阵并与当前的投影矩阵相乘。

投影空间通过上述矩阵变换就到了投影空间了,接着就是后续的视口变换,渲染等工做了。

裁剪如今咱们再来看一个地形网格

图片三

这个地形网格和图片一是一个程序生成的,不一样的是前者调节系数是8,后者调节系数是25结果致使了后者的分辨率明显大于前者。通过前面坐标系空间变换的介绍咱们知道这幅地形网格呈如今咱们眼前以前经历了模型变换,视图变换,投影变换等。这个地形网格是513*513尺寸,并且没有达到彻底分辨率,那么若是达到了彻底分辨率,势必顶点数目大幅增长,再如 若是地形尺寸是10000*10000呢,顶点数将增长400倍,再如一个实际的场景可能还有大量的树木,房屋,动物,人车等。除了矩阵运算还有纹理贴图,光照,雾化等,处理起来至关的消耗GPU和CPU。若是不控制好的话,系统渲染后运行很是卡。其中有一个能够改善的方法是LOD算法即层次细节算法,这个算法经常使用来绘制大规模实时地形。这个算法其中须要用到一个叫相机裁剪的算法。接下来就说一下相机裁剪。所谓相机裁剪就是在上图视景体中的物体进行处理,不在里面的物体被丢弃。前面已经说过渲染的时候OpenGL会自动丢弃不在视景体内的物体以免渲染,而此处的裁剪进一步减小了矩阵运算的次数,也就是说若是物体须要裁剪的话,那么在模型空间就被裁剪了,而没有通过后续的各类矩阵变换。那么必须设计一个算法来检测某个物体是否被相机裁剪。方法之一是将待处理的物体构造一个AABB包围盒,而后用包围盒的顶点与物体顶点所在的平面作相交测试,不想交就被裁剪掉,不然保留。

typedef struc aabb
{    
      someType  min[3];
      someType  max[3]; 
} AABB;

这个结构体里面min[3]里面保存的是(min_x,min_y,min_z)对应max[3]里面保存(max_x,max_y,max_z) 这些坐标是采用某种方法找到的物体的最小坐标或最大坐标值,最笨最耗时的办法就是遍历物体的每一个顶点找到对应的最小最大坐标值

那么怎么求视景体的六个面的方程呢?在OpenGL中通过透视投影后前文的视景体变为了规则的长方体,这个投影空间中顶点的坐标形式为(pro_x,pro_y,pro_z,w),如今到了w值大显神通的时候了,若是程序员没有在程序中故意操做矩阵的值,那么如今这个w就是每一个坐标值得各个份量的绝对值的最大范围,显而易见若是物体的坐标知足-w<x<w;-w<y<w;-w<z<w的话那么物体在这个投影空间内不然被裁减掉。这个投影空间的左平面的方程为x=-w,可是咱们把它写做-x-w=0的形式,这样他的法向量就是(-1,0,0)也就是指向投影空间的外部,保证其余六个面的法向量也指向物体外部,这样是为了方便后面的相交测试,固然使全部平面方程的法向量指向空间内部也是能够的,总之保持一致性就能够了。对于一个普通的平面方程A*x+B*y+C*z+D=0和一个顶点(a,b,c)将其代入原方程,若是A*a+B*b+C*c+D=0的话这个顶点就在平面上,若小于零则在平面一侧若大于零则在另外一侧,这个方法就能够用于包围盒与相机裁剪测试中。咱们的目的是在物体经不通过模型、视图、投影变换直接进行相交测试,那么必然要求得相机视景体在通过相机变换 模型变换前的六面体的6个平面方程。那么从当前的这个投影后的长方体就能够推导出这个所须要的六面体的六个面的方程。设局部坐标系下物体的任意一个顶点是vertex_local=(local_x,local_y,local_z,w)这里w=1;通过投影变换后的坐标vertex_per=(per_x,per_y,per_z);设M是模型变换矩阵,V是相机变换矩阵,P是透视投影变换矩阵。那么将会有以下的坐标变换关系vertex_local*MVP=vertex_per;其中MVP是模型视图投影三个矩阵的乘积,这个是显而易见的。这是目前能够利用的一个等式,咱们就是从这个等式推导出面的方程。这其中的矩阵M V P分别是模型 视图 投影矩阵,能够由OpenGL api得到,而后计算其乘积就好了。先来看一个图片:

在这个矩阵乘法当中一个原始坐标乘以一个4*4矩阵获得一个新的坐标。
其中 x_new=X1*x_origin+x2*y_origin+x3*z_origin+x_t*w;
    w_new= a*x_origin+b*y_origin+c*z_origin+d*w;
由上述可知投影以后在x轴方向的范围是[-w_new,w_new]
因此右平面上的点的方程式x_new=w_new;
结合上两个方程式获得:x_origin*(X1-a)+y_origin*(X2-b) +z_origin*(X3-c)+w*(x_t-d)=0;
因为初始w=1;因此     x_origin*(X1-a)+y_origin*(X2-b) +z_origin*(X3-c)+(x_t-d)=0;
其中小括号里的数据是能够计算出来的,至关于常量,因而上式的形式就是Ax+By+Cz+D=0。显然这是个平面的方程,而x y z是初始的点,那么这个方程就是初始的点所在的平面的方程。一样道理求得另外5个面的方程,而后作相交测试,即可以进行裁剪了。如今包围盒aabb 六个平面的方程已经了。
那么如今
如何进行相交测试呢?

如图一开始最小 最大点是红色的min max点,通过轴分离后最小 最大点变化为绿色的min  max在知道包围盒aabb和6个平面的方程后就可使用轴分离方向来测试相交问题。若是aabb最小的点在某个平面外的话,那么其余点必定在平面外。可是如今有一个问题就是怎么定义最大最小点,最大最小分别表示距离平面距离最大最小的点。一开始没有考虑平面的时候直接比较坐标值的大小来肯定最大最小点,如今引入平面方程后就要从新调整aabb获得新的包围盒。这里不试图给出严格的数学证实,只举一个简单的例子来讲明为什么要从新调整aabb。轴分离的话就是分别考虑x y z轴。如今假设考虑x轴,上述的投影后的长方体左平面方程为-x-w=0;假设aabb.max[]={-3,-3,-3} aabb.min[]={-5,-5,-5}可是如今距离-x-w=0(w<1.0) 最小的点显然是{-3,-5,-5) 最大点显然应该是(-5,-3,-3),而后y z轴以此类推,获得更新后的aabb。将新的aabb.min代入上面的平面方程若是大于0的话,算法马上return,这个aabb不在视镜体内,若是小于0的话,在将aabb.max代入,若是大于0的话设置标志insect=true;而后循环处理另外几个平面方程。最终若aabb不在包围盒内将会在循环的过程当中return,若是循环顺利结束,而且insect=true,说明aabb与视景体相交,不然aabb彻底在视景体中。

自此,本文就结束了,这只是任务中的一个小部分。。还有不少其余任务须要作。。

个人CSDN博客:

http://blog.csdn.net/cs_huster?viewmode=contents
邮箱:microland@126.com。欢迎指出文中的错误之处,本人也是初学者,由于毕设作这个,因而就开始看这方面的东西。

相关文章
相关标签/搜索