初學Opengl,看了learnopengl和以此為教材的視頻的入門教程,對繞點旋轉相機有疑問

view矩陣的獲取需要三個參數:相機位置,目標位置,相機Up向量

初始化是傳入:相機位置,目標位置,世界Up向量

通過:目標位置-相機位置得到Forward指向向量

通過Forward X 世界Up得到Right向量

通過Right X Forward 得到相機Up向量

我的繞點旋轉,是通過 相機位置 - 目標,將坐標變換到以目標為原點,減出的向量看做是:以目標為原點,以Forward的模長為半徑的圓上的一點,我直接將這個點乘旋轉矩陣,就相當於對相機位置做旋轉變換,再將結果加回目標位置,就是世界坐標下相機的位置

綁定滑鼠事件,當橫向移動滑鼠,相機繞點右手坐標系Y軸旋轉,縱向移動時,繞點X軸旋轉

實驗結果:

1.單橫向(Y軸旋轉)移動(不乘縱向旋轉矩陣)沒有問題

2.單縱向移動,平時看不出問題,但會在-90°和+90°發生roll角跳變180°

此時如果繞Forward旋轉(既roll角)旋轉180°,視角就會恢復正常

3.如果橫向縱向矩陣同時乘,滑鼠單單縱向移動,在正負45°時幾乎是正常的,但在±90°時會發生roll角偏移,差不多是第二種情況的漸變樣子,roll角會在極少的角度區間內旋轉:

後來通過寫±90°瞬間變換roll角,解決了2的問題,但3的問題還有,而且更加嚴重了,我感覺這不像是正確的寫法

我個人猜測,Right是Forward X WorldUp向量,在±90°時,根據右手判斷叉乘向量方向,Right會瞬間切換方向,但是LookAt矩陣也沒要傳入Right

求出現問題的解釋,或繞點旋轉相機的常用寫法


謝邀。

你的理解正確。LookAt計算雖然沒有要求傳入Right向量,但是要求傳入Up,且你的Up是根據Right計算出的,所以在過上下頂點的時候,發生了翻轉。

事實上,當在上下頂點的時候,你現在的這個計算流程,Forward X 世界Up會是零,因為它們平行了。你好好想想。所以,在這兩個點上,是不能這麼算的。

然後,過了這兩個點之後,相機的上方向會發生翻轉。右方向也會發生翻轉。所以這個時候再用世界的Up來計算Right就是錯誤的了。應該用世界的Down。如果再過一次上下頂點,則又應該恢復到世界的Up。

事實上,並不是需要用世界的Up,而是用一個大致上能代表相機Up方向的向量就好了。你現在用了世界Up,所以相機很努力地保持頭朝上,因此過了上下頂點之後突然180度翻轉,這恰恰說明瞭演算法的正確性。

至於第二個問題(垂直水平同時旋轉),這是一個基本問題。旋轉問題是不滿足交換律的。將一個方塊(各個面顏色不同)朝右邊轉90度再朝上轉90度,與先朝上轉90度再向右轉90度,結果是不同的。

所以,通過乘兩次矩陣進行旋轉是不行的。你需要根據最終位置直接算出一個旋轉矩陣。也就是要求出實際的(斜的)旋轉軸,寫出繞那個軸旋轉的矩陣。

總之,這些問題都與OpenGL沒有直接關係。暴露出的還是數學底子的問題。建議去補線性代數相關的內容。


我覺得問題在於:滑鼠移動只有2個自由度(X和Y),但是相機的方位有三個自由度(方位角、仰角、翻滾角)。那第三個自由度怎麼確定呢?按樓主的方法,設定一個Up向量,大概是為了希望頭永遠朝向上方吧(其實是朝向北極星,但那樣人如果已經走到北極,那經過北極的時候就會突然跳變)

(這段一開始語氣不太好所以改了一下)

一種不會跳變的方法可以這樣。

  • (1)如果滑鼠左右移動了,就先把攝像機繞著它的局部的Y軸旋轉一下,然後再重新計算位置(就是從那點出發,後退攝像機與點之間原先的距離那麼長的距離)
  • (2)如果滑鼠上下移動了,就先把攝像機繞著它的局部X軸旋轉一下,然後再重新計算位置。
  • (3)如果按著右鍵左右移動,就繞著局部Z軸旋轉一下,然後重新計算位置。

想像一輛車在一個球的表面開,它能左平移和右平移對應上面的(1)、前進和後退對應上面的(2)、左轉和右轉對應上面的(3)。那麼坐在車裡向地下看就是以下效果。

