天天看點

React 優化技巧在 Web 版光線追蹤裡的應用(下)

在《React 優化技巧在 Web 版光線追蹤裡的應用(上)》中,我們介紹了 JS 中的操作符重載方案;在《React 優化技巧在 Web 版光線追蹤裡的應用(中)》中,我們介紹了 Time Slicing 和 Streaming Rendering 優化政策。

代碼裡的公式不再難看,UI 線程不再卡頓。用漸進的方式進行第一次渲染,使用者更快的看見内容。這些都是不錯的優化。

不過,我們還可以做更多。

進階方案:Schedule

React 即将釋出的 Concurrency Mode 特性,裡面包含了 Suspense 和 Schedule 的功能。它們的作用是,讓 UI 的渲染根據不同的觸發來源和子產品重要程度,劃分出優先級。

一個頁面,并非所有子產品都一樣重要。總有一些子產品,如标題,頭圖,價格等,比其它子產品(如側邊欄,廣告)重要。

頁面中觸發更新的來源,也并非一樣緊急。比如響應使用者輸入,就應該比其它任何渲染請求更重要。為此,Facebook 工程師還專門向 Chrome 團隊,貢獻了 isInputPending 這個 API,可以知道目前是否有使用者輸入。如果有,React 可以及時截斷目前任務,處理使用者請求。

對于我們的光線追蹤場景來說,這種優先級關系也可以劃分出來。比如相比背景,物體,特别是聚焦的物體,顯然更重要。我們可以更多地計算物體所在的像素裡的光線采樣情況。

特别的,很多時候,背景不需要反複地去計算光線,它可能每次計算,都得到同一個值。我們可以探測到這種情況,直接跳過它們,把計算資源放到更重要的像素位置裡。

優先級劃分的政策,根據場景和需求的不同而變化。在此,我采用了一個相對通用的處理辦法,就是對比前後兩張圖檔的像素的均方誤差(Mean Squared Error),按照誤差大小進行排序,誤差大的排前面,誤差小的排後面。每次取前 20000 個來計算光線追蹤。

如此,我們相當于為光線追蹤添加了一個最優化算法。光線追蹤裡發射多條随機光線,本身就是使用蒙特卡羅方法去拟合渲染方程,它在用模拟出來的統計平均值,不斷地逼近渲染方程計算出來的理論值(對蒙特卡羅方法感興趣的同學,可以點選《40+行JS代碼打造你的2048遊戲AI》了解它在遊戲 AI 場景裡的應用)。

我們按照像素誤差來排列我們的蒙特卡洛采樣位置,可以更高效地逼近理論值。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

首先,我們不能在一個 render 函數裡把所有點位計算出來,我們需要細化出一個 renderByPosition 函數,如上所示。這樣我們可以用這個函數,對像素點按照優先級進行光線追蹤,而不必在 for 循環裡無腦依次追蹤。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

然後我們新增 3 個數組,renderCount ,prevImageData 和 currImageData。

之前無腦依次渲染,一個數字變量 innerCount 對所有像素點通用,相除即可以得到平均值。如今,每個像素點被渲染的次數,因為優先級的關系,可能不一緻了。是以需要專門的一一記錄。

我們用 prevImageData 記住上一個顔色值,用 currImageData 記住目前的顔色值,友善用來計算誤差。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

誤差計算很簡單,就是實作一個擷取均方誤差的函數,然後根據上一張圖檔,目前的圖檔,計算出每個位置前後兩個顔色的誤內插補點(每個顔色值包含 RGBA 四個數字)。根據誤差從大到小排列即可。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

我們在資料消費端,也實作了一個 renderByPosition 函數,把光線追蹤 ray.renderByPosition 的結果,跟 renderCount, prevImageData, currImageData 和 imageData.data 進行資料記錄和同步。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

我們在 render 函數裡依然使用了 innerCount,去記錄整體渲染的次數。當它大于 2 時,說明我們起碼有兩張圖檔,可以對比誤差。是以,我們不再遞歸地去 render,而是切換到 scheduleRender 函數,根據優先級追蹤光線。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

