為了實現一個全景圖片展示的功能,需要藉助手機的姿態感測器,實現一個這樣的功能:當手機旋轉時,視角也跟著旋轉(讀者若理解不能,可以參考下現在流行的 VR 應用,使用陀螺儀模式時的效果,亦可稱作“單目 VR 效果”)。這個功能的實現原理為:利用手機感測器得到手機的當前的姿態的信息(可以是用各種方式來描述 ...
為了實現一個全景圖片展示的功能,需要藉助手機的姿態感測器,實現一個這樣的功能:當手機旋轉時,視角也跟著旋轉(讀者若理解不能,可以參考下現在流行的 VR 應用,使用陀螺儀模式時的效果,亦可稱作“單目 VR 效果”)。這個功能的實現原理為:利用手機感測器得到手機的當前的姿態的信息(可以是用各種方式來描述的),然後調整投影的參數,實現最終的圖像跟著手機旋轉的效果。
手機姿態獲取
Android
Android 平臺有各種各樣的感測器(Sensor),不止一個 sensor 可以實現前文提到的目的。在 Android SDK API Guides 中可以找到所有可用的 sensors。
1 gyroscope
這個就是陀螺儀了,是硬體感測器(hardware sensor),可以提供設備繞 xyz 三個軸旋轉的角速度。註意這裡的坐標系就是設備自身的,短邊 x 軸,長邊 y 軸,z 軸穿越手機屏幕。為實現旋轉效果需要的數據是旋轉的角度(用以計算旋轉矩陣),所以如果要用 gyroscope 則許需要對數據進行積分,最好加上濾波使效果平穩。
2 orientation
這是一個軟體感測器(software sensor),返回的數據不是感測器硬體直接採集得到的,而是根據硬體的數據進行合適的計算得到的。
閱讀文檔即可得知,此用於獲取設備在 Yaw,Pitch and Roll 三個方向的旋轉的角度,根據這三個角度也就可以確定其姿態了。但是官方文檔不推薦這樣使用,而是推薦 rotation vector sensor 並配合 [getRotationMatrix()](https://developer.android.google.cn/reference/android/hardware/SensorManager.html#getRotationMatrix(float[],%20float[],%20float[],%20float[]) 來計算出這三個參數,至於原因,官方文檔只說現在留著這個感測器類型是歷史原因,讓大家不要用了,沒解釋其他的。具體原因後面解釋。
3 rotation vector
這個 sensor 和上面的 orientation 一樣,都是軟體感測器。這個表示的仍然是設備的姿態,但是與上面的不同,這個用一個四元數來表示設備的旋轉,旋轉的參考坐標系是真實的物理世界。
在應用該感測器提供的數據時,值得註意的是,有可能感測器返回的數據不一定是四個數據,也有可能是三個參數,比較安全的做法是這樣做:
@Override
public void onSensorChanged(SensorEvent event) {
float[] quat = new float[4];
SensorManager.getQuaternionFromVector(quat, event.values);
// use the quaternion
// ...
}
比較
那麼這兩個感測器似乎都能用,到底用哪個呢?顯而易見,第一個用起來不太方便。那麼後兩者區別在哪?
實際上,官方就不推薦使用 orientation,已經將其標記為 deprecated,並且提供了另一個方法叫做 getOrientation(),用法較複雜一點,從加速度計和地磁感測器獲取數據,根據此計算得旋轉矩陣,然後在根據旋轉矩陣計算三個方向的旋轉角度。個人感覺就是放棄了幫助開發者去計算這個數據,而是告訴大家應該怎麼樣算,自己去算吧。為什麼要廢棄呢,根據這裡的解釋,舊版感測器的問題主要是萬向節鎖,當某一個軸轉動 90 度,就很難準確描述另外兩個軸的轉動了,對萬向節鎖的理解可以看我之前的文章:萬向節鎖(Gimbal Lock)的理解。相比較,rotation vector 用四元數來描述旋轉,避免了這個問題,所以我採用這個感測器來採集設備姿態的信息。
iOS
iOS 平臺稍微簡單一些,沒有那麼多具體的感測器。deviceMotion 就很好用,獲取 deviceMotion.attitude 這是一個CMAttitude對象,可以直接獲取用四元數表示的設備的姿態信息。
其實 CoreMotion 這個 Framework 也是可以提供裸陀螺儀數據等等,但是 CMDeviceMotion 下麵的 attitude 這個屬性返回的數據是更加準確的,如果需要精確的數據,推薦使用這個方法。
實現旋轉
這裡我先簡單闡述一下一個旋轉的意義:當我們看著一張圖片的時候,如果要實現旋轉這張圖的效果,有兩個辦法:
- 將圖片轉一下(調整物體的模型的參數)
- 我的腦袋轉一下(調整攝像機的參數,也就是改變觀察矩陣(即 look at matrix)的參數)
實際上這兩種方法都可以實現,實際應用的時候隨便選一個更合適自己的就可以了。我這裡要說明的是如果採用後一個辦法,不能簡單地將兩個矩陣相乘就算數了,我們要從實際情況去考慮這個問題,考慮 OpenGL 的觀察矩陣的計算:
// GLU
gluLookAt(x0, y0, z0, xref, yref, zref, Vx, Vy, Vz)
// glm
GLM_FUNC_DECL tmat4x4<T, P> glm::lookAt ( tvec3< T, P > const & eye,
tvec3< T, P > const & center,
tvec3< T, P > const & up
)
兩種是一樣的,分別指定觀察坐標系的原點(eye),參考點(即視點,相機瞄準的點,center),和向上向量(up)。當旋轉相機時,需要將 center 和 up 同時應用這個相應的旋轉。我之前犯過的一個錯誤是:我僅僅把旋轉應用到了 center 上面,而沒有應用到 up vector,up vector 一直設置的是(0, 1, 0)
。直觀點說就是:視點轉了,但是頭頂朝向沒有變化,這個是不符合邏輯的,我們需要讓視線N:P_0 - P_ref
和 up vector 保持垂直。
問題解決
在實際實踐中,Android 的坑還真是特別的多,我發現使用 rotation vector 計算出旋轉矩陣,會根據手機當前和真實世界的關係產生一定偏移,也就是說手機朝向北方和朝向南方得到的數據是不一樣的,而我希望的是得到一個與手機初始的朝向無關(也就是手機繞真實世界的南北極組成的軸旋轉的角度),而與手機與地平線的夾角有關的這麼一個數據。總的來說就是 rotation vector 做了一些我不想要的校正。同時我找到另一個 sensor:GAME_ROTATION_VECTOR,其介紹如下:
Identical to
TYPE_ROTATION_VECTOR
except that it doesn't use the geomagnetic field. Therefore the Y axis doesn't point north, but instead to some other reference, that reference is allowed to drift by the same order of magnitude as the gyroscope drift around the Z axis.
In the ideal case, a phone rotated and returning to the same real-world orientation will report the same game rotation vector (without using the earth's geomagnetic field). However, the orientation may drift somewhat over time. SeeTYPE_ROTATION_VECTOR
for a detailed description of the values. This sensor will not have the estimated heading accuracy value.
使用這個我基本上實現我想要的效果,所以以後要用這個 Rotation vector 的時候,要考慮清楚到底要用哪一個。另外還有一個 Geomagnetic Rotation Vector 實際使用效果較差,精度較低,但是省電。