軟體渲染器實現Phong Shading的性能優化?

上次承蒙葉老師@Milo Yip,和前輩@周翀

幫我解決了這個問題:

Gouraud著色的邊界問題? - 計算機圖形學

隨後我便嘗試加入了Phong Shading,最後實現的效果如下:

(左圖為Gouraud Shading,右圖為Phong Shading)

從畫面上可以看出,改進的效果還是很明顯的。可是正如書上所說的,

Phong Shading由於要逐像素計算光照,計算量將數倍於Gouraud Shading,因此會有性能問題。我這裡也遇到了這個問題。

當物體投影時縮放得比較小時,性能還好,看不出明顯的卡頓。用滑鼠自由拖曳轉動,最高可以達到200+ fps。但是當使用較大的縮放時,物體幾乎佔據了整個窗口,這時要計算的像素就變多了,性能也下降得厲害。這時候最高只能有40+fps。

我想問一下,針對Phong Shading有沒有什麼好的性能優化?

我找到了一篇論文:

Fast Phong Shading - Gary Bishop, David M. Weimer

裡面提到了用泰勒展開式來近似光照強度函數,從而可以使用增量式計算,減小計算量。但是我感覺裡面有許多Corner Case沒有考慮到,比如在插值的過程中

L,H,N這幾個向量有可能會出現零向量,以及演算法的開頭要解一個向量方程:

N = Ax + By + C,感覺這些部分會引入比較複雜的邏輯,效率並不會高很多啊?

思考再三,還是貼一下代碼吧...寫得不好,見笑了:

