深入探讨透视投影坐标变换

Leave a comment


写3d图形程序,就一定会做坐标变换。而谈到坐标变换,就不得不提起投影变换,因为它是所有变换中最不容易弄懂的。但有趣的是,各种关于透视变换的文档却
依然是简之又简,甚至还有前后矛盾的地方。看来如此这般光景,想要弄清楚它,非得自己动手不可了。所以在下面的文章里,作者尝试推导一遍这个难缠的透视变
换,然后把它套用到 DX和 PS2lib 的实例中去。


  1. 一般概念

所谓透视投影变换,就是view 空间到project
空间的带透视性质的坐标变换步骤(这两个空间的定义可以参考其他文档和书籍)。我们首先来考虑它应该具有那些变换性质。很显然,它至少要保证我们在view空间中所有处于可视范围内的点通过变换之后,统统落在project空间的可视区域内。好极了,我们就从这里着手——先来看看两个空间的可视区域。


由于是透视变换,view空间中的可见范围既是常说的视平截体(view frustum)。如图,


(图1)

它就是由前后两个截面截成的这个棱台。

从view空间的x正半轴看过去是下图这个样子。


(图2)


接下来是project空间的可视范围。这个空间应当是处于你所见到的屏幕上。实际上将屏幕表面视作project空间的xoy平面,再加一条垂直屏幕向
里(或向外)的z轴(这取决于你的坐标系是左手系还是右手系),这样就构成了我们想要的坐标系。好了,现在我们可以用视口(view
port)的大小来描述这个可视范围了。比如说全屏幕640*480的分辨率,原点在屏幕中心,那我们得到的可视区域为一个长方体,它如下图(a)所示。


(图3)


但是,这样会带来一些设备相关性而分散我们的注意力,所以不妨先向DirectX文档学学,将project空间的可视范围定义为x∈[-1,1],
y∈[-1,1],
z∈[0,1]的一个立方体(上图b)。这实际上可看作一个中间坐标系,从这个坐标系到上面我们由视口得出的坐标系,只需要对三个轴向做一些放缩和平移操作即可。另外,这个project坐标系对clip操作来说,也是比较方便的。


  1. 推导过程

先从project空间的x正半轴看看我们的变换目标。


(图4)

    

这个区域的上下边界为y’=±1,
而图2中的上下边界为y =

± z *
tan(fov/2),要实现图
2到图4的变换,我们有y’ = y * cot(fov/2) /
z。这下完了,这是一个非线性变换,怎么用矩阵计算来完成呢?还好我们有w这个分量。注意到我们在做投影变换之前所进行的两次坐标变换——world变换
和view变换,他们只是一系列旋转平移和缩放变换的叠加。仔细观察这些变换矩阵,你会发现它们其实不会影响向量的w分量。换句话说,只要不是故意,一个
w分量等于1的向量,再来到投影变换之前他的w分量仍旧等于1。好的,接下来我们让w’= w*z,
新的w就记录下了view空间中的z值。同时在y分量上我们退而求其次,只要做到y’ = y *
cot(fov/2)。那么,在做完线性变换之后,我们再用向量的y除以w,就得到了我们想要的最终的y值。

     

x分量的变换可以如法炮制,只是fov要换一换。事实上,很多用以生成投影变换矩阵的函数都使用了aspect这个参数。这个参数给出了视平截体截面的纵横比(这个比值应与view
port的纵横比相等,否则变换结果会失真)。如果我们按照惯例,定义aspect = size of X / size of
Y。x = z*tan(fov/2)*aspect那么我们就可以继续使用同一个fov而给出x分量的变换规则:x’ = x * cot(fov/2) / aspect。

      现在只剩下z分量了。我们所渴望的变换应将z = Znear 变换到z = 0,将z = Zfar变换到z =
1。这个很简单,但是等等,x, y最后还要除以w,你z怎能例外。既然也要除,那么z = Zfar 就不能映射到z = 1了。唔,先映射到z =
Zfar试试。于是,有(z-Znear)/(Zfar-Znear)=z
’/Zfar (成比例) ;   z’ = Zfar*(z-Znear)/(Zfar – Znear)。接下来,看看z’/z的性质。令f(z) = z’/z =
Zfar*(z-Znear)/(z*(Zfar – Znear))。

   则f’(z) = Zfar * Znear / ( z^2 * (Zfar –Znear )), 显而易见f’(z) >
