用DirectX實現魔方(三)視角變換及縮放(附源碼)

在本系列第一篇介紹過鼠標按鍵的功能,以下。html

  • 左鍵拖拽 - 旋轉魔方
  • 右鍵拖拽 - 變換視角
  • 滾輪 - 縮放魔方

今天研究一下如何實現後面兩個功能,用到的技術主要是Arcball,Arcball是實現Model-View-Camera的重要技術,這裏的旋轉基於Quaternion(四元數)來實現,固然也能夠經過歐拉角來實現,可是歐拉角的旋轉不夠平滑。先看一下Model-View-Camera的效果,以下,這個gif效果圖是用LICEcap錄製的,幀率有些慢,略有卡頓現象,你們能夠下載文末的可執行文件查看更加平滑的效果。git

右鍵拖拽 - 變換視角

由上面的動畫能夠看到,經過用戶按下並拖拽鼠標右鍵便可以旋轉視角(表面上看是魔方在旋轉,但其實是camera在旋轉,相對運動而已)。爲了研究這個功能是如何實現的,咱們能夠將鼠標右鍵拖拽這個過程分解一下。github

  • 按下鼠標右鍵(此時鼠標的位置是P1)
  • 拖拽右鍵(此時鼠標的位置是P2,注意P2是隨拖拽實時變化的)
  • 擡起鼠標右鍵(中止旋轉)

爲了實現上面的功能,咱們在屏幕上虛擬出一個球體來,將P1和P2映射到這個球體,再從球心到P1和P2連線構成兩個向量,有了這兩個向量就能夠求出旋轉軸及旋轉角度了,這個虛擬的球體,就是Arcball了,以下圖。函數

在上圖中P1和P2的夾角就是旋轉角度,N則是旋轉軸。旋轉角度能夠經過P1和P2的點積來實現,旋轉軸能夠經過P1和P2的叉積來實現,稍後詳述,下面看看如何將屏幕上的點映射到球體上,這是實現Arcball的關鍵步驟。直觀一點的想法,能夠把屏幕當作一個矩形紋理,球體看作一個模型,因此將屏幕座標映射到球體座標的過程實際上至關於將這個矩形紋理貼圖到球體上。須要注意的是,咱們這裏只用到半個球體(若是屏幕將球體一份爲二的話)。動畫

屏幕座標到球座標

看代碼,顧名思義,這個函數完成屏幕座標到球體座標(單位向量)的轉換,兩個輸入參數分別是鼠標按下時屏幕的X,Y座標。ui

 1 D3DXVECTOR3 ArcBall::ScreenToVector(int screen_x, int screen_y)
 2 {
 3     // Scale to screen
 4     float x = -(screen_x - window_width_ / 2) / (radius_ * window_width_ / 2);
 5     float y = (screen_y - window_height_ / 2) / (radius_ * window_height_ / 2);
 6 
 7     float z = 0.0f;
 8     float mag = x * x + y * y;
 9 
10     if(mag > 1.0f)
11     {
12         float scale = 1.0f / sqrtf(mag);
13         x *= scale;
14         y *= scale;
15     }
16     else
17         z = sqrtf(1.0f - mag);
18 
19     return D3DXVECTOR3(x, y, z);
20 }

代碼解釋:spa

4-5兩行代碼將屏幕座標映射到球體座標的範圍,但此時還只是xy兩個份量,因此後續的代碼都是計算z座標並單位化的。這裏radius_是球體的半徑,爲了方便計算,一般設置爲1。3d

10-15行,若是xy的平方和大於1,此時該點剛好位於半球球的邊緣,因此令z=0code

17行,若是xy平方和小於1,說明該點不位於半球邊緣,計算z的值。orm

19行返回球體座標對應的向量(已經單位化)。

關於這個函數更加詳細的解釋,看以看看個人另外一篇隨筆,ScreenToVector詳解

旋轉軸及旋轉角度

這裏咱們用四元組來表示旋轉,一個四元組包含四個份量x, y, z, w。假設一個旋轉的旋轉軸是axis,旋轉角度是theta。那麼對應的四元組q以下。

q.x = sin(theta / 2) * axis.x;
q.y = sin(theta / 2) * axis.y;
q.z = sin(theta / 2) * axis.z;
q.w = cos(theta / 2);

有了上面的公式,咱們就能夠根據旋轉軸和旋轉角度來構造四元組了。下面的函數就是用來作這件事的,兩個參數分別是旋轉的起始向量和結束向量,這兩個向量是由前面的ScreenToVector函數生成的。

 1 D3DXQUATERNION ArcBall::QuatFromBallPoints(D3DXVECTOR3& start_point, D3DXVECTOR3& end_point)
 2 {
 3     // Calculate rotate angle
 4     float angle = D3DXVec3Dot(&start_point, &end_point);    
 5 
 6     // Calculate rotate axis
 7     D3DXVECTOR3 axis;
 8     D3DXVec3Cross(&axis, &start_point, &end_point);        
 9 
10     // Build and Normalize the Quaternion
11     D3DXQUATERNION quat(axis.x, axis.y, axis.z, angle);
12     D3DXQuaternionNormalize(&quat, &quat);
13 
14     return quat;
15 }

