By: Anson | May 20th, 2008 | Tags: c/cpp, CG
以前JAY写过一篇鼠标旋转物体算法的文,不过是OpenGL的。最近初学D3D,对忘光的向量和矩阵非常怨念,便自己写一篇D3D的文加强记忆。
其实鼠标旋转物体的思路就是将物体看做是被一个圆球包裹起来的,然后通过判断鼠标在圆球上移动的位置来旋转那个圆球。
这里仅用当场景中只有一个居中物体的情况来讲述。
首先我们需要定义一些变量来储存所需信息
D3DXQUATERNION g_qNow, g_qDown; //分别是 现在的旋转用四元数 按下鼠标时的旋转用四元数
D3DXVECTOR3 g_vDownPt, g_vCurrentPt; //分别是 按下鼠标时的球面上的位置向量 当前的位置向量
bool g_bDrag = false; //是否按下鼠标左键 用来判断按键拖拽
float g_fRadius = 0.0f; //半径 其实是相对于窗口大小的
然后初始化这些变量
D3DXQuaternionIdentity(&g_qNow);
D3DXQuaternionIdentity(&g_qDown);
g_bDrag = false;
g_fRadius = 0.9f; //让包裹物体的球不占满整个窗口 这样就可以只绕Z旋转
好了,初始化工作做完了,然后具体讲下算法。
鼠标在球上的位置向量可以通过鼠标在窗口中的位置和物体中心的位置计算所得。
其中 x = -(screenX - objectX)/radius; y = (screenY - objectY)/radius; z = sqrt(1 - x^2 - y^2);
因为x坐标在窗口和在空间坐标系中是相反的,所以取负。
在此例中因为只有一个物体居中在窗口中 所以代码是这样的
D3DXVECTOR3 ScreenToVector(float fScreenPtX, float fScreenPtY) {
float x = -(fScreenPtX - SCREEN_WIDTH/2) / (g_fRadius * SCREEN_WIDTH/2);
//以窗口宽度的一半作为物体中心x坐标,以窗口宽度的一半*g_fRadius作为椭圆x半径
float y = (fScreenPtY - SCREEN_HEIGHT/2) / (g_fRadius * SCREEN_HEIGHT/2);
//以窗口高度的一半作为物体中心y坐标,以窗口高度的一半*g_fRadius作为椭圆y半径
float z = 0.0f;
float mag = x*x + y*y;
if (mag > 1.0f) { //如果mag大于1即说明鼠标位置在球体之外,将xy标准化,z为0。即绕z轴旋转
float scale = 1.0f / sqrtf(mag);
x *= scale;
y *= scale;
} else
z = sqrtf(1.0f - mag);
return D3DXVECTOR3(x, y, z);
}
这段代码可以DXSDK的Sample中找到,在DXUT中。它返回的是一个单位向量。
接下来是事件处理过程
case WM_LBUTTONDOWN:
g_bDrag = true; //开始拖拽
g_qDown = g_qNow;
g_vDownPt = ScreenToVector((float)x, (float)y); //得到按下鼠标时的位置向量
break;
case WM_MOUSEMOVE:
if (g_bDrag) { //如果是拖拽
g_vCurrentPt = ScreenToVector((float)x, (float)y); //得到当前位置向量
g_qNow = g_qDown * QuatFromBallPoints(g_vDownPt, g_vCurrentPt); //从两个向量计算旋转四元数并累加
}
break;
case WM_LBUTTONUP:
g_bDrag = false; //停止拖拽
break;
看下其中的QuatFromBallPoints函数
D3DXQUATERNION QuatFromBallPoints(const D3DXVECTOR3 &vFrom, const D3DXVECTOR3 &vTo) {
D3DXVECTOR3 vPart;
float fDot = D3DXVec3Dot(&vFrom, &vTo); //取得两向量的点乘,因为两个都是单位向量,所以fDot等于cos theta
D3DXVec3Cross(&vPart, &vFrom, &vTo); //叉乘,获得的是垂直于两个向量的一个向量,即旋转轴。其模等于|a||b|sin theta等于sin theta
return D3DXQUATERNION(vPart.x, vPart.y, vPart.z, fDot); //正好构成一个旋转2*theta角度的四元数
}
这段也是DXUT中的代码。首先说一下旋转四元数,一个(x*sin theta, y*sin theta, z*sin theta, cos theta)的四元数被用来旋转2*theta角度。在上面的代码中通过两个单位向量得到了一个旋转四元数。从效果上来说就是鼠标在球体上做过theta角度,物体就旋转2*theta角度。这是一个很方便的解决方法,不仅省却了换算,也使得物体能够在一次拖拽中旋转360度。现在大部分事情都清楚了,接下来只要在绘制之前把世界矩阵按所得的旋转四元数旋转之后绘制物体就可以了。
我们使用D3DXMatrixRotationQuaternion这个函数从四元数得到一个旋转矩阵作为世界矩阵。
D3DXMATRIXA16 matWorld;
D3DXMatrixRotationQuaternion( &matWorld, &g_qNow );
g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );
至此完成,我们只需要在每一次绘制的时候按当前的旋转四元数即g_qNow来设置世界矩阵即可。
写完发现我的语言表达能力果然很差。。。