0。所以除了z = 0是一个奇点,函数f(z)是一个单调增的函数。因此,当Znear
≤zZfar时,f(Znear)≤f(z)≤f(Zfar),
即0≤f(z)≤1。


至此,我们可以给出投影变换的表达式了。


x’ = x*cot(fov/2)/aspect


y’ = y*cot(fov/2)


z’ = z*Zfar / ( Zfar – Znear ) – Zfar*Znear / ( Zfar – Znear
)


w’ = z


以矩阵表示,则得到变换矩阵如下,


cot(fov/2)/aspect          0                   0                   0

         
0               cot(fov/2)               0                   0

         
0                      0       Zfar/(Zfar-Znear)          1

         
0                      0     -Zfar*Znear/(Zfar-Znear)  0。


做完线性变换之后,再进行所谓的“归一化”,即用w分量去除结果向量。



  现在我们考虑一下这个变换对全view空间的点的作用。首先是x和y分量,明了地,当z>0时,一切都如我们所愿;当z<0时,x和y的
符号在变换前后发生了变化,从图象上来说,view空间中处于camera后面的图形经过变换之后上下颠倒,左右交换;当z= 0
时,我们得到的结果是无穷大。这个结果在实际中是没有意义的,以后我们得想办法弄掉它。再来看z,

    

仍旧拿我们上面定义的f(z)函数来看,我们已经知道当z
≥Zfar时,f(z)≥1;同时当z→+∞,f(z)→Zfar/(Zfar-Znear);当z→+0时,f(z)→-∞;
z→-0时,f(z)→+∞; z→∞时,f(z)→Zfar/(Zfar-Znear).由此我们画出f(z)的图像。



(图5)

     

由此图可以看出当z
≤0时,如果我们仍旧使用f(z)进行绘制会产生错误。所以我们会想需要clip操作——只要这个三角形有任意一个顶点经过变换后z值落在[Zfar/(Zfar-Znear),

+∞]区间中,我们就毫不怜悯地抛弃她——因为无论如何,这个结果是错的。那么万一有三角形在view空间内横跨了Znear到0的范围,按我们想应该是
画不出来了。但是回想一下我们所看见过的DirectX程序,似乎从未看到过这种情况。有点奇怪,但是不得不先放放,稍后再说。


3.到DirectX中求证


在DirectX中拿一个用fov生成投影矩阵的函数来看。


D3DXMATRIX* D3DXMatrixPerspectiveFovLH( D3DXMATRIX*

pOut
,
FLOAT

fovy
,
FLOAT

Aspect
,FLOAT

zn
,
FLOAT

zf

)

      

这个函数恰好使用了我们刚才推导所使用的几个参数,经过一些数据的代入计算之后,我们就会发现它所产生的矩阵就是我们计算出来的。看来,DirectX的
思路和我们是一致的。好的,一个问题解决了,但一个新的问题接着产生——DirectX是怎么做clip的?我不知道,而且看样子现在也知道不了,只能期
待牛人相助或者是碰到一本好书了。


4.研究ps2lib的投影变换



  其实投影变换都是一回事,但是PS2lib的函数怎么有点不一样呢?仔细看看,原来我们的思路是先做“归一化”,然后再做view
port的放缩和平移,而PS2不是这样——它把“归一化”放在最后。接下来,我们就按这个顺序试试。


先看缩放操作,把它和除z交换顺序很方便,直接换便是了。于是我们记view port 的宽度为Vw,高度为Vh,
Z缓存的最大值为Zmax, 最小值为Zmin则有


x’ = x * cot(fov/2)/aspect*(Vw/2)


y’ = y * cot(fov/2)*(Vh/2)


z’ = Zfar(z-Znear)/(Zfar-Znear) * (Zmax-Zmin);


w’ = z


再看平移部分,既然是要平移后再除,则必须平移原来的z倍,于是我们又记view port中心坐标为(Cx, Cy),就有


x’’ = x’ + z * Cx


y’’ = y’ + z * Cy


z’’ = z’ + z * Zmin


w’’ = w