代碼解釋:

第4行,計算量個向量的夾角餘弦值,用的是點積公式,兩個向量a和b,他們的點積a dot b = |a||b|cost(theta),若是a和b都是單位向量的話,那麼a dot b = cost(theta),這裏start_point和end_point已是單位向量了,因此angle = cos(theta)。

第7,8兩行代碼計算旋轉軸,用的是叉積公式,兩個向量P1和P2的叉積生成第三個向量N,且N垂直於P1和P2。

第11,12行構造四元組,並單位化。須要注意的是旋轉軸部分並無嚴格按照上面的四元組公式,由於旋轉軸是一個向量,而同一個方向能夠有多種表示方法,好比(1,2,3)和(2,4,6)表示的是同一個方向向量。

Arcball的調用

Arcball能夠在處理Windows消息的時候調用。

LRESULT Camera::HandleMessages(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // update view arc ball
    if(uMsg == WM_RBUTTONDOWN)
    {
        SetCapture(hWnd) ;

        frame_need_update_ = true ;
        int mouse_x = (short)LOWORD(lParam) ;
        int mouse_y = (short)HIWORD(lParam) ;
        view_arcball_.OnBegin(mouse_x, mouse_y) ;
    }

    // mouse move
    if(uMsg == WM_MOUSEMOVE)
    {
        frame_need_update_ = true ;
        int mouse_x = (short)LOWORD(lParam);
        int mouse_y = (short)HIWORD(lParam);
        view_arcball_.OnMove(mouse_x, mouse_y) ;
    }

    // right button up, terminate view arc ball rotation
    if(uMsg == WM_RBUTTONUP)
    {
        frame_need_update_ = true ;
        view_arcball_.OnEnd();
        ReleaseCapture() ;
    }


    return TRUE ;
}

當鼠標右鍵按下時,設置frame_need_update_爲true,這個向量表示鼠標移動時是否有拖拽發生,由於Windows並無對應鼠標拖拽的消息,因此要經過兩個方面來判斷,一是鼠標按下了,二是鼠標移動了,同時知足這兩個條件才表示拖拽發生了。調用ArcBall.OnBegin函數,這個函數會判斷當前的鼠標位置是否位於窗口客戶區內,若是在客戶區外則不作相應。若是鼠標在窗口客戶區內,還要記錄當前鼠標的位置,並生成球體向量用於後續計算。

當鼠標移動時,調用ArcBall.OnMove(),這個函數首先求取鼠標當前位置,並生成球體向量,在根據上一次保存的球體向量計算出旋轉增量對應的四元組。

當鼠標右鍵擡起時,設置frame_need_update_爲false,結束旋轉。

void ArcBall::OnBegin(int mouse_x, int mouse_y)
{
    // enter drag state only if user click the window's client area
    if(mouse_x >= 0 && mouse_x <= window_width_ 
       && mouse_y >= 0 && mouse_y < window_height_)
    {
        is_dragged_ = true ; // begin drag state
        previous_quaternion_ = current_quaternion_ ;
        previous_point_ = ScreenToVector(mouse_x, mouse_y) ;
        old_point_ = previous_point_ ;
    }
}

void ArcBall::OnMove(int mouse_x, int mouse_y)
{
    if(is_dragged_)
    {
        current_point_ = ScreenToVector(mouse_x, mouse_y) ;
        rotation_increament_ = QuatFromBallPoints( old_point_, current_point_ ) ;
        current_quaternion_ = previous_quaternion_ * QuatFromBallPoints( previous_point_, current_point_ ) ;
        old_point_ = current_point_ ;
    }
}

void ArcBall::OnEnd()
{
    is_dragged_ = false ;
}

鼠標滾輪 - 縮放

縮放使用鼠標滾輪來完成,在WM_MOUSEWHEEL消息,HIWORD裏面存放的是鼠標滾輪的增量。獲取這個增量,並

// Mouse wheel, zoom in/out
if(uMsg == WM_MOUSEWHEEL) 
{
    frame_need_update_ = true ;
    mouse_wheel_delta_ += (short)HIWORD(wParam);
}

在Camera類的OnFrameMove中判斷是否有滾輪滾動,並作響應的處理,代碼以下。

if(mouse_wheel_delta_)
{
    radius_ -= mouse_wheel_delta_ * radius_ * 0.1f / 360.0f;

    // Make the radius in range of [min_radius_, max_radius_]
    // This can Prevent the cube became too big or too small
    radius_ = max(radius_, min_radius_) ;
    radius_ = min(radius_, max_radius_) ;
}

