【引擎開發技術點記錄】RotationGizmo的純C++實現方式

初衷

寫這篇小文章,是發現網上對於C++實現的Gizmo實現的描述太少了,大多都是在講Unity Gizmo怎麼使用。而我最近在研究開發自己的圖形引擎,Gizmo的實現成爲了我的一個障礙。研究一下,發現Gizmo的實現方式很有意思,雖然不難,但是給人一種很巧妙的感覺。所以有這篇文章。

關於Gizmo

從事遊戲行業的同學只要用過商業引擎,那麼Gizmo是一定使用過的。大多是用來顯示錶示物體的translate、rotate、scale等操作。本文給出rotation的實現方式。Gizmo與遊戲Object之間的關係類似於觀察者模式,其中設計模式的技巧用的很到位。關於引擎Object Tree的實現又是長篇大述,這裏不再多說。

關於Rotation Gizmo的實現步驟

對於gizmo的實現,應用了一點3D數學的技巧。

  • 算法輸入:screen級別的點 from與 點to。這兩個點是點選Gizmo後拖動的起始點和終點。Gizmo利用這兩個點來完成相應的計算。
  • 算法步驟:
    (1)將點from與to從Screen Space轉移到World Space。這個過程是逆向的光柵化流程,不再贅述。得到兩個線段l1, l2。
    (2)計算l1,l2到對應平面的交點p1, p2。如果選擇了X軸向,那麼我們的平面是Plane{point(0,0,0), dir(1,0,0)}。學過線性代數的同學可以發現這是平面的代數形式。
    (3)計算p1,p2兩點到原點(0,0,0)的向量之間的theta角度。這個可以使用點乘之後除以兩者長度得到。(一個3D數學中經典的求角度公式)
    (4)判定p1,p2是否與相對應軸向的面的dir(對,就是上面那個dir)的叉乘是否小於零。用這一點來判斷你的旋轉方向是正向還是反向。
    (5)最後乘以(180 / pi)從而完成角度轉換。

此時,我們完成了從drag動作到角度的一系列轉換。
下面給出代碼實現,基於QT的實現方案,看起來通俗易懂:


void RotateGizmo::drag(QPoint from, QPoint to, int scnWidth, int scnHeight, QMatrix4x4 proj, QMatrix4x4 view) {
    if (m_host == 0) return;
    Line l1 = screenPosToWorldRay(QVector2D(from), QVector2D(scnWidth, scnHeight), proj, view);
    Line l2 = screenPosToWorldRay(QVector2D(to), QVector2D(scnWidth, scnHeight), proj, view);
    QMatrix4x4 invModelMat = globalSpaceMatrix().inverted();
    l1 = invModelMat * l1;
    l2 = invModelMat * l2;
    if (m_axis == X) {
        QVector3D p1 = getIntersectionOfLinePlane(l1, { QVector3D(0, 0, 0), QVector3D(1, 0, 0) });
        QVector3D p2 = getIntersectionOfLinePlane(l2, { QVector3D(0, 0, 0), QVector3D(1, 0, 0) });
        float theta = qAcos(qMin(qMax(QVector3D::dotProduct(p1, p2) / p1.length() / p2.length(), -1.0f), 1.0f));
        if (QVector3D::dotProduct(QVector3D(1, 0, 0), QVector3D::crossProduct(p1, p2)) < 0)
            theta = -theta;
        rotate(theta * QVector3D(180.0f / 3.1415926f, 0.0f, 0.0f));
    } else if (m_axis == Y) {
        QVector3D p1 = getIntersectionOfLinePlane(l1, { QVector3D(0, 0, 0), QVector3D(0, 1, 0) });
        QVector3D p2 = getIntersectionOfLinePlane(l2, { QVector3D(0, 0, 0), QVector3D(0, 1, 0) });
        float theta = qAcos(qMin(qMax(QVector3D::dotProduct(p1, p2) / p1.length() / p2.length(), -1.0f), 1.0f));
        if (QVector3D::dotProduct(QVector3D(0, 1, 0), QVector3D::crossProduct(p1, p2)) < 0)
            theta = -theta;
        rotate(theta * QVector3D(0.0f, 180.0f / 3.1415926f, 0.0f));
    } else if (m_axis == Z) {
        QVector3D p1 = getIntersectionOfLinePlane(l1, { QVector3D(0, 0, 0), QVector3D(0, 0, 1) });
        QVector3D p2 = getIntersectionOfLinePlane(l2, { QVector3D(0, 0, 0), QVector3D(0, 0, 1) });
        float theta = qAcos(qMin(qMax(QVector3D::dotProduct(p1, p2) / p1.length() / p2.length(), -1.0f), 1.0f));
        if (QVector3D::dotProduct(QVector3D(0, 0, 1), QVector3D::crossProduct(p1, p2)) < 0)
            theta = -theta;
        rotate(theta * QVector3D(0.0f, 0.0f, 180.0f / 3.1415926f));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章