好的,我们看看cot(fov/2)等于什么,从图2看,实际上它就是D/(Vh/2),那么cot(fov/2)/aspect实际上就是D/(Vw/2)。但是,ps2在这上面耍了个小花招,它在view空间中的view
port和project空间的view port可以不相等。最明显的一点是,它在view空间中的view
port的高度为480,但实际上它的输出的y向分辨率只有224。也就是说,ps2想要输出纵横比等于电视机的图像,就必须在y向上再加一个缩放。这个缩放在我们的变换中体现在哪呢?就在y’
= D/(Vh/2) * (Vhscr/2)中,注意到两个Vh不相等(project空间中的Vh记成Vhscr),两个值一运算就得到x’ =
D*(224/480) =
0.466667D。这个0.4666667就是ps2lib函数参数ay的由来。同理,我们亦可得知ax一般应取值为1。那么,实际上ps2lib函数的scrz,ax,
ay三个参数的作用等同于DirectX的象形函数的fov和aspect,在确定的规则下,他们可以相互转换,得到性质完全相同的透视变换。至于这个规则,这里就不给出了。


转回正题,有了上面的讨论,我们就可以展开我们的变换表达式如下,


x’’ = x * scrz * ax + z * Cx


y’’ = x * scrz * ay + z * Cy


z’’ = z * (Zfar*Zmax–Znear*Zmin)/(Zfar – Znear)


–Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)


w’’ = z


z分量好像还有点不一样,注意到一般ps2程序在z
buffer的操作为greater&equal,而DirectX的操作为less&equal,就是说,z方向得做些变动——得把z=Znear映射到z’’ =
Zmax,z=Zfar映射到z’’=Zmin。说变就变,我们马上有


z’ = Zfar(z-Znear)/(Zfar-Znear)*(Zmin-Zmax)


z’’ = z’+Zmax


再次展开,得到z’’ = z * (Zfar*Zmin–Znear*Zmax)/(Zfar – Znear )


+ Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)


好了,用矩阵把这个变换写出来,


scrz*ax      0                               0                                 0

  
0       scrz*ay                           0                                 0

  
Cx        Cy     (Zfar*Zmin–Znear*Zmax)/(Zfar – Znear )     1

  
0           0       Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)         0,



这下就完全一样了。下面的任务就是看看这个变换的性质。因为最后同样要除以z,所以x,y分量上的情形的和原来我们推导的DirectX的投影变换是一样的,区别在z分量上。来看新的f(z)函数,它的图像为



(图6)


5.结论



至此,我们已经完成了预定的目标。但是,将坐标变换完全掌握之后,为了做一个像样的图形程序,我们还有更多事情要做——至少在PS2上是这样

在面试一个游戏编程职位前,你需要知道的东西

Leave a comment

      很长时间以来,游戏产业不允许新的血液加入,因为每个职位都要求申请人有2年以上的经验.   但是现在是加入游戏产业的最好的时机.  
因为研发队伍的急剧增长,   有经验的人很难找了,   所以越来越多的公司从大学里招人.  
不管你上的是什么大学,你都有机会在游戏产业里找到一份工作.   你需要熟悉以下领域. 
        数学   –  
对于视频游戏开发,这个非常重要.   不,你不需要去记如何求一个绕x轴旋转的曲面的表面积.   你需要有很好的线性代数技能.  
如果你不知道点乘的公式(a   dot   b   =   a.x   *   b.x   +   a.y   *   b.y   +  
a.z   *b.z   )和它的几何意义( ¦a ¦   * ¦b ¦   *   a和b夹角的余弦),   你是不可能获得工作的.  
你需要知道如何把一个向量投影到另一个向量上(a到   b的投影   =   (a   dotb)/ ¦b ¦^2*   b)  
以及一个平面上.   你需要知道向量积(叉乘)是什么(a   x   b   =   [a.y   *b.z   –   a.z   *  
b.y,   a.z   *   b.x   –   a.x   *   b.z,   a.x   *   b.y   –   a.y   *
  b.x]),以及它的几何意义(一个和两个向量都垂直的向量,长度等于原来两向量的长度乘以两向量的夹角的正弦).  
我记忆向量积公式的方法是把i,j,k   放在矩阵的第一行,然后是第一个向量,最后一行是第二个向量,   然后计算这个行列式.  
如果你不知道什么是行列式或者如何计算一个行列式,   这可不是个好的信号!  
你应该知道一个矩阵的逆以及如何计算,你应该知道如何转置一个矩阵.   你应该知道什么是正交矩阵以及求它的逆的简单方法(求它的转置就行了).  
熟悉各种坐标空间之间的转换是很重要的.   碰撞检测函数背后的数学原理也很需要学习.  
你如何找到一条光线和另一条光线的碰撞点?一个平面呢?一个球面呢?   什么是四元数(quaternions)?  
你如何对它们做一些常见的操作?   如果你的数学已经不那么熟了或者不知道如何开始,   我推荐   "3D   Math   Primer  
for   Graphics and  Game  Development".我读过以后发现它很好地覆盖了所有的基础知识.  
我没有读过别的书来做比较,   但是这本书肯定会给你打下很好地基础,同时它也是很多游戏开发中常见数学运算的参考资料. 
       