這個if語句會根據滾輪的增量計算radius_,並將radius_限制在範圍[min_radius_, max_radius_]內,防止模型過大或者太小。radius_變量稍後會用來計算眼睛到視點的距離,經過改變這個距離的值達到模型放大和縮小的效果,實際上模型並無真正被縮放,只是觀察的距離變了而已,這樣就會產生近大遠小的效果了。下面的代碼用來計算眼睛的位置。

// Update the eye point based on a radius away from the lookAt position
eye_point_ = lookat_point_ - world_ahead_vector * radius_;

Camera

Camera類是Arcball的使用者,裏面的OnFrameMove函數每一幀都會被調用,該函數負責縮放和旋轉,並生成新的View Matrix。

 1 void Camera::OnFrameMove()
 2 {
 3     // No need to handle if no drag since last frame move
 4     if(!m_bDragSinceLastUpdate)
 5         return ;
 6     m_bDragSinceLastUpdate = false ;
 7 
 8     if(m_nMouseWheelDelta)
 9     {
10         m_fRadius -= m_nMouseWheelDelta * m_fRadius * 0.1f / 120.0f;
11 
12         // Make the radius in range of [m_fMinRadius, m_fMaxRadius]
13         m_fRadius = max(m_fRadius, m_fMinRadius) ;
14         m_fRadius = min(m_fRadius, m_fMaxRadius) ;
15     }
16 
17     // The mouse delta is retrieved IN every WM_MOUSE message and do not accumulate, so clear it after one frame
18     m_nMouseWheelDelta = 0 ;
19 
20     // Get the inverse of the view Arcball's rotation matrix
21     D3DXMATRIX mCameraRot ;
22     D3DXMatrixInverse(&mCameraRot, NULL, m_ViewArcBall.GetRotationMatrix());
23 
24     // Transform vectors based on camera's rotation matrix
25     D3DXVECTOR3 vWorldUp;
26     D3DXVECTOR3 vLocalUp = D3DXVECTOR3(0, 1, 0);
27     D3DXVec3TransformCoord(&vWorldUp, &vLocalUp, &mCameraRot);
28 
29     D3DXVECTOR3 vWorldAhead;
30     D3DXVECTOR3 vLocalAhead = D3DXVECTOR3(0, 0, 1);
31     D3DXVec3TransformCoord(&vWorldAhead, &vLocalAhead, &mCameraRot);
32 
33     // Update the eye point based on a radius away from the lookAt position
34     m_vEyePt = m_vLookatPt - vWorldAhead * m_fRadius;
35 
36     // Update the view matrix
37     D3DXMatrixLookAtLH( &m_matView, &m_vEyePt, &m_vLookatPt, &vWorldUp );
38 }

代碼解釋:

第4行首先判斷是否有拖拽,若是沒有拖拽動做則沒必要更新視角,直接返回。

第6行將是否拖拽標誌設置爲false,由於能走到這一行表示有拖拽。

第8-15行處理鼠標滾輪動做,並確保camera的radius在控制範圍內,這樣魔方不至於過小或者太大。

第18行將滾輪的旋轉增量清0,由於增量不累加,每一個frame計算一次,下一個frame從新計算。

第21-22行求出旋轉矩陣的逆矩陣,由於若是要達到一樣的視角,模型和camera的旋轉方向恰好相反。能夠這樣理解,若是想看魔方的背面,咱們能夠將魔方旋轉180度,這至關於旋轉模型,也能夠固定魔方,走到魔方的背面去看,這就是旋轉camera了。

源碼

以前有幾個網友提出公佈源代碼,當時因爲代碼比較混亂,因此沒有公佈,我花了幾個星期的時間,將全部代碼從新整理了一遍,如今基本上能夠看了,可是還有不少細節須要打磨。昨晚上傳到了github上,歡迎fork,若是不熟悉github,也能夠在博客園本地下載。

編譯源代碼須要安裝DirectX SDK,推薦你們使用Microsoft DirectX SDK (June 2010),這是最新的SDK,固然也是最後一個。你們能夠本身編譯試着玩玩,若有問題,歡迎留言討論。

可執行程序

若是不想看代碼,能夠下載下面的可執行文件試玩,這個版本修復了以前幾位網友發現的幾個bug,仍是那句話,歡迎你們繼續找毛病。

RubikCube

To Be Continued

這個Demo剛剛上傳到github,還有不少功能須要完善,因爲我的精力有限,若是哪位網友有興趣,能夠和我一塊兒完成,那就太好了,期待你的加入!稍後將這個Demo升級,編寫DirectX10及DirectX11版本的RubikCube,也算是一個練手的過程吧,歡迎繼續關注!

相關文章
相關標籤/搜索