此篇講圖像采樣
一、采樣流程
在上一節裡的流程圖有寫到,圖像繪制的實際渲染發生在某個blitter的blitrect函數中,我們先看一個具體的blitrect實作。
其中shadespan用來将shader中x,y坐标處的值取n個到dst的buffer中。
對于圖像繪制時,它是 skbitmapprocshader,這裡是其實作:
流程如下:
1、存在 shaderproc,直接用
2、計算一次能處理的像素數count
3、mproc計算count個坐标,sproc根據坐标值去取色
注意到之前三個函數指針:
state.getshaderproc32
mproc = state.getmatrixproc
sproc = state.getshaderproc32
這三個函數指針在一開始建立blitter時設定:
skblitter::choose -> skshader::createcontext -> skbitmapprocshader::oncreatecontext -> skbitmapprocstate::chooseprocs
這是一個相當長的函數,它做的事情如下:
1、(優化步驟)在大于skpaint::klow_filterlevel的品質要求下,試圖做預縮放。
2、選擇matrix函數:choosematrixproc。
3、選擇sample函數:
(1)高品質:setbitmapfilterprocs
(2)klow_filterlevel或knone_filterlevel:采取flags計算的方法,根據x,y變化矩陣情況和采樣要求選擇函數
4、(優化步驟)在滿足條件時,選取shader函數,此函數替代matrix和sample函數
5、(優化步驟)platformprocs(),進一步選擇優化版本的sample函數
對于rgb565格式的目标,使用的是skshader的 shadespan16 方法。shadespan16的代碼邏輯類似,不再說明。
二、matrixproc和sampleproc
matrixproc的使命是生成坐标集。sampleproc則根據坐标集取像素,采樣合成
我們先倒過來看 sampleproc 看這個坐标集是怎麼使用的:
nofilter_dx系列:
nofilter_dxdy系列:
這兩個系列是直接取了x,y坐标處的圖像像素
filter_dx系列:
filter_dxdy系列:
将四個相鄰像素取出來之後,作filter處理
看暈了麼,其實總結一下是這樣:
nofilter_dx,第一個32位數表示y,其餘的32位數包含兩個x坐标。
nofilter_dxdy,用16位表示x,16位表示y。這種情況就是取的最近值,直接到x,y坐标處取值就可以了。
filter_dxdy系列,每個32位數分别表示x和y坐标(14:4:14),交錯排列,中間的內插補點部分是相差的小數擴大16倍而得的近似整數。
filter_dx系列,第一個數為y坐标用14:4:14的方式存儲,後面的數為x坐标,也用14:4:14的方式存儲,前後為對應坐标,中間為放大16倍的距離,這個情況是一行之内y坐标相同(隻做縮放或小數平移的情況),一樣是作雙線性插值。
下面我們來看matrixproc的實作,
先跟進 choosematrixproc的代碼:
有些函數是找符号找不到的,我們注意到skbitmapprocstate.cpp 中包含了多次 skbitmapprocstate_matrix.h 頭檔案:
頭檔案代碼如下:
然後我們就清楚了,這些函數名是用宏組合出來的。(神一般的代碼。。。。。)
怎麼算坐标的不詳述了,主要按原理去推就可以了,坐标計算有三種模式:clamp(越界時限制在邊界)、repeat(越界時從開頭取起)、mirror(越界時取樣方向倒轉去取)。
sampleproc函數也是類似的方法組合出來的,不詳述。
三、進階插值算法
雙線性插值雖然在一般情況下夠用了,但在放大圖檔時,效果還是不夠好。需要更好的效果,可以用進階插值算法,代價是性能的大幅消耗。
進階插值算法目前在android的java代碼處是走不進去的,不知道chromium是否用到。
幾個要點:
1、在 setbitmapfilterprocs 時判斷進階插值是否支援,若支援,設定 shaderproc 為 highqualityfilter32/highqualityfilter16(也就是獨立計算坐标和采樣像素)
2、highqualityfilter先通過變換矩陣計算原始點。
3、highqualityfilter根據 skbitmapfilter 的采樣視窗,将這個視窗中的所有點按其與原始點矩離,查詢對應權重值,然後相加,得到最終像素點。
4、skbitmapfilter 采用查表法去給出權重值,預計算由子類完成。
5、目前skia庫用的是雙三次插值 mitchell 法。
sk_conf_declare(const char *, c_bitmapfilter, "bitmap.filter", "mitchell", "which scanline bitmap filter to use [mitchell, lanczos, hamming, gaussian, triangle, box]");
詳細代碼見 external/skia/src/core/skbitmapfilter.cpp,盡量這部分代碼幾乎無用武之地,但裡面的公式很值得借鑒,随便改改就能做成 glsl shader 用。
看完這段代碼,可以作不負責任的猜想:skia設計之初,隻考慮了近鄰插值和雙線性插值兩種情況,是以采用這種模闆方法,可以最小化代碼量。而且matrixproc和sampleproc可以後續分别作simd優化(intel的sse和arm的neon),以提高性能。
但是對于線性插值,兩步法(取值——采樣)在算法實作上本來就不是最優的,後面又不得不引入shader函數,應對一些場景做優化。高階插值無法在這個設計下實作,是以又像更新檔一樣打上去。
四、總結
看完這一部分代碼,有幾個感受。
第一:繪張圖檔看上去一件簡單的事,在渲染執行時,真心不容易,如果追求效果,還會有各種各樣的花樣。
第二:在性能有要求的場景下,用模闆真是災難:函數改寫時,遇到模闆,就不得不重新定義函數,并替換之,弄得代碼看上去一下子混亂不少。
第三:從圖像繪制這個角度上看,skia渲染性能雖然确實很好了,但遠沒有達到極限,仍然是有一定的優化空間的,如果這部分出現了性能問題,還是能做一定的優化的。關于skia性能的讨論将放到介紹skia系列的最後一章。
第四:opengl+glsl确實是輕松且高效多了,軟體渲染在複雜場景上性能很有限。