物理   –   你需要对各种抛物体的公式非常熟悉.如果你不能记住或推导出d   =   v0   *   t   +   0.5   *  
a   *   t^2,   你肯定会在面试或编程测试中遇上麻烦.   你应该能轻松解决任何涉及到抛物体和重力的问题.不论未知量是重力加速度,
  发射角,   y方向速度,   xz方向速度,   时间,距离,  
或其他任何可解的组合,你应该能没有任何困难的求出解来.你应该熟悉弹性和非弹性碰撞下的动量和动能变化.   滑动和摩擦力也是你应该理解的概念. 
 
      C++   –   虽然一些地方还在用c多过c++,但是C++已经是游戏编程中向被广泛接受的几乎标准的语言.  
C++的知识是非常重要的.因为如果用的不好,   你会写出非常糟糕的代码.首先,你应该知道什么是虚函数,以及如何使用它.  
理解虚函数表是如何工作的也同样重要,这样你就能够理解当你调用虚函数时需要的额外步骤,这些步骤会降低性能(每个对象都有一个隐藏的虚函数表指针[具体
在哪取决于编译,并且对指针做类型转换时,this会有变化],这样调用一个虚函数,你需要根据这个指针找到那个函数表,然后跳到它所包含的函数指针所指
的函数里,这会降低cache的效果).   知道如何有效地使用模板和操作符重载也很重要.   关于C++,好的OO设计是最重要的.  
最常用的一些类层次很深而且很费解,你最后需要做可怕的虚拟继承或者为一些不需要的变量浪费内存. 
        人工智能   –  
我在大学里学习了标准的人工智能课程,但是这些东西没有一点在游戏AI中是可接受的.   教的很多算法很难实现或者对游戏而言不现实.  
你知道什么是有穷状态机就够了.   绝大部分游戏使用有穷状态机,   而那些不用这个的游戏,使用的是简单的基于规则的系统.  
"白与黑"(包括它的续集)是我能想到的唯一的使用了其他东西(神经网络)的例外,   而且很多玩家对它的结果并不满意.   如果你想找本书,  
我推荐"AI   Game   Programming   Wisdom   3".  
里面有两篇特别优秀的文章,一个是关于有趣的游戏AI设计,另一个是关于行为合成.  
严肃的说,虽然书会很有帮助,但是学习游戏AI的最好的方法是实际做一个. 
        图形学   –   这是一个重要的领域.  
如果图形是你的主要感兴趣的方向,那么你应该知道的比这里说的多.但是,不论你想要在这个产业里做什么职位,   你应该知道一些基础.  
你需要知道什么是渲染状态以及通常都有哪些渲染状态?(cull,   z   write,   ztest之类的).什么是标准的光照方程?(I
  =ka*Ia   +   Ii*(kd*(L   dot   N)   +   ks*(R   dot   V)^n)   ).  
材质映射是如何工作的?材质的wrapping和clamping有什么区别?  
如何高效的渲染程序中一组几何体?(按照渲染状态组织你的几何体,按照你的硬件最快的primitive分批,饭后发给图形卡).你需要知道  
skinning   是如何工作的.   如果你对书感兴趣,   我推荐   "3D   Computer   Graphics".  
这个不是专为游戏写的,多以它包括了一些在游戏中不现实的技术.但是,我想一个总体的了解是有帮助的,而且,随着游戏硬件的发展,  
那些现在看来不现实的技术会变的普通的. 
        工具开发  –  C#  
