在上一篇文章中,我討論了一個解決方案,即通過使用單個緩沖帶渲染減少VR中的延遲。我提到,VR對圖像質量的要求相當高,因為GPU相比傳統的移動應用程序需要做更多的工作。
工作負載增加的其中一個原因是對象內容需要進行兩次渲染(每單位矩陣渲染一次)。另一個原因是GPU最后一次渲染使用了桶形失真過濾(每單位矩陣再渲染一次)。這使得圖像處理器的工作負荷特別大,因此我們需要思考良策以降低負荷。
為何使用透鏡?
您可能會有疑惑:當今的移動設備功能已十分強大,為什么我們還需要這些學校里習得的老舊的光學技巧?
VR耳機使用透鏡有兩大主要原因:
1. 透鏡可以拓寬視野
2. 透鏡可以使屏幕與你的眼睛離得更近
例如,如果您的手機屏幕是11.5cm x 6.5cm,那么每只眼睛水平位置可視的屏幕寬度為5.75cm。由于屏幕與人臉非常靠近,因此相比人眼實際可視的寬度,這個寬度還遠遠不夠。在兩個屏幕之間放置一塊透鏡,那么不管是水平視角還是垂直視角,可視范圍均會增加。
使用透鏡,我們可以使虛擬世界比實際世界看起來大很多。
其次,有了透鏡,即使用戶的眼睛與屏幕非常接近,但其看到的屏幕效果比實際上的卻遠很多,這樣使得用戶體驗更為輕松。
顯然,也存在一些缺陷。由于失真,圖像的某些部分將被壓縮,從而將失去一些信息;而剩下的部分則意味著要用更高的分辨率進行填充。同時,透鏡導致的一些色差也需要被修正。
桶形失真
由于存在桶形失真,我們需要應用反向轉換,這樣屏幕發出的光線才比較適宜。而這種轉換的完成主要取決于物理透鏡的設計。
在VR中,透鏡設計相當復雜。Oculus Rift CV1是一款做的非常好的VR案例。顯然,所有這些設計的決策都會影響到修正圖像所需的算法。
桶形失真最常見的方法是使用。當然,還有很多其他可用的失真模型——如使用多項式函數或曲線。一個常見的問題是創建的是非零逆函數(non-trival)。為簡單起見,我們將使用以下模型用于桶形失真,其逆函數可以進行計算:
α定義了我們想要應用的失真量,而α由理論透鏡設計進行界定。p中的輸入值x和y在[-1, +1]之間是標準化的。
p(x, 0)的正向和反向失真函數,α=0.3
α呈增長態勢的桶形失真圖
基于像素的修正 VS 基于網格的修正
一個方法便是,在顯示最后圖像之前,在后期處理片段著色器中進行最后渲染的修正。假設將VR內容渲染至每矩陣的幀緩沖對象(FBO)中。使用OpenGL ES 3.0,頂點著色器和片段著色器便如下所示:
#version 300 es
in highp vec4 posVtx;
uniform mat4 mvpM;
void main(void)
{
gl_Position = mvpM * posVtx;
}
#version 300 es
in highp vec2 texFrg;
out highp vec4 frgCol;
uniform highp vec2 centre;
uniform highp sampler2D texSampler;
void main(void)
{
highp vec4 col = vec4(0.0, 0.0, 0.0, 1.0); /* base colour */
highp float alpha = 0.2; /* lens parameter */
/* Left/Right eye are slightly off centre */
/* Normalize to [-1, 1] and put the centre to "centre" */
highp vec2 p1 = vec2(2.0 * texFrg - 1.0) - centre;
/* Transform */
highp vec2 p2 = p1 / (1.0 - alpha * length(p1));
/* Back to [0, 1] */
p2 = (p2 + centre + 1.0) * 0.5;
if (all(greaterThanEqual(p2, vec2(0.0))) &&
all(lessThanEqual(p2, vec2(1.0))))
{
col = texture(texSampler, p2);
}
frgCol = col;
}
centre 允許將失真集中的部分稍微移出FBO的中間位置,通常透鏡并不完全集中到屏幕的左邊或右邊。
這是我們可以使用的用來計算失真的最準確的方法。不過就GPU的使用而言,這種方法的成本也非常高。由于我們僅處理6個點(兩個三角形到輸出一個矩形),因此頂點著色器運行很快。另外,片段著色器需要在屏幕上為每個像素進行轉換。所以對于1920 x 1080像素的顯示,要進行2073600次運算及紋理查找。
當然,我們可以做得更好!每個程序員都知道,良好性能的關鍵在于預計算和近似值。如果觀察游戲的對象內容,可以看到這些對象中有很多近似值。像級聯陰影貼圖那樣進行陰影計算。不過在未來有了光線追蹤,這將得到極大的改善。
看著以上數字,很明顯我們需要在片段著色器之外進行轉換。我們可以使用多個矩形連接組成網格,而不是在顯示FBO時形成一個矩形。如果現在對網格進行預轉換,便可以得到以下信息:
這種方法的優點是,在初始化時間以上工作只需要做一次。因為透鏡參數不會隨時間變化,因此我們可以反復利用每一幀上的網格。這是優化的第一部分——預計算。
第二部分是近似值:在本例中,由網格分辨率對其進行定義。網格點之間可以進行插補。網格分辨率越低產生的圖像質量則越高,而網格分辨率越高則運行速度會越快。所以使用網格的優勢在哪里呢?使用32 x 32像素的網格可以生成1920 x 1080像素的顯示:
(1920, 1080) / 32 = (60, 33.75)
因此,有2040個矩形和12240個點組成了頂點。頂點著色器要處理的是12240個點,而不是6個。不過,上述相同的頂點著色器也可以使用。可以簡化片段著色器,如下所示:
#version 300 es
in highp vec2 texFrg;
out highp vec4 frgCol;
uniform highp sampler2D texSampler;
void main(void)
{
highp vec4 col = vec4(0.0, 0.0, 0.0, 1.0); /* base colour */
if (all(greaterThanEqual(texFrg, vec2(0.0))) &&
all(lessThanEqual(texFrg, vec2(1.0))))
{
col = texture(texSampler, texFrg);
}
frgCol = col;
}
這還需要進行紋理查找,但可以避免轉換工作。我們節省了大量的功耗,且分辨率更高將節省更多的功耗。不過,在 PowerVR GPU上,還有另一大優勢。由于頂點著色器在TA階段運行 (可以參考PowerVR架構一文),它可以在3D階段獨立運行,而3D階段由于要進行光柵化,因此要執行片段著色器。如果對象填充率有限,將有更多的空間可以確保按時完成對象的渲染。這里,我再次總結了全高清顯示節省功耗的示例:
接下來用一個視頻來展示這兩種方法:
可以找出不同之處么?我也找不出不同點。第一個渲染在片段著色器中進行了轉換,而第二個使用了預計算的網格。
質量評估
看看以上視頻,似乎近似法行之有效。但是千萬不要被我們的雙眼蒙蔽!讓我們嘗試進行量化。對于每一個近似,最重要(最困難)的部分是找到合適的參數。在我們的例子中,這個參數即網格的大小。我使用的網格大小是32 x 32像素,這個大小極好地權衡了輸出質量和速度。以下視頻展示的是使用160 x 160像素作為網格大小時的效果:
當立方體停止旋轉時,看看其左邊的部分,可以看到它是如何循著網格的軌跡。在矩形邊緣點之間,紋理的查找只是線性插補。另一個問題是動畫效果是搖擺的,因為它們不能遵循失真的曲線。所以,可以確定160 x 160像素不是最佳選擇。但看看靜態圖像,并將之與片段著色器制中的輸出轉換進行比較。首先,我為每個輸出圖像創建了向前轉換圖像(在軟件中完成,不借助GPU)。即,僅使用片段著色器時,輸出的網格大小為32px和160px。我還針對向前轉換圖像和原始非轉換圖像輸出的不同創建了差值圖像,如下左所示:
圖像質量對比圖
可以看到,在差值圖像中有更多紅色。但可以用數值進行表述。在視頻編解碼器的圖像對比中,PSNR值(峰值信噪比)是已知的標準。通過使用ImageMagick,并計算差值圖像和PSNR,可以得到:
PSNR值更高意味兩個圖像更接近??梢郧宄乜吹?,使用32px作為圖像的網格參數與使用片段渲染生成的圖像非常接近。
總結
透鏡修正是VR流水線一個至關重要的部分。通過使用正確的技巧,我們可以降低這個特定部分的GPU需求,并同時保持圖像的高質量。這使應用程序得以創建更豐富的對象或設備,以通過縮短喚醒GPU的時間,從而節省功耗。
評論
查看更多