OpenGL 四元數旋轉

原文鏈接:OpenGL_3_3_Tutorial_Translation

第十七課:旋轉

[TOC]

Tags: OpenGL 教程

雖然本課有些超出OpenGL的範圍,但是解決了一個常見問題:怎樣表示旋轉?

《第三課:矩陣》中,我們瞭解到矩陣可以讓點繞某個軸旋轉。矩陣可以簡潔地表示頂點的變換,但使用難度較大:例如,從最終結果中獲取旋轉軸就很麻煩。

本課將展示兩種最常見的表示旋轉的方法:歐拉角(Euler angles)和四元數(Quaternion)。最重要的是,本課將詳細解釋爲何要儘量使用四元數。

前言:旋轉與朝向(orientation)

閱讀有關旋轉的文獻時,你可能會爲其中的術語感到困惑。本課中:

  • “朝向”是狀態:該物體的朝向爲……
  • “旋轉”是操作:旋轉該物體

也就是說,當實施旋轉操作時,就改變了物體的朝向。 兩者形式相同,因此容易混淆。閒話少敘,開始進入正題……

歐拉角

歐拉角是表示朝向的最簡方法,只需存儲繞X、Y、Z軸旋轉的角度,非常容易理解。你可以用vec3來存儲一個歐拉角:

vec3 EulerAngles( RotationAroundXInRadians, RotationAroundYInRadians, RotationAroundZInRadians);

這三個旋轉是依次施加的,通常的順序是:Y-Z-X(但並非一定要按照這種順序)。順序不同,產生的結果也不同。

一個歐拉角的簡單應用就是用於設置角色的朝向。通常,遊戲角色不會繞X和Z軸旋轉,僅僅繞豎直的Y軸旋轉。因此,無需處理三個朝向,只需用一個float型變量表示方向即可。

另外一個使用歐拉角的例子是FPS相機:用一個角度表示頭部朝向(繞Y軸),一個角度表示俯仰(繞X軸)。參見common/controls.cpp的示例。

不過,面對更加複雜的情況時,歐拉角就顯得力不從心了。例如:

  • 對兩個朝向進行插值比較困難。簡單地對X、Y、Z角度進行插值得到的結果不太理想。
  • 實施多次旋轉很複雜且不精確:必須計算出最終的旋轉矩陣,然後據此推測書歐拉角。
  • “臭名昭著”的“萬向節死鎖”(Gimbal Lock)問題有時會讓旋轉“卡死”。其他一些奇異狀態還會導致模型方向翻轉。
  • 不同的角度可產生同樣的旋轉(例如-180°和180°)
  • 容易出錯——如上所述,一般的旋轉順序是YZX,如果用了非YZX順序的庫,就有麻煩了。
  • 某些操作很複雜:如繞指定的軸旋轉N角度。

四元數是表示旋轉的好工具,可解決上述問題。

四元數

四元數由4個數[x y z w]構成,表示瞭如下的旋轉:

// RotationAngle is in radians
x = RotationAxis.x * sin(RotationAngle / 2)
y = RotationAxis.y * sin(RotationAngle / 2)
z = RotationAxis.z * sin(RotationAngle / 2)
w = cos(RotationAngle / 2)

RotationAxis,顧名思義即旋轉軸。RotationAngle是旋轉的角度。

因此,四元數實際上存儲了一個旋轉軸和一個旋轉角度。這讓旋轉的組合變簡單了。

###解讀四元數###

四元數的形式當然不如歐拉角直觀,不過還是能看懂的:xyz分量大致代表了各個軸上的旋轉分量,而w=acos(旋轉角度/2)。舉個例子,假設你在調試器中看到了這樣的值[ 0.7 0 0 0.7 ]。x=0.7,比y、z的大,因此主要是在繞X軸旋轉;而2*acos(0.7) = 1.59弧度,所以旋轉角度應該是90°。

同理,[0 0 0 1] (w=1)表示旋轉角度 = 2acos(1) = 0,因此這是一個單位四元數*(unit quaternion),表示沒有旋轉。

###基本操作###

不必理解四元數的數學原理:這種表示方式太晦澀了,因此我們一般通過一些工具函數進行計算。如果對這些數學原理感興趣,可以參考實用工具和鏈接中的數學書籍。