看来在游戏业界获得了不少地位.所以你也应该让你熟悉一下C#.C#在加快工具的开发上表现出不少优势.很多人说工具可以制造下一代的游戏,  
我同意这一点. 拥有的工具越多,团队花在可以被轻易自动化的工作上的时间就越少.   可能你的团队有10个美工,  
花程序员的一个人年来开发一个可以给美工每天省15分钟的工具并不值得,但是如果你有50个美工,那这就肯定值得了.C#可以快速的创建GUI,而且和其
他一些能简化创建GUI的方法不同,  
这些GUI很好理解和使用,因为他们是标准的windows组件,允许你方便的设定快捷键,就像其他Windows程序一样.   Visual  
Studio   里的Form   Designer  
真的很好用,而且C#的metadata和event驱动的设计使得工具的开发轻而易举.你最好也对mfc有基本的了解,因为可能面试你的开发人员会有点
落后与时代.如果你有兴趣学习很多关于这些技术是如何工作的(这会让你写出更好的C#代码),我强烈推荐"Applied   Microsoft  
.NET   Framework  
Programming",我在空闲的时候读这本书,然后我就不能放下了.我一章一章不停的读,因为我是那么激动的像学习下一章的话题了.记住,  
这本书不会教你各种framework里的类和这些类是干嘛的.它讲的是这个语言的核心机制,它是如何工作的.   这本书也不是专门为C#写的.  
它告诉了你如何用VB.net做一些在C#里不能做的是,它还提供了用MSIL(Microsoft Intermediate   Language,
  所有.net语言的编译器都会编译出这种字节码)写的例子,实现一些不被任何一种.net语言支持的功能. 
      操作系统   –
  虽然这个看起来和这个和游戏开发相对而言不是太相关,   但是我很高兴我在大学学习了操作系统的课程,因为我经常会用到从这门课学到的知识.  
关于操作系统的知识非常重要,因为它在幕后为程序处理着很多重要的工作.特别是由于下一代的游戏终端和双核PC上的多核特性,多此案成的知识是至关重要
的.对虚拟内存的了解当然是很有用的.关于文件系统,内核态和用户态切换,分页的知识对PC游戏开发更为有用,  
不过随着一代代的发展,终端也在变的越来越像PC.所以,知道这些东西没有坏处. 
    编译器   –  
虽然知道如何写一个分析器可能并不重要,  
但重要的是知道你的代码是如何编程汇编代码的.这样你就能明白如何写出更优化的代码.你显然必须熟悉把你写的程序变为可执行文件的4个步骤:预处理、编
译、汇编和链接.   编译器如何处理常量的?   很好的理解宏和知道如何用它来简化一些常用的操作是很重要的.  
知道你正在用的编译器的调用约定,因为参数超过一定数目的时候,会导致它们被放在栈上而不是在预留的寄存器里.  
调用约定的知识在你用汇编写函数的时候也是有用的. 
    这些只是一个作为一个游戏程序员,你应该具备的各种知识和技能的入门介绍.   对每个领域,根据你的工作类型,总会有更多的你需要学习的东西.   知道一些实际使用的知识之外的东西总是没有坏处的.

[3D基础]理解计算机3D图形学中的坐标系变换

Leave a comment

  要谈坐标系变换,那么坐标系有哪些呢?依次有:物体坐标系,世界坐标系,相机坐标系,投影坐标系以及屏幕坐标系.我要讨论的就是这些坐标系间的转换。
     这些坐标系不是凭空而来,他们都是为了完成计算机3D图形学最最最基本的目标而出现.
     计算机3D图形学最最最基本的目标就是:将构建好的3D物体显示在2D屏幕坐标上.
     初看好像就是将最初的物体坐标系转换到屏幕坐标系就可以了呀,为什么多出了世界坐标系,相机坐标系,投影坐标
系。这是因为:在一个大世界里有多个物体,而每个物体都有自己的坐标系,如何表述这些物体间相对的关系,这个多出了世界坐标系;如果只需要看到这个世界其
中一部分,这里就多出了相机坐标系;至于投影坐标系那是因为直接将3D坐标转换为屏幕坐标是非常复杂的(因为它们不仅维度不同,度量不同(屏幕坐标一般都
是像素为单位,3D空间中我们可以现实世界的米,厘米为单位),XY的方向也不同,在2D空间时还要进行坐标系变换),所以先将3D坐标降维到2D坐标,
然后2D坐标转换到屏幕坐标。
理解3D图形学的第一步:理解左手坐标系与右手坐标系
     为什么会有左手坐标系与右手坐标系之分?
     在3D空间(没错!就是3D)中,所有2D坐标系是等价的(就是通过一系列的仿射变换,可以互相转换)
     而3D坐标系不是等价的,通过仿射变换,是无法将左手坐标系转换到右手坐标系;也就是说,物体坐标系用的就是左手坐标系,世界坐标系用的是右手
