電腦或者手機上作圖像處理有不少方式,可是目前爲止最高效的方法是有效地使用圖形處理單元,或者叫 GPU。你的手機包含兩個不一樣的處理單元,CPU 和 GPU。CPU 是個多面手,而且不得不處理全部的事情,而 GPU 則能夠集中來處理好一件事情,就是並行地作浮點運算。事實上,圖像處理和渲染就是在將要渲染到窗口上的像素上作許許多多的浮點運算。程序員
經過有效的利用 GPU,能夠成百倍甚至上千倍地提升手機上的圖像渲染能力。若是不是基於 GPU 的處理,手機上實時高清視頻濾鏡是不現實,甚至不可能的。編程
着色器 (shader) 是咱們利用這種能力的工具。着色器是用着色語言寫的小的,基於 C 語言的程序。如今有很許多種着色語言,但你若是作 OS X 或者 iOS 開發的話,你應該專一於 OpenGL 着色語言,或者叫 GLSL。你能夠將 GLSL 的理念應用到其餘的更專用的語言 (好比 Metal) 上去。這裏咱們即將介紹的概念與和 Core Image 中的自定義核矩陣有着很好的對應,儘管它們在語法上有一些不一樣。數組
這個過程可能會很讓人恐懼,尤爲是對新手。這篇文章的目的是讓你接觸一些寫圖像處理着色器的必要的基礎信息,並將你帶上書寫你本身的圖像處理着色器的道路。ide
在 OpenGL ES 中你必須建立兩種着色器:頂點着色器 (vertex shaders) 和片斷着色器 (fragment shaders)。這兩種着色器是一個完整程序的兩半,你不能僅僅建立其中任何一個;想建立一個完整的着色程序,兩個都是必須存在。函數
頂點着色器定義了在 2D 或者 3D 場景中幾何圖形是如何處理的。一個頂點指的是 2D 或者 3D 空間中的一個點。在圖像處理中,有 4 個頂點:每個頂點表明圖像的一個角。頂點着色器設置頂點的位置,而且把位置和紋理座標這樣的參數發送到片斷着色器。工具
而後 GPU 使用片斷着色器在對象或者圖片的每個像素上進行計算,最終計算出每一個像素的最終顏色。圖片,歸根結底,實際上僅僅是數據的集合。圖片的文檔包含每個像素的各個顏色份量和像素透明度的值。由於對每個像素,算式是相同的,GPU 能夠流水線做業這個過程,從而更加有效的進行處理。使用正確優化過的着色器,在 GPU 上進行處理,將使你得到百倍於在 CPU 上用一樣的過程進行圖像處理的效率。性能
好吧,關於着色器咱們說的足夠多了。咱們來看一個實踐中真實的着色器程序。這裏是一個 GPUImage 中一個基礎的頂點着色器:優化
NSString *const kGPUImageVertexShaderString = SHADER_STRING ( attribute vec4 position; attribute vec4 inputTextureCoordinate; varying vec2 textureCoordinate; void main() { gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; } );
咱們一句一句的來看:網站
attribute vec4 position;
像全部的語言同樣,着色器語言的設計者也爲經常使用的類型創造了特殊的數據類型,例如 2D 和 3D 座標。這些類型是向量,稍後咱們會深刻更多。回到咱們的應用程序的代碼,咱們建立了一系列頂點,咱們爲每一個頂點提供的參數裏的其中一個是頂點在畫布中的位置。而後咱們必須告訴咱們的頂點着色器它須要接收這個參數,咱們稍後會將它用在某些事情上。ui
attribute vec4 inputTextureCoordinate;
如今你或許很奇怪,爲何咱們須要一個紋理座標。咱們不是剛剛獲得了咱們的頂點位置了嗎?難道它們不是一樣的東西嗎?
其實它們並不是必定是一樣的東西。紋理座標是紋理映射的一部分。這意味着你想要對你的紋理進行某種濾鏡操做的時候會用到它。左上角座標是 (0,0)。右上角的座標是 (1,0)。若是咱們須要在圖片內部而不是邊緣選擇一個紋理座標,咱們須要在咱們的應用中設定的紋理座標就會與此不一樣,像是 (.25, .25) 是在圖片左上角向右向下各圖片高寬 1/4 的位置。在咱們當前的圖像處理應用裏,咱們但願紋理座標和頂點位置一致,由於咱們想覆蓋到圖片的整個長度和寬度。有時候你或許會但願這些座標是不一樣的,因此須要記住它們未必是相同的座標。在這個例子中,頂點座標空間從 -1.0 延展到 1.0,而紋理座標是從 0.0 到 1.0。
varying vec2 textureCoordinate;
由於頂點着色器負責和片斷着色器交流,因此咱們須要建立一個變量和它共享相關的信息。在圖像處理中,片斷着色器須要的惟一相關信息就是頂點着色器如今正在處理哪一個像素。
gl_Position = position;
gl_Position 是一個內建的變量。GLSL 有一些內建的變量,在片斷着色器的例子中咱們將看到其中的一個。這些特殊的變量是可編程管道的一部分,API 會去尋找它們,而且知道如何和它們關聯上。在這個例子中,咱們指定了頂點的位置,而且把它從咱們的程序中反饋給渲染管線。
textureCoordinate = inputTextureCoordinate.xy;
最後,咱們取出這個頂點中紋理座標的 X 和 Y 的位置。咱們只關心 inputTextureCoordinate 中的前兩個參數,X 和 Y。這個座標最開始是經過 4 個屬性存在頂點着色器裏的,但咱們只須要其中的兩個。咱們拿出須要的屬性,而後賦值給一個將要和片斷着色器通訊的變量,而不是把更多的屬性反饋給片斷着色器。
在大多數圖像處理程序中,頂點着色器都差很少,因此,這篇文章接下來的部分,咱們將集中討論片斷着色器。
看過了咱們簡單的頂點着色器後,咱們再來看一個能夠實現的最簡單的片斷着色器:一個直通濾鏡:
varying highp vec2 textureCoordinate; uniform sampler2D inputImageTexture; void main() { gl_FragColor = texture2D(inputImageTexture, textureCoordinate); }
這個着色器實際上不會改變圖像中的任何東西。它是一個直通着色器,意味着咱們輸入每個像素,而後輸出徹底相同的像素。咱們來一句句的看:
varying highp vec2 textureCoordinate;
由於片斷着色器做用在每個像素上,咱們須要一個方法來肯定咱們當前在分析哪個像素/片斷。它須要存儲像素的 X 和 Y 座標。咱們接收到的是當前在頂點着色器被設置好的紋理座標。
uniform sampler2D inputImageTexture;
爲了處理圖像,咱們從應用中接收一個圖片的引用,咱們把它當作一個 2D 的紋理。這個數據類型被叫作 sampler2D ,這是由於咱們要從這個 2D 紋理中採樣出一個點來進行處理。
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
這是咱們碰到的第一個 GLSL 特有的方法:texture2D,顧名思義,建立一個 2D 的紋理。它採用咱們以前聲明過的屬性做爲參數來決定被處理的像素的顏色。這個顏色而後被設置給另一個內建變量,gl_FragColor。由於片斷着色器的惟一目的就是肯定一個像素的顏色,gl_FragColor 本質上就是咱們片斷着色器的返回語句。一旦這個片斷的顏色被設置,接下來片斷着色器就不須要再作其餘任何事情了,因此你在這以後寫任何的語句,都不會被執行。
就像你看到的那樣,寫着色器很大一部分就是了解着色語言。即便着色語言是基於 C 語言的,依然有不少怪異和細微的差異讓它和普通的 C 語言有不一樣。
看一看咱們的直通着色器,你會注意到有一個屬性被標記爲 「varying」,另外一個屬性被標記爲 「uniform」。
這些變量是 GLSL 中的輸入和輸出。它容許從咱們應用的輸入,以及在頂點着色器和片斷着色器之間進行交流。
在 GLSL 中,實際有三種標籤能夠賦值給咱們的變量:
Uniforms 是一種外界和你的着色器交流的方式。Uniforms 是爲在一個渲染循環裏不變的輸入值設計的。若是你正在應用茶色濾鏡,而且你已經指定了濾鏡的強度,那麼這些就是在渲染過程當中不須要改變的事情,你能夠把它做爲 Uniform 輸入。 Uniform 在頂點着色器和片斷着色器裏均可以被訪問到。
Attributes 僅僅能夠在頂點着色器中被訪問。Attribute 是在隨着每個頂點不一樣而會發生變更的輸入值,例如頂點的位置和紋理座標等。頂點着色器利用這些變量來計算位置,以它們爲基礎計算一些值,而後把這些值以 varyings 的方式傳到片斷着色器。
最後,但一樣重要的,是 varyings 標籤。Varying 在頂點着色器和片斷着色器都會出現。Varying 是用來在頂點着色器和片斷着色器傳遞信息的,而且在頂點着色器和片斷着色器中必須有匹配的名字。數值在頂點着色器被寫入到 varying ,而後在片斷着色器被讀出。被寫入 varying 中的值,在片斷着色器中會被以插值的形式插入到兩個頂點直接的各個像素中去。
回看咱們以前寫的簡單的着色器的例子,在頂點着色器和片斷着色器中都用 varying 聲明瞭 textureCoordinate。咱們在頂點着色器中寫入 varying 的值。而後咱們把它傳入片斷着色器,並在片斷着色器中讀取和處理。
在咱們繼續以前,最後一件要注意的事。看看建立的這些變量。你會注意到紋理座標有一個叫作 highp 的屬性。這個屬性負責設置你須要的變量精度。由於 OpenGL ES 被設計爲在處理能力有限的系統中使用,精度限制被加入進來能夠提升效率。
若是不須要很是高的精度,你能夠進行設定,這或許會容許在一個時鐘循環內處理更多的值。相反的,在紋理座標中,咱們須要儘量的確保精確,因此咱們具體說明確實須要額外的精度。
精度修飾存在於 OpenGL ES 中,由於它是被設計用在移動設備中的。可是,在老版本的桌面版的 OpenGL 中則沒有。由於 OpenGL ES 其實是 OpenGL 的子集,你幾乎老是能夠直接把 OpenGL ES 的項目移植到 OpenGL。若是你這樣作,記住必定要在你的桌面版着色器中去掉精度修飾。這是很重要的一件事,尤爲是當你計劃在 iOS 和 OS X 之間移植項目時。
在 GLSL 中,你會用到不少向量和向量類型。向量是一個很棘手的話題,它們表面上看起來很直觀,可是由於它們有不少用途,這使咱們在使用它們時經常會感到迷惑。
在 GLSL 環境中,向量是一個相似數組的特殊的數據類型。每一種類型都有固定的能夠保存的元素。深刻研究一下,你甚至能夠得到數組能夠存儲的數值的精確的類型。可是在大多數狀況下,只要使用通用的向量類型就足夠了。
有三種向量類型你會常常看到:
這些向量類型包含特定數量的浮點數:vec2 包含兩個浮點數,vec3 包含三個浮點數,vec4 包含四個浮點數。
這些類型能夠被用在着色器中可能被改變或者持有的多種數據類型中。在片斷着色器中,很明顯 X 和 Y 座標是的你想保存的信息。 (X,Y) 存儲在 vec2 中就很合適。
在圖像處理過程當中,另外一個你可能想持續追蹤的事情就是每一個像素的 R,G,B,A 值。這些能夠被存儲在 vec4 中。
如今咱們已經瞭解了向量,接下來繼續瞭解矩陣。矩陣和向量很類似,可是它們添加了額外一層的複雜度。矩陣是一個浮點數數組的數組,而不是單個的簡單浮點數數組。
相似於向量,你將會常常處理的矩陣對象是:
vec2 保存兩個浮點數,mat 保存至關於兩個 vec2 對象的值。將向量對象傳遞到矩陣對象並非必須的,只須要有足夠填充矩陣的浮點數便可。在 mat2 中,你須要傳入兩個 vec2 或者四個浮點數。由於你能夠給向量命名,並且相比於直接傳浮點數,你只須要負責兩個對象,而不是四個,因此很是推薦使用封裝好的值來存儲你的數字,這樣更利於追蹤。對於 mat4 會更復雜一些,由於你要負責 16 個數字,而不是 4 個。
在咱們 mat2 的例子中,咱們有兩個 vec2 對象。每一個 vec2 對象表明一行。每一個 vec2 對象的第一個元素表明一列。構建你的矩陣對象的時候,確保每一個值都放在了正確的行和列上是很重要的,不然使用它們進行運算確定得不到正確的結果。
既然咱們有了矩陣也有了填充矩陣的向量,問題來了:「咱們要用它們作什麼呢?「 咱們能夠存儲點和顏色或者其餘的一些的信息,可是要若是經過修改它們來作一些很酷的事情呢?
我找到的最好的關於線性代數和矩陣是如何工做的資源是這個網站的更好的解釋。我從這個網站偷來借鑑的一句引述就是:
線性代數課程的倖存者都成爲了物理學家,圖形程序員或者其餘的受虐狂。
矩陣操做整體來講並不「難」;只不過它們沒有被任何上下文解釋,因此很難概念化地理解究竟爲何會有人想要和它們打交道。我但願能在給出一些它們在圖形編程中的應用背景後,咱們能夠了解它們怎樣幫助咱們實現難以想象的東西。
線性代數容許你一次在不少值上進行操做。假想你有一組數,你想要每個數乘以 2。你通常會一個個地順次計算數值。可是由於對每個數都進行的是一樣的操做,因此你徹底能夠並行地實現這個操做。
咱們舉一個看起來可怕的例子,CGAffineTransforms。仿射轉化是很簡單的操做,它能夠改變具備平行邊的形狀 (好比正方形或者矩形) 的大小,位置,或者旋轉角度。
在這種時候你固然能夠坐下來拿出筆和紙,本身去計算這些轉化,但這麼作其實沒什麼意義。GLSL 有不少內建的函數來進行這些龐雜的用來計算轉換的函數。瞭解這些函數背後的思想纔是最重要的。
這篇文章中,咱們不會把全部的 GLSL 內建的函數都過一遍,不過你能夠在 Shaderific 上找到很好的相關資源。不少 GLSL 函數都是從 C 語言數學庫中的基本的數學運算導出的,因此解釋 sin 函數是作什麼的真的是浪費時間。咱們將集中闡釋一些更深奧的函數,從而達到這篇文章的目的,解釋怎樣才能充分利用 GPU 的性能的一些細節。
step(): GPU 有一個侷限性,它並不能很好的處理條件邏輯。GPU 喜歡作的事情是接受一系列的操做,並將它們做用在全部的東西上。分支會在片斷着色器上致使明顯的性能降低,在移動設備上尤爲明顯。step() 經過容許在不產生分支的前提下實現條件邏輯,從而在某種程度上能夠緩解這種侷限性。若是傳進 step() 函數的值小於閾值,step() 會返回 0.0。若是大於或等於閾值,則會返回 1.0。經過把這個結果和你的着色器的值相乘,着色器的值就能夠被使用或者忽略,而不用使用 if() 語句。
mix(): mix 函數將兩個值 (例如顏色值) 混合爲一個變量。若是咱們有紅和綠兩個顏色,咱們能夠用 mix() 函數線性插值。這在圖像處理中很經常使用,好比在應用程序中經過一組獨特的設定來控制效果的強度等。
*clamp(): GLSL 中一個比較一致的方面就是它喜歡使用歸一化的座標。它但願收到的顏色份量或者紋理座標的值在 0.0 和 1.0 之間。爲了保證咱們的值不會超出這個很是窄的區域,咱們可使用 clamp() 函數。 clamp() 會檢查並確保你的值在 0.0 和 1.0 之間。若是你的值小於 0.0,它會把值設爲 0.0。這樣作是爲了防止一些常見的錯誤,例如當你進行計算時意外的傳入了一個負數,或者其餘的徹底超出了算式範圍的值。
我知道數學的洪水必定讓你快被淹沒了。若是你還能跟上我,我想舉幾個優美的着色器的例子,這會更有意義,這樣你又有機會淹沒在 GLSL 的潮水中。
這是一個作飽和度調節的片斷着色器。這個着色器出自 《圖形着色器:理論和實踐》一書,我強烈推薦整本書給全部對着色器感興趣的人。
飽和度是用來表示顏色的亮度和強度的術語。一件亮紅色的毛衣的飽和度要遠比北京霧霾時灰色的天空的飽和度高得多。
在這個着色器上,參照人類對顏色和亮度的感知過程,咱們有一些優化可使用。通常而言,人類對亮度要比對顏色敏感的多。這麼多年來,壓縮軟件體積的一個優化方式就是減小存儲顏色所用的內存。
人類不只對亮度比顏色要敏感,一樣亮度下,咱們對某些特定的顏色反應也更加靈敏,尤爲是綠色。這意味着,當你尋找壓縮圖片的方式,或者以某種方式改變它們的亮度和顏色的時候,多放一些注意力在綠色光譜上是很重要的,由於咱們對它最爲敏感。
varying highp vec2 textureCoordinate; uniform sampler2D inputImageTexture; uniform lowp float saturation; // Values from "Graphics Shaders: Theory and Practice" by Bailey and Cunningham const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721); void main() { lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate); lowp float luminance = dot(textureColor.rgb, luminanceWeighting); lowp vec3 greyScaleColor = vec3(luminance); gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w); }
咱們一行行的看這個片斷着色器的代碼:
varying highp vec2 textureCoordinate; uniform sampler2D inputImageTexture; uniform lowp float saturation;
再一次,由於這是一個要和基礎的頂點着色器通訊的片斷着色器,咱們須要爲輸入紋理座標和輸入圖片紋理聲明一個 varyings 變量,這樣才能接收到咱們須要的信息,並進行過濾處理。這個例子中咱們有一個新的 uniform 的變量須要處理,那就是飽和度。飽和度的數值是一個咱們從用戶界面設置的參數。咱們須要知道用戶須要多少飽和度,從而展現正確的顏色數量。
const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
這就是咱們設置三個元素的向量,爲咱們的亮度來保存顏色比重的地方。這三個值加起來要爲 1,這樣咱們才能把亮度計算爲 0.0 – 1.0 之間的值。注意中間的值,就是表示綠色的值,用了 70% 的顏色比重,而藍色只用了它的 10%。藍色對咱們的展現不是很好,把更多權重放在綠色上是頗有意義的。
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
咱們須要取樣特定像素在咱們圖片/紋理中的具體座標來獲取顏色信息。咱們將會改變它一點點,而不是想直通濾鏡那樣直接返回。
lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
這行代碼會讓那些沒有學過線性代數或者很早之前在學校學過可是不多用過的人看起來不那麼熟悉。咱們是在使用 GLSL 中的點乘運算。若是你記得在學校裏曾用過點運算符來相乘兩個數字的話,那麼你就能明白是什麼回事兒了。點乘計算以包含紋理顏色信息的vec4 爲參數,捨棄 vec4 的最後一個不須要的元素,將它和相對應的亮度權重相乘。而後取出全部的三個值把它們加在一塊兒,計算出這個像素綜合的亮度值。
lowp vec3 greyScaleColor = vec3(luminance);
最後,咱們把全部的片斷組合起來。爲了肯定每一個新的顏色是什麼,咱們使用剛剛學過的很好用的 mix 函數。mix 函數會把咱們剛剛計算的灰度值和初始的紋理顏色以及咱們獲得的飽和度的信息相結合。
這就是一個很棒的,好用的着色器,它讓你用主函數裏的四行代碼就能夠把圖片從彩色變到灰色,或者從灰色變到彩色。還不錯,不是嗎?