scheduleRender 函數,先去獲得誤差清單,取前 20000 個誤差最大的像素點,依次渲染。同時它也做了 Time Slicing 和 Streaming Rendering 處理,讓 UI 保持流暢。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

在 scheduleRender 函數的底部,我們通過 count 記錄 scheduleRender 的次數,大于 5 次後,将渲染模式切換回整體渲染的 render 函數。

這是因為,蒙特卡羅模拟裡帶有随機性,隻靠第一和第二張圖檔的誤差來排優先級,會存在機率性的忽視,現象是圖檔變得不平整,仿佛有很多壞點。按照一定頻次,進行整體渲染,可以讓運氣壞的像素點得到重新鑒定的機會。

通過切換 scheduleRender 和 render,我們盡可能消除了統計偏差。既實作了優先級劃分,又保持了渲染的整體平滑效果。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

花 1000 秒時間,渲染結果如上。上半部分是渲染的圖檔,下半部分是每個像素點的渲染次數,次數越多,顔色越白。我們可以很直覺地看到,我們的計算資源主要配置設定在哪些地方。

從圖中我們可以看到,交界和陰影裡的光線情況相對複雜,是以我們着重去拟合了這些地方的光線情況(更白)。而背景跟天空,顔色比較單一,投入的光線計算資源就應該比較少(更黑)。

重複多次後,我們可以看到,誤差清單裡的 value 值,越來越接近 0。意味着對理論值的拟合變得更好了。

如上所示,通過 Schedule + MSE 最優化政策,我們用更短的時間,就得到了一個局部高清的圖像。而不必等很長時間,去得到一個全局高清的圖像。

進階方案:心理加速

渲染性能上的優化,并非唯一的優化途徑。

實體意義上的快,跟人類心理感受上的快,有時也不是一緻的。

我們之是以在更新階段進行 Schedule 渲染,是因為我們起碼需要兩張圖檔來計算誤差梯度。

但是,仔細一想。第一次渲染,難道就分不出優先級嗎?

根據我們對人類創造的圖檔的浏覽經驗,很容易可以總結出:圖檔中心的内容,比圖檔邊緣的内容,往往更重要。而我們的 Streaming Rendering,仿照的是 React SSR 渲染 HTML 的模式,從上到下。

對于 HTML 文檔來說,從上到下渲染,無可厚非。然而我們是圖檔,我們應該從中間開始,往上下兩個方向展開。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

如上所示,我們僅僅改變了一下渲染的起始位置和方向,使用者就更大機率看到他們更感興趣的東西。而不是首先看到空無一物的天空。

此外,人類視覺有自動捕捉模式的能力。我們不必完整渲染,靠人眼對物體的模式比對,也能讓使用者知道圖檔裡大概包含什麼内容。是以,我們可以按照一定間隙,渲染部分更粗略的圖檔。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

上圖隻渲染了一半像素,隻花了一半的時間。我們确實能識别出圖檔裡的大概内容。如此,我們可以快速地生成粗略圖,讓使用者有視覺占位;結合前面一招,從中間展開像素,細化内容,實作更好的視覺體驗。

React 優化技巧在 Web 版光線追蹤裡的應用(下)

如上所示,現在使用者不僅更快看到視覺中心的物體,還對圖檔的整體有了一定的把握。我們的 Schedule 優先級政策,在首次渲染階段也得到了成功的應用。

總結

回顧一下,我們可以看到,React/Vue 裡的渲染優化政策,在其它地方同樣适用。

+-*/編譯成函數調用,跟 JSX 編譯成 React.createElement 函數調用如出一轍。

長時間渲染卡住 UI 主線程,都能采用 Time Slicing 的做法。

長時間等待一次完整渲染,都可以采用 Streaming Rendering 的做法。

子產品之間存在優先級劃分,都可以采用 Schedule 的做法。