坐标系,那么物体可能就是不会是我们所希望的样子了,可能是倒立的,也可能是背对着我们的,所以我们要区分左手坐标系与右手坐标系。也许在4D空间,左右
手坐标系就可以互相变换了吧。
     进入正题吧:
     首先讨论的是物体坐标系->世界坐标系
     前面说了为了描述多个物体间相对的关系,这里引进了世界坐标系,所以世界坐标系是个参考坐标系。
     这一步的目的将所有的物体的点都转移到世界坐标系,这里主要涉及的是旋转,缩放,平移等。
     不过我将详细说明为何及如何用矩阵来描述这些变换。
     例:如果有两个坐标系C与C`, C`是C绕Z轴旋转θ得到的。下面是各坐标轴的变换:
             
     如果是C坐标系的点P(x, y, z),而在C`的表示就是
   
     这时该如何建立矩阵呢? 答案就是区分你用的是行向量还是列向量.也许有人会问为什么不区分是左手坐标系还是右手坐标系呢?因为C可以变换到C`,那么他们一定是同在左手坐标系或右手坐标系,变换只能在可以互相转换的坐标系之间进行。
     如果你用的是行向量:由于行向量只能左乘矩阵(注意乘与乘以的区别)
     所以矩阵形式应该是这样
    
    只有这样,在左乘矩阵时才能得到上面P`的形式。
    
     如果你用的是列向量: 由于列向量只能右乘矩阵(注意乘与乘以的区别)
     所以矩阵形式应该是这样
    
     只有这样,在右乘矩阵时才能得到上面P`的形式。
    
     至于如何旋转,缩放,平移我不在多说。
     …………………………………觉得自己好像跑题了.还好这两个坐标系变换很简单。
     我们再讨论世界坐标系->相机坐标系
     引进相机的目的就是只需看到世界的一部分,而哪些是可以在相机里看到的,就需要进行筛选。将物体转换到相机坐标系,这样相机坐标系进行筛选时就会简单很多。这里的重点是构建相机坐标系。
    
物体坐标系,世界坐标系是美工在绘制时就定义好了的。而相机坐标系是需要程序实时构建的。(当然这是通常情况下,如果你要建立一个世界,这个世界都是围绕
你转,要实时改变所有物体坐标系,固定相机坐标系(其实这时候相机坐标系就是世界坐标系),建立一个地心说的世界,我也没办法,你的思维也太不一样了。)
     如何构建相机坐标系呢?首先我们要明确目标:我们是要构建3D坐标系(好像是废话),三个坐标轴要互相垂直(也好像是废话).
     我们一般用UVN相机。例如:D3D的D3DXMatrixLookAtLH,D3DXMatrixLookAtRH,OGL的gluLookAt(右手坐标系).
     如何建立呢UVN相机呢? 我们就要利用叉积这个工具了:两个不平行,不重叠的向量的叉积可以得到与这两个向量互相垂直的向量。
     如果有了相机的位置与目标的位置那么我们可以确定一个Z轴(有人问为什么是Z轴,因为物体的远与近我们就习惯用Z值来表示的)。求Z轴时要注意
是左手坐标系还是右手坐标系,左右手坐标系XY轴方向相同时,Z轴的方向相反。所以左手坐标系是目标位置减去相机位置,而右手坐标系则是相机位置减去目标
位置。记得normalize
     这是我们要得到X与Y轴了。如何求X,Y轴呢?
     一般方法是:
     1、选择一个临时Y轴,
     2、对临时Y 与Z 轴进行叉积求得一个X轴
     3、X轴再与Z轴进行叉积,得到一个Y轴。
     有了XYZ就可以求出旋转的相机矩阵了。
     如何选择一个Y轴呢?大多数情况下是(0,1,0),但是如果是相机位置E与目标位置T垂直,即(E-T=(0,+/-1,0)时),这时就不能用(0,1,0)了, 因为两个平行向量的叉积是零向量,所以我们就要另选一个Y轴。
     但是我觉得我们可以改变方法。
     如果不能选Y轴,我们就选择一个临时X轴,这个临时X轴就是(1,0,0)。
     然后再对临时X轴与Z轴进行叉积求得一个Y轴。
     最后Y轴再与Z轴进行叉积,得到X轴。
     这样可以得到XYZ轴。
     最后再根据行向量与列向量建立相机矩阵,再进行平移。

     相机坐标系->投影坐标系.
     投影的目的就是:降维.
     两种投影方式:正交投影与透视投影.
     在我们TEAM中易颖已经写了,我就不多说了,大家去看他的文章。

     投影坐标系->屏幕坐标系
     这是最简单的。2D坐标变换。也不多说。