原文:http://blog.csdn.net/bluekitty/article/details/6070828
3D应用程序中,我们可以通过鼠标进行空间中物体的旋转和视角的变换等,而鼠标的移动是2D的(只有x,y坐标的变化),鼠标的这个2D移动是如何反映到3D空间中的旋转呢?这就要进行2D坐标和3D坐标的转换。
DXUT的CD3DArcBall类有一个成员函数ScreenToVector负责这个转换,下面是这个函数的实现:
D3DXVECTOR3 CD3DArcBall::ScreenToVector( float fScreenPtX, float fScreenPtY ){ // Scale to screen FLOAT x = -( fScreenPtX - m_Offset.x - m_nWidth / 2 ) / ( m_fRadius * m_nWidth / 2 ); FLOAT y = ( fScreenPtY - m_Offset.y - m_nHeight / 2 ) / ( m_fRadius * m_nHeight / 2 ); FLOAT z = 0.0f; FLOAT mag = x * x + y * y; if( mag > 1.0f ) { FLOAT scale = 1.0f / sqrtf( mag ); x *= scale; y *= scale; } else z = sqrtf( 1.0f - mag ); // Return vector return D3DXVECTOR3( x, y, z );}
变量说明:
m_Offset:POINT类型,D3D视口起始的偏移量,一般D3D视口都是独占并铺满一个窗口,所以一般为(0,0)。
m_fRadius:球的半径,这个半径一般为1。
m_nWidth,m_nHeight:视口的宽高,没有偏移的话,就是窗口的ClientWidth和ClientHeight。
这里简单的介绍一下ArcBall,想象有一个位于空间原点的球体,这个球的半径是1,它被x轴所在的纵向平面一切2半,一半位于z轴的正半轴,另一半在负半轴,在z正半轴的那个半球就是ArcBall(注意D3D是左手坐标系)。之所以定义半径为1,就是使所有位于这个球面上的向量的模都是1,是归一化的向量,便于后续计算,所以这个球也可以叫做归一化的球(实际可能没这个叫法,但就是这个意思)。
回到ScreenToVector这个函数,它实际是把屏幕平面坐标fScreenPtX 和fScreenPtY投影到这个球上以完成2D到3D的转换(逆投影),它的2个参数是float fScreenPtX, float fScreenPtY,很好理解,就是转化为float的屏幕坐标。前两行代码:
FLOAT x = -( fScreenPtX - m_Offset.x - m_nWidth / 2 ) / ( m_fRadius * m_nWidth / 2 );
FLOAT y = ( fScreenPtY - m_Offset.y - m_nHeight / 2 ) / ( m_fRadius * m_nHeight / 2 );对于一般情况(没有偏移,球是归一化的球),这两行代码可以简化成这样,
FLOAT x = -( fScreenPtX- m_nWidth / 2 ) / ( m_nWidth / 2 );
FLOAT y = ( fScreenPtY- m_nHeight / 2 ) / ( m_nHeight / 2 );
这两行代码乍一看不是很好理解,可以看出x和y的范围是-1到1,这有什么意义?记得刚才说的那个球被x轴所在的纵向平面一切为2么,实际上就是把那个切面上的圆的一个内接矩形当做你的屏幕了,如下图(图画的比较烂,稍微有点走样)
红色代表切面的那个圆,蓝色表示你的屏幕,屏幕的中心(实际是窗口的中心,这里假定是全屏显示的)和空间原点是重合的,那两行代码所求出的x,y值就是蓝色区域(也可以说是红色圆区域内,为什么后面有说明)内的点的坐标。当屏幕坐标fScreenPtX和fScreenPtY增大的时候,投影到空间坐标中那个蓝色矩形的点的坐标是如何变化的呢?应该的情况是x随fScreenPtX增大而增大,GDI坐标的y轴方向和空间坐标的y轴方向是相反的,所以y随fScreenPtY的增大而减小,但求的值正好相反,x是减小的(第一行代码将结果加了负号),而y是增大的,最终的结果接就是,当屏幕上的点向右下移动的时候,转换的空间坐标的点实际是在向左上移动。这和观察变换一摸一样(观察变换是世界变换的逆向变换),所以这个球用来模拟摄像机的位置非常合适。
继续往下看,这段代码
FLOAT z = 0.0f; FLOAT mag = x * x + y * y; if( mag > 1.0f ) { FLOAT scale = 1.0f / sqrtf( mag ); x *= scale; y *= scale; } else z = sqrtf( 1.0f - mag );
很明显是在求z值,但首先必须对x和y进行修正,必须进行修正的原因就是向量模的定义:向量的模代表了向量的长度,它的值是根号(x*x+y*y+z*z),即|v|^2=x^2+y^2+z^2,这个公式很容易用勾股定律推出来。因为我们求的向量终点都在那个球面上,所以v的模在这里就是m_fRadius也就是1,即x*x+y*y+z*z=1,很明显x*x+y*y不能大于1,代码中用变量mag存贮x*x+y*y,当mag>1的时候就必须修正xy的值使它们的平方和等于1,这时z=0,当mag<1的时候,z = sqrtf( 1.0f - mag )。最后函数返回一个对应平面坐标的向量。
现在回过头来整个再看一遍代码就可以用另外一种方式来解释这个变换,可以通过将2D的点逆投影到3D球体的表面来进行变换,窗口是2D矩形,它被放入空间后变形为一个圆形,窗口上的每一个点都被转化成这个圆范围内的点,这里是有变形的,例如,根据上面的计算,窗口左上角对应的空间坐标是(0.707,-0.707,0)(注意是空间坐标的反方向,值是根号2除以2),那我要问你窗口左边中点对应空间坐标的x坐标是什么?用GDI的方式考虑,因为x方向上没有变化,只是点垂直向下移动了,所以横坐标没有变化,但对应空间坐标的x是0.707么?试一试就知道了。