static int calculate_lighting(const MATHLIB3D::Vector4D& normal, const MATHLIB3D::Vector4D& to_light_source, const MATHLIB3D::Vector4D& to_camera) noexcept
{
// normal 是該點的法向量
// to_light_source 是該點指向光源的向量
// to_camera 是該點指向攝像機的向量

//散射光
double diffuse_factor;
//高光
double specular_factor;
//環境光
double ambient_factor = 0.1;

auto N_len_square = MATHLIB3D::dotproduct(normal, normal);
double L_len_square = MATHLIB3D::dotproduct(to_light_source, to_light_source);
auto V_len_square = MATHLIB3D::dotproduct(to_camera, to_camera);
double L_len = sqrt(L_len_square);
if (N_len_square == 0.0 || L_len_square == 0.0)
{
diffuse_factor = 0.0;
specular_factor = 0.0;
}
else
{
//N, L, V分別是normal, to_light_source, to_camera對應的單位向量
auto N = normal * (1.0 / sqrt(N_len_square));
auto L = to_light_source * (1.0 / L_len);
auto V = V_len_square == 0.0 ? to_camera : to_camera * (1.0 / sqrt(V_len_square));

diffuse_factor = std::max&(MATHLIB3D::dotproduct(N, L), 0.0);

auto H = L + V;
auto H_len_square = MATHLIB3D::dotproduct(H, H);
if (MATHLIB3D::dotproduct(N, L) &<= 0.0 || H_len_square == 0.0) specular_factor = 0.0; else { H *= 1.0 / sqrt(H_len_square); // 將高光的衰減因子設為32 specular_factor = MATHLIB3D::dotproduct(N, H); if (specular_factor != 0.0) { specular_factor *= specular_factor; specular_factor *= specular_factor; specular_factor *= specular_factor; specular_factor *= specular_factor; specular_factor *= specular_factor; } } } double intensity_factor = 10.0 / (10.0 * L_len_square + L_len + 10.0); intensity_factor = std::min&(intensity_factor, 1.0);

int gray = static_cast&((ambient_factor + intensity_factor * diffuse_factor + intensity_factor * specular_factor) * static_cast&(0xff));
gray = std::min&(gray, 0xff);
return RGB(gray, gray, gray);
}


這個問題看得一頭霧水,在看了鏈接才知道,原來說的是軟體渲染器。請在這個問題里說清楚,保持問題本身完整。

Gouraud是算完光照,插值結果。Phong是插值L H N等,在去算光照。所以這裡涉及到兩個會降低性能的地方。第一個是,LHN的插值和傳遞,第二個是像素級光照的計算。你需要測出到底是哪個慢。而這些都跟0向量以及向量方程沒關係,插值就是插值。考慮好perspective correction就行了。

你還得確定用的BRDF是Blinn-Phong還是Phong。前者計算量更小而且效果更好。

在GPU上,有一個用Spherical Gaussian近似pow的方法,可以節省一條指令。來自Spherical Gaussian approximation for Blinn-Phong, Phong and Fresnel

pow(dotHN, K) = exp((K+0.775)*(dotHN-1)) = exp2((K+0.775)/Log(2)*(dotHN-1)

== 更新 ==

有代碼之後可以看出更多問題。

1. double什麼鬼。float足矣。

2. H也應該在頂點上算完插值出來。

3. MATHLIB3D::dotproduct(N, L)重複計算了

4. N dot L不需要在normalize之後才計算。normalize並不影響N dot L的結果。所以應該先dot,有機會提前退出。

5. 2016年了,該用physically-based BRDF了。


除了做近似化,可做底層優化,如用 SIMD SOA 形式一次計算 4 個或 8 個像素,對於少分支的計算密集、流處理的場合會有很多性能提升。

一般 SIMD 還有 rsqrt 指令,快速地計算開平方的倒數的近似值,適合做歸一化。


第一,不要用pow。

第二,Profiling一下。


大神在,不敢班門弄斧。

不過我還是要稍微說一下關於指令集優化。

avx已經可以8個float一起運算了,速度比普通運算翻倍還是很容易的。

不過數據並行有時候並不容易,因為很可能你不一定能找到這樣合適的機會(我只在判斷包圍盒的時候順利用到了8個float同時運算)

而且指令集是有坑的,比如avx的plane,不能隨意shuffle。比如sse和avx混用會性能下降,比如指令集的延遲和吞吐量,還比如指令集佔用的發射埠相同可能會導致前後堵塞。。。。

不過用來代替點積運算還是可以的,sse4.1就有了相關的指令集我記得。。。

此外就是多線程,像素和像素之間不相關,那就有並行的潛力。

反正我在光線跟蹤里用的是64*64為一個方格,並由N個線程靠原子變數自行爭搶需要渲染的方格,效果拔群(cpu瞬間滿載)。


有兩點陣圖形學巨擎在此,我就不敢從圖形學方面說了。下面幾個拙見,題主隨便批判批判~

1,crt里的pow確實不快,但它的精度是有保證的。我以前從網上用「fast pow」搜過,找到了下面一段程序。經實測確實比pow快。雖然冪次只能用整數,但是對於specularity的計算足夠了。

template&
T opt_pow(T x, unsigned int n)
{
T pw = (T)1;
while (n &> 0)
{
if (n 1)
pw *= x;
x *= x;
n &>&>= 1;
}

return pw;
}

2,用SIMD會給你一定程度的驚喜。咬咬牙,用SSE1/2/3把所有向量和矩陣運算重寫一遍吧!

這是葉神@Milo Yip 曾推薦的,我認為我見過的最好的SIMD手冊。不過我沒有在農企的u上試過。

https://software.intel.com/sites/landingpage/IntrinsicsGuide/

3,我想,算上超線程,你的機器里至少有4個邏輯核吧?你不用並行的話,渲染的時候,你打開任務管理器,看看cpu使用率,會看到其中一個核全綠,另外的都閑著。這樣的不平衡會加速cpu老化,將來你打開機箱,會看到cpu有四分之一的面積上明顯發黑!……好吧我又不正經了。

你一定要上並行計算。如果你沒有多路xeon,就只在pixel shading時並行就好。但是這比較挑戰你的設計功力。你要考慮:

(1)怎麼在vertex shading線程和pixel shading線程之間傳遞數據,才能盡量不用鎖?盡量少等待?

(2)多個pixel shading線程同時寫frame buffer時,它們的寫入點之間距離不要小於64位元組,即一個高緩線的長度,以避免false sharing。

(3)盡量避免某些shading線程,在某些情況里,提前結束工作,閑著等著別的線程。要盡量把所有線程餵飽。做到這一點,比上一點難得多得多,這涉及一些delay hiding技巧。但是好在即使這一點做的不夠好,你也能在一定程度上享受並行帶來的快感,不像上一條,若有些許沒做好,速跪。


early-z ?

當然這個沒針對Phong Shading,求摺疊。。。


卡負優化性能降35%


推薦閱讀:

在基於高度圖的遊戲大世界中該怎麼做動態物體的遮擋剔除比較好呢?
OpenGL適合用來加速圖像處理嗎?
請問光柵化與渲染這兩個術語的區別和聯繫是什麼?
如何評價蘋果在 WWDC 2017 更新的圖形 API Metal 2?
影視渲染和遊戲渲染的區別?

TAG:遊戲引擎 | C | 計算機圖形學 |