只改變了兩個自由度的繞點旋轉

只確定了兩個自由度的繞點旋轉

class Camera {
public:
Camera();
glm::mat4 GetViewMatrix();
void Update(float);
glm::vec3 pos, lookdir, up, vel; // in world coordinates
void RotateAlongPoint(glm::vec3 p, glm::vec3 local_axis, float rad);
private:
void do_RotateInLocalCoords(glm::vec3 local_axis, float rad);
};

// 方位繞局部坐標系的axis順時針轉rad弧度
void Camera::do_RotateInLocalCoords(glm::vec3 local_axis, float rad) {
glm::vec3 world_x = -glm::cross(up, lookdir),
world_z = -lookdir,
world_y = up;
glm::mat3 basis(world_x, world_y, world_z);

glm::mat3 rot = glm::mat3(glm::rotate(rad, local_axis));
basis = basis * rot;

lookdir = -basis[2];
up = basis[1];
}

// 攝像機的姿態繞著自己局部坐標系的axis軸旋轉rad弧度,同時保持指向某個點
void Camera::RotateAlongPoint(glm::vec3 p, glm::vec3 local_axis, float rad) {
float dist = sqrt(glm::dot(p - pos, p - pos));
do_RotateInLocalCoords(glm::vec3(local_axis), rad);
pos = p - (lookdir * dist);
}

(一開始沒看樓主的回復,不知道樓主已經清楚了物體的旋轉一事,所以多出了以下這些囉嗦的一段……)

另外我覺得表示物體的旋轉也可以用3x3矩陣,雖然冗餘度高,但是容易理解。不過樓主可能已經知道了:==============以下是一些樓主可能已經會了的內容=================

如果拿GLM數學庫來看,似乎沒有直接「繞著某點的X軸旋轉」的方法,最基本的操作卻是有「某物體繞著它的局部坐標系的某個軸旋轉」。

而且並不一定要用到四元數,樓主可以將物體的旋轉方位(orientation)用一個3x3矩陣來表示。

這樣的好處是易於理解,不好之處是佔用空間多,而且可能會隨著計算的進行丟失精度而需要重新標準化。說易於理解是因為:這個3x3的矩陣M的第一列是「該物體的局部X軸在世界坐標系中的表示」,第二列是「物體的局部Y軸在世界坐標系中的表示」,第三列是「物要的局部Z軸在世界坐標系中的表示」。所以這是一個正交矩陣,它的三列是三個互相垂直的單位向量,亦即這裡的X、Y、Z軸。如果想把物體的局部坐標系中的向量變換到世界坐標系中,就乘以這個矩陣。如果想把世界坐標系中的向量變換到局部坐標系中,就乘在這個矩陣的轉置。

所以,一個物體的屬性,就有它的位置(position)和它的方位(orientation)。

class Sprite {
public:
glm::vec3 pos, vel;
glm::mat3 orientation;
virtual void Render() = 0;
};

在畫一個物體的時候,是這樣的:

(其中的chunk是一個ChunkGrid,儲存了32x32x32的體素的一個VAO與VBO等數據的集合,由一個draw call負責完成)

void Sprite::Render() {
chunk-&>Render(pos, scale, orientation, anchor);
}

void ChunkGrid::Render(
const glm::vec3 pos, const glm::vec3 scale,
const glm::mat3 orientation, const glm::vec3 anchor) {

// GLM的操作,包括平移、旋轉,都是在矩陣M的局部坐標系中完成的!
glm::mat4 M(orientation); // 構建Model Matrix,以orientation為基礎
M = glm::scale(M, scale); // 先在原地進行放大
M = glm::translate(M, glm::inverse(orientation) * pos / scale); // 要移到世界坐標系中的pos,就先將pos移到局部坐標系中,再除以縮放係數
M = glm::translate(M, -anchor); // 錨點原本就是在局部坐標中的,所以就直接減去就行了

this-&>Render(M);
}

頂點著色器裡面,對M的使用是這樣的。

(其中的View Matrix是取決於攝像機的位置的。)

gl_Position = P * V * M * vec4(position, 1.0f);

那麼現在是如何實現樓主所需求的「水晶球視角」,就是攝像機遠離物體一定距離,而物體原地旋轉的效果:只要改變這個物體的orientation就可以了:

void ChunkSprite::RotateAroundGlobalAxis(const glm::vec3 axis, const float deg) {
glm::mat4 o4(orientation);
o4 = glm::rotate(o4, deg*3.14159f/180.0f, glm::inverse(orientation)*axis); //繞著全局坐標系的axis轉,就是繞著局部坐標系的(orientation的逆矩陣(對於正交矩陣,逆就是轉置))乘以axis轉
orientation = glm::mat3(o4);
}

void ChunkSprite::RotateAroundLocalAxis(const glm::vec3 axis, const float deg) {
glm::mat4 o4(orientation);
o4 = glm::rotate(o4, deg*3.14159f/180.0f, axis); // 繞著局部坐標系的axis轉
orientation = glm::mat3(o4);
}

這樣,就可以達到以下的效果。

其中比較長的那個是全局坐標系,中間小的那個是一個物體的局部坐標系。攝像機是居高臨下的視角,從高於地面(y&>0)的某處看向地面的:繞局部坐標系和全局坐標系旋轉

對於樓主的需求,可以這樣:

1)令攝像機面朝Z軸負方向,攝像機的Up設為Y軸正方向

2)把物體的位置置於屏幕裡面的某點,譬如(0, 0, -10)3)把滑鼠移動與物體的旋轉映射起來: ?滑鼠左移=物體繞全局Y軸順時針轉 ?滑鼠右移=物體繞全局Y軸逆時針轉 ?滑鼠上移=物體繞全局X軸逆時針轉 ?滑鼠下移=物體繞全局X軸順時針轉

希望能有些用的說!

(‐^▽^‐)

用四元數


看描述是典型的歐拉角的死鎖問題,建議瞭解一下四元數的解決方案


謝邀!

隨便說幾句,不知會不會跑題,沒把你的問題說清楚。

1.剛開始學習圖形學,用OpenGL寫程序,往往用旋轉平移這些概念。要逐步轉到矩陣演算的概念上去理解,根據矩陣演算構建程序,拋棄旋轉平移等概念。旋轉平移等概念僅僅在用戶界面出現,程序內部都是矩陣演算。旋轉平移等概念,往往需要計算sin()和cos(),以及他們的反函數,在某些特殊值(例如90度,180度,270度等),往往會有跳躍,往往超出函數定義域。你的那些跳躍,可能就和這些有關。

2.要考慮清楚如何構建場景樹。討論任何坐標變換問題,都是在某個場景樹的節點,也就是某個坐標系中去討論的。一定選一個問題最簡單的坐標系去討論問題。沒有清晰的場景樹,沒有明確的坐標變換關係,會把簡單問題搞的非常複雜。

3.旋轉問題,都是圍繞某個旋轉軸的。討論旋轉,首先要明確旋轉軸。你的繞點旋轉,沒有旋轉軸,是怎麼定義的?

4.旋轉問題,一般是這樣做的。確定旋轉軸。根據旋轉軸,確定討論旋轉問題的坐標系。在該坐標系中,圍繞著某個坐標軸旋轉,這時問題就很簡單了。


這個世界上所有的旋轉,都是繞著軸的旋轉,近似代數上大概是這樣定義旋轉的:

1.給你一根旋轉軸

2.把轉軸正交補上的東西,繞著轉動,轉動一個角度

所以所謂的繞著點轉動就是邪教。

所以,你要繞點轉動,實際上是繞著通過這個點的某一根瞬時轉軸轉動。

我沒太明白你說的世界up是什麼意思,猜想一下應該是相機坐標系和世界坐標系沒有相對旋轉時,相機坐標系的up方向。而你需要對相機進行旋轉,使得相機坐標系的up方向為你所定義的方向,如果是這個問題,那麼求解思路如下:

1. 算轉軸,正所謂兩個相交直線確定一個平面,那麼實際上你已知的是轉軸的正交補(也就是這個平面),需要得到轉軸,也就是平面的單位法向量,只需要兩個向量叉積之後單位化即可。

2. 算轉角,顯然只需要兩個相機內積除以模可得轉角餘弦值。

最後你的旋轉用四元數表示還是用矩陣表示,甚至歐拉角表示,只是你對旋轉的表示形式而已

比如說用矩陣表示,Rodrigues方程瞭解一下


關鍵字track ball,假想滑鼠在一個虛擬的球面,根據滑鼠移動求出旋轉變換,用四元數可以簡化計算。


歐拉角攝像機是有侷限的,也即你描述的情況。 四元數攝像機可以解決這個問題,包括萬向節死鎖。


推薦閱讀:
相關文章