####怎樣用C++創建四元數?####

// Don't forget to #include <glm/gtc/quaternion.hpp> and <glm/gtx/quaternion.hpp>

// Creates an identity quaternion (no rotation)
quat MyQuaternion;

// Direct specification of the 4 components
// You almost never use this directly
MyQuaternion = quat(w,x,y,z);

// Conversion from Euler angles (in radians) to Quaternion
vec3 EulerAngles(90, 45, 0);
MyQuaternion = quat(EulerAngles);

// Conversion from axis-angle
// In GLM the angle must be in degrees here, so convert it.
MyQuaternion = gtx::quaternion::angleAxis(degrees(RotationAngle), RotationAxis);

####怎樣用GLSL創建四元數?####

不要在shader中創建四元數。應該把四元數轉換爲旋轉矩陣,用於模型矩陣中。頂點會一如既往地隨着MVP矩陣的變化而旋轉。

某些情況下,你可能確實需要在shader中使用四元數。例如,GPU骨骼動畫。GLSL中沒有四元數類型,但是可以將四元數存在vec4變量中,然後在shader中計算。

####怎樣把四元數轉換爲矩陣?####

mat4 RotationMatrix = quaternion::toMat4(quaternion);

這下可以像往常一樣建立模型矩陣了:

mat4 RotationMatrix = quaternion::toMat4(quaternion);
...
mat4 ModelMatrix = TranslationMatrix * RotationMatrix * ScaleMatrix;
// You can now use ModelMatrix to build the MVP matrix

那究竟該用哪一個呢?

在歐拉角和四元數之間作選擇還真不容易。歐拉角對於美工來說顯得很直觀,因此如果要做一款3D編輯器,請選用歐拉角。但對程序員來說,四元數卻是最方便的。所以在寫3D引擎內核時應該選用四元數。

一個普遍的共識是:在程序內部使用四元數,在需要和用戶交互的地方就用歐拉角。

這樣,在處理各種問題時,你才能得心應手(至少會輕鬆一點)。如果確有必要(如上文所述的FPS相機,設置角色朝向等情況),不妨就用歐拉角,附加一些轉換工作。

其他資源

  1. 實用工具和鏈接中的書籍

  2. 老是老了點,《遊戲編程精粹1》(Game Programming Gems I)有幾篇關於四元數的好文章。也許網絡上就有這份資料。

  3. 一個關於旋轉的[GDC報告]http://www.essentialmath.com/GDC2012/GDC2012_JMV_Rotations.pdf

  4. The Game Programing Wiki Quaternion tutorial

  5. Ogre3D FAQ on quaternions。 第二部分大多是針對OGRE的。

  6. Ogre3D Vector3D.hQuaternion.cpp

速查手冊

###怎樣判斷兩個四元數是否相同?###

向量點積是兩向量夾角的餘弦值。若該值爲1,那麼這兩個向量同向。判斷兩個四元數是否相同的方法與之十分相似:

float matching = quaternion::dot(q1, q2);
if ( abs(matching-1.0) < 0.001 ){
// q1 and q2 are similar
}

由點積的acos值還可以得到q1和q2間的夾角。

###怎樣旋轉一個點?###

方法如下:

rotated_point = orientation_quaternion *  point;

……但如果想計算模型矩陣,你得先將其轉換爲矩陣。注意,旋轉的中心始終是原點。如果想繞別的點旋轉:

rotated_point = origin + (orientation_quaternion * (point-origin));

###怎樣對兩個四元數插值?###

SLERP意爲球面線性插值(Spherical Linear intERPolation)、可以用GLM中的mix函數進行SLERP:

glm::quat interpolatedquat = quaternion::mix(quat1, quat2, 0.5f); // or whatever factor

###怎樣累積兩個旋轉?###

只需將兩個四元數相乘即可。順序和矩陣乘法一致。亦即逆序相乘:

quat combined_rotation = second_rotation * first_rotation;

###怎樣計算兩向量之間的旋轉?###

(也就是說,四元數得把v1旋轉到v2)

基本思路很簡單:

  • 兩向量間的夾角很好找:由點積可知其cos值。
  • 旋轉軸很好找:兩向量的叉乘積。

如下的算法就是依照上述思路實現的,此外還處理了一些特例:

quat RotationBetweenVectors(vec3 start, vec3 dest){
start = normalize(start);
dest = normalize(dest);

float cosTheta = dot(start, dest);
vec3 rotationAxis;

if (cosTheta < -1 + 0.001f){
// special case when vectors in opposite directions:
// there is no "ideal" rotation axis
// So guess one; any will do as long as it's perpendicular to start
rotationAxis = cross(vec3(0.0f, 0.0f, 1.0f), start);
if (gtx::norm::length2(rotationAxis) < 0.01 ) // bad luck, they were parallel, try again!
rotationAxis = cross(vec3(1.0f, 0.0f, 0.0f), start);

rotationAxis = normalize(rotationAxis);
return gtx::quaternion::angleAxis(180.0f, rotationAxis);
}

rotationAxis = cross(start, dest);

float s = sqrt( (1+cosTheta)*2 );
float invs = 1 / s;

return quat(
s * 0.5f,
rotationAxis.x * invs,
rotationAxis.y * invs,
rotationAxis.z * invs
);

}

(可在common/quaternion_utils.cpp中找到該函數)

###我需要一個類似gluLookAt的函數。怎樣旋轉物體使之朝向某點?###

調用RotationBetweenVectors函數!

// Find the rotation between the front of the object (that we assume towards +Z,
// but this depends on your model) and the desired direction
quat rot1 = RotationBetweenVectors(vec3(0.0f, 0.0f, 1.0f), direction);

現在,你也許想讓物體保持豎直:

// Recompute desiredUp so that it's perpendicular to the direction
// You can skip that part if you really want to force desiredUp
vec3 right = cross(direction, desiredUp);
desiredUp = cross(right, direction);

// Because of the 1rst rotation, the up is probably completely screwed up.
// Find the rotation between the "up" of the rotated object, and the desired up
vec3 newUp = rot1 * vec3(0.0f, 1.0f, 0.0f);
quat rot2 = RotationBetweenVectors(newUp, desiredUp);

組合到一起:

quat targetOrientation = rot2 * rot1; // remember, in reverse order.

注意,“direction”僅僅是方向,並非目標位置!你可以輕鬆計算出方向:targetPos – currentPos

得到目標朝向後,你很可能想對startOrientationtargetOrientation進行插值

(可在common/quaternion_utils.cpp中找到此函數。)

###怎樣使用LookAt且限制旋轉速度?###

基本思想是採用SLERP(用glm::mix函數),但要控制插值的幅度,避免角度偏大。

float mixFactor = maxAllowedAngle / angleBetweenQuaternions;
quat result = glm::gtc::quaternion::mix(q1, q2, mixFactor);

如下是更爲複雜的實現。該實現處理了許多特例。注意,出於優化的目的,代碼中並未使用mix函數。

quat RotateTowards(quat q1, quat q2, float maxAngle){

if( maxAngle < 0.001f ){
// No rotation allowed. Prevent dividing by 0 later.
return q1;
}

float cosTheta = dot(q1, q2);

// q1 and q2 are already equal.
// Force q2 just to be sure
if(cosTheta > 0.9999f){
return q2;
}

// Avoid taking the long path around the sphere
if (cosTheta < 0){
q1 = q1*-1.0f;
cosTheta *= -1.0f;
}

float angle = acos(cosTheta);

// If there is only a 2° difference, and we are allowed 5°,
// then we arrived.
if (angle < maxAngle){
return q2;
}

float fT = maxAngle / angle;
angle = maxAngle;

quat res = (sin((1.0f - fT) * angle) * q1 + sin(fT * angle) * q2) / sin(angle);
res = normalize(res);
return res;

}

可以這樣用RotateTowards函數:

CurrentOrientation = RotateTowards(CurrentOrientation, TargetOrientation, 3.14f * deltaTime );

(可在common/quaternion_utils.cpp中找到此函數)

###怎樣……###

若有疑問,請通過e-mail聯繫我們。我們將把您的問題添加到此文中。

© http://www.opengl-tutorial.org/

Written with Cmd Markdown.

發佈了8 篇原創文章 · 獲贊 14 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章