原文地址: https://www.cnblogs.com/salam/archive/2016/01/08/5113572.html
對(duì)照原文, 實(shí)現(xiàn)了一份示例代碼, 放在https://github.com/shaopx/opengl_image_saturation_demo
電腦或者手機(jī)上做圖像處理有很多方式,但是目前為止最高效的方法是有效地使用圖形處理單元,或者叫 GPU。你的手機(jī)包含兩個(gè)不同的處理單元,CPU 和 GPU。CPU 是個(gè)多面手,并且不得不處理所有的事情,而 GPU 則可以集中來(lái)處理好一件事情,就是并行地做浮點(diǎn)運(yùn)算。事實(shí)上,圖像處理和渲染就是在將要渲染到窗口上的像素上做許許多多的浮點(diǎn)運(yùn)算。
通過(guò)有效的利用 GPU,可以成百倍甚至上千倍地提高手機(jī)上的圖像渲染能力。如果不是基于 GPU 的處理,手機(jī)上實(shí)時(shí)高清視頻濾鏡是不現(xiàn)實(shí),甚至不可能的。
著色器 (shader) 是我們利用這種能力的工具。著色器是用著色語(yǔ)言寫的小的,基于 C 語(yǔ)言的程序?,F(xiàn)在有很許多種著色語(yǔ)言,但你如果做 OS X 或者 iOS 開發(fā)的話,你應(yīng)該專注于 OpenGL 著色語(yǔ)言,或者叫 GLSL。你可以將 GLSL 的理念應(yīng)用到其他的更專用的語(yǔ)言 (比如 Metal) 上去。這里我們即將介紹的概念與和 Core Image 中的自定義核矩陣有著很好的對(duì)應(yīng),盡管它們?cè)谡Z(yǔ)法上有一些不同。
這個(gè)過(guò)程可能會(huì)很讓人恐懼,尤其是對(duì)新手。這篇文章的目的是讓你接觸一些寫圖像處理著色器的必要的基礎(chǔ)信息,并將你帶上書寫你自己的圖像處理著色器的道路。
什么是著色器
在 OpenGL ES 中你必須創(chuàng)建兩種著色器:頂點(diǎn)著色器 (vertex shaders) 和片段著色器 (fragment shaders)。這兩種著色器是一個(gè)完整程序的兩半,你不能僅僅創(chuàng)建其中任何一個(gè);想創(chuàng)建一個(gè)完整的著色程序,兩個(gè)都是必須存在。
頂點(diǎn)著色器定義了在 2D 或者 3D 場(chǎng)景中幾何圖形是如何處理的。一個(gè)頂點(diǎn)指的是 2D 或者 3D 空間中的一個(gè)點(diǎn)。在圖像處理中,有 4 個(gè)頂點(diǎn):每一個(gè)頂點(diǎn)代表圖像的一個(gè)角。頂點(diǎn)著色器設(shè)置頂點(diǎn)的位置,并且把位置和紋理坐標(biāo)這樣的參數(shù)發(fā)送到片段著色器。
然后 GPU 使用片段著色器在對(duì)象或者圖片的每一個(gè)像素上進(jìn)行計(jì)算,最終計(jì)算出每個(gè)像素的最終顏色。圖片,歸根結(jié)底,實(shí)際上僅僅是數(shù)據(jù)的集合。圖片的文檔包含每一個(gè)像素的各個(gè)顏色分量和像素透明度的值。因?yàn)閷?duì)每一個(gè)像素,算式是相同的,GPU 可以流水線作業(yè)這個(gè)過(guò)程,從而更加有效的進(jìn)行處理。使用正確優(yōu)化過(guò)的著色器,在 GPU 上進(jìn)行處理,將使你獲得百倍于在 CPU 上用同樣的過(guò)程進(jìn)行圖像處理的效率。
我們的第一個(gè)著色器的例子
頂點(diǎn)著色器
好吧,關(guān)于著色器我們說(shuō)的足夠多了。我們來(lái)看一個(gè)實(shí)踐中真實(shí)的著色器程序。這里是一個(gè) GPUImage 中一個(gè)基礎(chǔ)的頂點(diǎn)著色器:
NSString *const kGPUImageVertexShaderString = SHADER_STRING
(
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
);
我們一句一句的來(lái)看:
attribute vec4 position;
像所有的語(yǔ)言一樣,著色器語(yǔ)言的設(shè)計(jì)者也為常用的類型創(chuàng)造了特殊的數(shù)據(jù)類型,例如 2D 和 3D 坐標(biāo)。這些類型是向量,稍后我們會(huì)深入更多?;氐轿覀兊膽?yīng)用程序的代碼,我們創(chuàng)建了一系列頂點(diǎn),我們?yōu)槊總€(gè)頂點(diǎn)提供的參數(shù)里的其中一個(gè)是頂點(diǎn)在畫布中的位置。然后我們必須告訴我們的頂點(diǎn)著色器它需要接收這個(gè)參數(shù),我們稍后會(huì)將它用在某些事情上。
attribute vec4 inputTextureCoordinate;
現(xiàn)在你或許很奇怪,為什么我們需要一個(gè)紋理坐標(biāo)。我們不是剛剛得到了我們的頂點(diǎn)位置了嗎?難道它們不是同樣的東西嗎?
其實(shí)它們并非一定是同樣的東西。紋理坐標(biāo)是紋理映射的一部分。這意味著你想要對(duì)你的紋理進(jìn)行某種濾鏡操作的時(shí)候會(huì)用到它。左上角坐標(biāo)是 (0,0)。右上角的坐標(biāo)是 (1,0)。如果我們需要在圖片內(nèi)部而不是邊緣選擇一個(gè)紋理坐標(biāo),我們需要在我們的應(yīng)用中設(shè)定的紋理坐標(biāo)就會(huì)與此不同,像是 (.25, .25) 是在圖片左上角向右向下各圖片高寬 1/4 的位置。在我們當(dāng)前的圖像處理應(yīng)用里,我們希望紋理坐標(biāo)和頂點(diǎn)位置一致,因?yàn)槲覀兿敫采w到圖片的整個(gè)長(zhǎng)度和寬度。有時(shí)候你或許會(huì)希望這些坐標(biāo)是不同的,所以需要記住它們未必是相同的坐標(biāo)。在這個(gè)例子中,頂點(diǎn)坐標(biāo)空間從 -1.0 延展到 1.0,而紋理坐標(biāo)是從 0.0 到 1.0。
varying vec2 textureCoordinate;
因?yàn)轫旤c(diǎn)著色器負(fù)責(zé)和片段著色器交流,所以我們需要?jiǎng)?chuàng)建一個(gè)變量和它共享相關(guān)的信息。在圖像處理中,片段著色器需要的唯一相關(guān)信息就是頂點(diǎn)著色器現(xiàn)在正在處理哪個(gè)像素。
如果你使用最新的opengl es版本, 這個(gè)關(guān)鍵字
varying已經(jīng)取消了. 2008.8.11 OGL3.0發(fā)布,伴隨GLSL1.30.10, 其中的attribute varying等就已經(jīng)改成了 in和out; 為了保持原文的一致性, 這里仍然使用varying關(guān)鍵字.
gl_Position = position;
gl_Position 是一個(gè)內(nèi)建的變量。GLSL 有一些內(nèi)建的變量,在片段著色器的例子中我們將看到其中的一個(gè)。這些特殊的變量是可編程管道的一部分,API 會(huì)去尋找它們,并且知道如何和它們關(guān)聯(lián)上。在這個(gè)例子中,我們指定了頂點(diǎn)的位置,并且把它從我們的程序中反饋給渲染管線。
textureCoordinate = inputTextureCoordinate.xy;
最后,我們?nèi)〕鲞@個(gè)頂點(diǎn)中紋理坐標(biāo)的 X 和 Y 的位置。我們只關(guān)心 inputTextureCoordinate 中的前兩個(gè)參數(shù),X 和 Y。這個(gè)坐標(biāo)最開始是通過(guò) 4 個(gè)屬性存在頂點(diǎn)著色器里的,但我們只需要其中的兩個(gè)。我們拿出需要的屬性,然后賦值給一個(gè)將要和片段著色器通信的變量,而不是把更多的屬性反饋給片段著色器。
在大多數(shù)圖像處理程序中,頂點(diǎn)著色器都差不多,所以,這篇文章接下來(lái)的部分,我們將集中討論片段著色器。
片段著色器
看過(guò)了我們簡(jiǎn)單的頂點(diǎn)著色器后,我們?cè)賮?lái)看一個(gè)可以實(shí)現(xiàn)的最簡(jiǎn)單的片段著色器:一個(gè)直通濾鏡:
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
這個(gè)著色器實(shí)際上不會(huì)改變圖像中的任何東西。它是一個(gè)直通著色器,意味著我們輸入每一個(gè)像素,然后輸出完全相同的像素。我們來(lái)一句句的看:
varying highp vec2 textureCoordinate;
因?yàn)槠沃髯饔迷诿恳粋€(gè)像素上,我們需要一個(gè)方法來(lái)確定我們當(dāng)前在分析哪一個(gè)像素/片段。它需要存儲(chǔ)像素的 X 和 Y 坐標(biāo)。我們接收到的是當(dāng)前在頂點(diǎn)著色器被設(shè)置好的紋理坐標(biāo)。
uniform sampler2D inputImageTexture;
為了處理圖像,我們從應(yīng)用中接收一個(gè)圖片的引用,我們把它當(dāng)做一個(gè) 2D 的紋理。這個(gè)數(shù)據(jù)類型被叫做 sampler2D ,這是因?yàn)槲覀円獜倪@個(gè) 2D 紋理中采樣出一個(gè)點(diǎn)來(lái)進(jìn)行處理。
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
這是我們碰到的第一個(gè) GLSL 特有的方法:texture2D,顧名思義,創(chuàng)建一個(gè) 2D 的紋理。它采用我們之前聲明過(guò)的屬性作為參數(shù)來(lái)決定被處理的像素的顏色。這個(gè)顏色然后被設(shè)置給另外一個(gè)內(nèi)建變量,gl_FragColor。因?yàn)槠沃鞯奈ㄒ荒康木褪谴_定一個(gè)像素的顏色,gl_FragColor 本質(zhì)上就是我們片段著色器的返回語(yǔ)句。一旦這個(gè)片段的顏色被設(shè)置,接下來(lái)片段著色器就不需要再做其他任何事情了,所以你在這之后寫任何的語(yǔ)句,都不會(huì)被執(zhí)行。
就像你看到的那樣,寫著色器很大一部分就是了解著色語(yǔ)言。即使著色語(yǔ)言是基于 C 語(yǔ)言的,依然有很多怪異和細(xì)微的差別讓它和普通的 C 語(yǔ)言有不同。
輸入,輸出,以及精度修飾 (Precision Qualifiers)
看一看我們的直通著色器,你會(huì)注意到有一個(gè)屬性被標(biāo)記為 “varying”,另一個(gè)屬性被標(biāo)記為 “uniform”。
這些變量是 GLSL 中的輸入和輸出。它允許從我們應(yīng)用的輸入,以及在頂點(diǎn)著色器和片段著色器之間進(jìn)行交流。
在 GLSL 中,實(shí)際有三種標(biāo)簽可以賦值給我們的變量:
- Uniforms
- Attributes
- Varyings
再一次聲明, Varyings 已經(jīng)作廢, 請(qǐng)使用 in out關(guān)鍵字.
Uniforms 是一種外界和你的著色器交流的方式。Uniforms 是為在一個(gè)渲染循環(huán)里不變的輸入值設(shè)計(jì)的。如果你正在應(yīng)用茶色濾鏡,并且你已經(jīng)指定了濾鏡的強(qiáng)度,那么這些就是在渲染過(guò)程中不需要改變的事情,你可以把它作為 Uniform 輸入。 Uniform 在頂點(diǎn)著色器和片段著色器里都可以被訪問到。
Attributes 僅僅可以在頂點(diǎn)著色器中被訪問。Attribute 是在隨著每一個(gè)頂點(diǎn)不同而會(huì)發(fā)生變動(dòng)的輸入值,例如頂點(diǎn)的位置和紋理坐標(biāo)等。頂點(diǎn)著色器利用這些變量來(lái)計(jì)算位置,以它們?yōu)榛A(chǔ)計(jì)算一些值,然后把這些值以 varyings 的方式傳到片段著色器。
最后,但同樣重要的,是 varyings 標(biāo)簽。Varying 在頂點(diǎn)著色器和片段著色器都會(huì)出現(xiàn)。Varying 是用來(lái)在頂點(diǎn)著色器和片段著色器傳遞信息的,并且在頂點(diǎn)著色器和片段著色器中必須有匹配的名字。數(shù)值在頂點(diǎn)著色器被寫入到 varying ,然后在片段著色器被讀出。被寫入 varying 中的值,在片段著色器中會(huì)被以插值的形式插入到兩個(gè)頂點(diǎn)直接的各個(gè)像素中去。
回看我們之前寫的簡(jiǎn)單的著色器的例子,在頂點(diǎn)著色器和片段著色器中都用 varying 聲明了 textureCoordinate。我們?cè)陧旤c(diǎn)著色器中寫入 varying 的值。然后我們把它傳入片段著色器,并在片段著色器中讀取和處理。
在我們繼續(xù)之前,最后一件要注意的事??纯磩?chuàng)建的這些變量。你會(huì)注意到紋理坐標(biāo)有一個(gè)叫做 highp 的屬性。這個(gè)屬性負(fù)責(zé)設(shè)置你需要的變量精度。因?yàn)?OpenGL ES 被設(shè)計(jì)為在處理能力有限的系統(tǒng)中使用,精度限制被加入進(jìn)來(lái)可以提高效率。
如果不需要非常高的精度,你可以進(jìn)行設(shè)定,這或許會(huì)允許在一個(gè)時(shí)鐘循環(huán)內(nèi)處理更多的值。相反的,在紋理坐標(biāo)中,我們需要盡可能的確保精確,所以我們具體說(shuō)明確實(shí)需要額外的精度。
精度修飾存在于 OpenGL ES 中,因?yàn)樗潜辉O(shè)計(jì)用在移動(dòng)設(shè)備中的。但是,在老版本的桌面版的 OpenGL 中則沒有。因?yàn)?OpenGL ES 實(shí)際上是 OpenGL 的子集,你幾乎總是可以直接把 OpenGL ES 的項(xiàng)目移植到 OpenGL。如果你這樣做,記住一定要在你的桌面版著色器中去掉精度修飾。這是很重要的一件事,尤其是當(dāng)你計(jì)劃在 iOS 和 OS X 之間移植項(xiàng)目時(shí)。
向量
在 GLSL 中,你會(huì)用到很多向量和向量類型。向量是一個(gè)很棘手的話題,它們表面上看起來(lái)很直觀,但是因?yàn)樗鼈冇泻芏嘤猛荆@使我們?cè)谑褂盟鼈儠r(shí)常常會(huì)感到迷惑。
在 GLSL 環(huán)境中,向量是一個(gè)類似數(shù)組的特殊的數(shù)據(jù)類型。每一種類型都有固定的可以保存的元素。深入研究一下,你甚至可以獲得數(shù)組可以存儲(chǔ)的數(shù)值的精確的類型。但是在大多數(shù)情況下,只要使用通用的向量類型就足夠了。
有三種向量類型你會(huì)經(jīng)常看到:
- vec2
- vec3
- vec4
這些向量類型包含特定數(shù)量的浮點(diǎn)數(shù):vec2 包含兩個(gè)浮點(diǎn)數(shù),vec3 包含三個(gè)浮點(diǎn)數(shù),vec4 包含四個(gè)浮點(diǎn)數(shù)。
這些類型可以被用在著色器中可能被改變或者持有的多種數(shù)據(jù)類型中。在片段著色器中,很明顯 X 和 Y 坐標(biāo)是的你想保存的信息。 (X,Y) 存儲(chǔ)在 vec2 中就很合適。
在圖像處理過(guò)程中,另一個(gè)你可能想持續(xù)追蹤的事情就是每個(gè)像素的 R,G,B,A 值。這些可以被存儲(chǔ)在 vec4 中。
矩陣
現(xiàn)在我們已經(jīng)了解了向量,接下來(lái)繼續(xù)了解矩陣。矩陣和向量很相似,但是它們添加了額外一層的復(fù)雜度。矩陣是一個(gè)浮點(diǎn)數(shù)數(shù)組的數(shù)組,而不是單個(gè)的簡(jiǎn)單浮點(diǎn)數(shù)數(shù)組。
類似于向量,你將會(huì)經(jīng)常處理的矩陣對(duì)象是:
- mat2
- mat3
- mat4
vec2 保存兩個(gè)浮點(diǎn)數(shù),mat 保存相當(dāng)于兩個(gè) vec2 對(duì)象的值。將向量對(duì)象傳遞到矩陣對(duì)象并不是必須的,只需要有足夠填充矩陣的浮點(diǎn)數(shù)即可。在 mat2 中,你需要傳入兩個(gè) vec2 或者四個(gè)浮點(diǎn)數(shù)。因?yàn)槟憧梢越o向量命名,而且相比于直接傳浮點(diǎn)數(shù),你只需要負(fù)責(zé)兩個(gè)對(duì)象,而不是四個(gè),所以非常推薦使用封裝好的值來(lái)存儲(chǔ)你的數(shù)字,這樣更利于追蹤。對(duì)于 mat4 會(huì)更復(fù)雜一些,因?yàn)槟阋?fù)責(zé) 16 個(gè)數(shù)字,而不是 4 個(gè)。
在我們 mat2 的例子中,我們有兩個(gè) vec2 對(duì)象。每個(gè) vec2 對(duì)象代表一行。每個(gè) vec2 對(duì)象的第一個(gè)元素代表一列。構(gòu)建你的矩陣對(duì)象的時(shí)候,確保每個(gè)值都放在了正確的行和列上是很重要的,否則使用它們進(jìn)行運(yùn)算肯定得不到正確的結(jié)果。
既然我們有了矩陣也有了填充矩陣的向量,問題來(lái)了:“我們要用它們做什么呢?“ 我們可以存儲(chǔ)點(diǎn)和顏色或者其他的一些的信息,但是要如果通過(guò)修改它們來(lái)做一些很酷的事情呢?
向量和矩陣運(yùn)算,也就是初等線性代數(shù)
我找到的最好的關(guān)于線性代數(shù)和矩陣是如何工作的資源是這個(gè)網(wǎng)站的更好的解釋。我從這個(gè)網(wǎng)站偷來(lái)借鑒的一句引述就是:
線性代數(shù)課程的幸存者都成為了物理學(xué)家,圖形程序員或者其他的受虐狂。
矩陣操作總體來(lái)說(shuō)并不“難”;只不過(guò)它們沒有被任何上下文解釋,所以很難概念化地理解究竟為什么會(huì)有人想要和它們打交道。我希望能在給出一些它們?cè)趫D形編程中的應(yīng)用背景后,我們可以了解它們?cè)鯓訋椭覀儗?shí)現(xiàn)不可思議的東西。
線性代數(shù)允許你一次在很多值上進(jìn)行操作。假想你有一組數(shù),你想要每一個(gè)數(shù)乘以 2。你一般會(huì)一個(gè)個(gè)地順次計(jì)算數(shù)值。但是因?yàn)閷?duì)每一個(gè)數(shù)都進(jìn)行的是同樣的操作,所以你完全可以并行地實(shí)現(xiàn)這個(gè)操作。
我們舉一個(gè)看起來(lái)可怕的例子,CGAffineTransforms。仿射轉(zhuǎn)化是很簡(jiǎn)單的操作,它可以改變具有平行邊的形狀 (比如正方形或者矩形) 的大小,位置,或者旋轉(zhuǎn)角度。
在這種時(shí)候你當(dāng)然可以坐下來(lái)拿出筆和紙,自己去計(jì)算這些轉(zhuǎn)化,但這么做其實(shí)沒什么意義。GLSL 有很多內(nèi)建的函數(shù)來(lái)進(jìn)行這些龐雜的用來(lái)計(jì)算轉(zhuǎn)換的函數(shù)。了解這些函數(shù)背后的思想才是最重要的。
GLSL 特有函數(shù)
這篇文章中,我們不會(huì)把所有的 GLSL 內(nèi)建的函數(shù)都過(guò)一遍,不過(guò)你可以在 Shaderific 上找到很好的相關(guān)資源。很多 GLSL 函數(shù)都是從 C 語(yǔ)言數(shù)學(xué)庫(kù)中的基本的數(shù)學(xué)運(yùn)算導(dǎo)出的,所以解釋 sin 函數(shù)是做什么的真的是浪費(fèi)時(shí)間。我們將集中闡釋一些更深?yuàn)W的函數(shù),從而達(dá)到這篇文章的目的,解釋怎樣才能充分利用 GPU 的性能的一些細(xì)節(jié)。
step(): GPU 有一個(gè)局限性,它并不能很好的處理?xiàng)l件邏輯。GPU 喜歡做的事情是接受一系列的操作,并將它們作用在所有的東西上。分支會(huì)在片段著色器上導(dǎo)致明顯的性能下降,在移動(dòng)設(shè)備上尤其明顯。step() 通過(guò)允許在不產(chǎn)生分支的前提下實(shí)現(xiàn)條件邏輯,從而在某種程度上可以緩解這種局限性。如果傳進(jìn) step() 函數(shù)的值小于閾值,step() 會(huì)返回 0.0。如果大于或等于閾值,則會(huì)返回 1.0。通過(guò)把這個(gè)結(jié)果和你的著色器的值相乘,著色器的值就可以被使用或者忽略,而不用使用 if() 語(yǔ)句。
mix(): mix 函數(shù)將兩個(gè)值 (例如顏色值) 混合為一個(gè)變量。如果我們有紅和綠兩個(gè)顏色,我們可以用 mix() 函數(shù)線性插值。這在圖像處理中很常用,比如在應(yīng)用程序中通過(guò)一組獨(dú)特的設(shè)定來(lái)控制效果的強(qiáng)度等。
*clamp(): GLSL 中一個(gè)比較一致的方面就是它喜歡使用歸一化的坐標(biāo)。它希望收到的顏色分量或者紋理坐標(biāo)的值在 0.0 和 1.0 之間。為了保證我們的值不會(huì)超出這個(gè)非常窄的區(qū)域,我們可以使用 clamp() 函數(shù)。 clamp() 會(huì)檢查并確保你的值在 0.0 和 1.0 之間。如果你的值小于 0.0,它會(huì)把值設(shè)為 0.0。這樣做是為了防止一些常見的錯(cuò)誤,例如當(dāng)你進(jìn)行計(jì)算時(shí)意外的傳入了一個(gè)負(fù)數(shù),或者其他的完全超出了算式范圍的值。
更復(fù)雜的著色器的例子
我知道數(shù)學(xué)的洪水一定讓你快被淹沒了。如果你還能跟上我,我想舉幾個(gè)優(yōu)美的著色器的例子,這會(huì)更有意義,這樣你又有機(jī)會(huì)淹沒在 GLSL 的潮水中。
飽和度調(diào)整

這是一個(gè)做飽和度調(diào)節(jié)的片段著色器。這個(gè)著色器出自 《圖形著色器:理論和實(shí)踐》一書,我強(qiáng)烈推薦整本書給所有對(duì)著色器感興趣的人。
飽和度是用來(lái)表示顏色的亮度和強(qiáng)度的術(shù)語(yǔ)。一件亮紅色的毛衣的飽和度要遠(yuǎn)比北京霧霾時(shí)灰色的天空的飽和度高得多。
在這個(gè)著色器上,參照人類對(duì)顏色和亮度的感知過(guò)程,我們有一些優(yōu)化可以使用。一般而言,人類對(duì)亮度要比對(duì)顏色敏感的多。這么多年來(lái),壓縮軟件體積的一個(gè)優(yōu)化方式就是減少存儲(chǔ)顏色所用的內(nèi)存。
人類不僅對(duì)亮度比顏色要敏感,同樣亮度下,我們對(duì)某些特定的顏色反應(yīng)也更加靈敏,尤其是綠色。這意味著,當(dāng)你尋找壓縮圖片的方式,或者以某種方式改變它們的亮度和顏色的時(shí)候,多放一些注意力在綠色光譜上是很重要的,因?yàn)槲覀儗?duì)它最為敏感。
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);
}
我們一行行的看這個(gè)片段著色器的代碼:
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp float saturation;
再一次,因?yàn)檫@是一個(gè)要和基礎(chǔ)的頂點(diǎn)著色器通信的片段著色器,我們需要為輸入紋理坐標(biāo)和輸入圖片紋理聲明一個(gè) varyings 變量,這樣才能接收到我們需要的信息,并進(jìn)行過(guò)濾處理。這個(gè)例子中我們有一個(gè)新的 uniform 的變量需要處理,那就是飽和度。飽和度的數(shù)值是一個(gè)我們從用戶界面設(shè)置的參數(shù)。我們需要知道用戶需要多少飽和度,從而展示正確的顏色數(shù)量。
const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
這就是我們?cè)O(shè)置三個(gè)元素的向量,為我們的亮度來(lái)保存顏色比重的地方。這三個(gè)值加起來(lái)要為 1,這樣我們才能把亮度計(jì)算為 0.0 – 1.0 之間的值。注意中間的值,就是表示綠色的值,用了 70% 的顏色比重,而藍(lán)色只用了它的 10%。藍(lán)色對(duì)我們的展示不是很好,把更多權(quán)重放在綠色上是很有意義的。
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
我們需要取樣特定像素在我們圖片/紋理中的具體坐標(biāo)來(lái)獲取顏色信息。我們將會(huì)改變它一點(diǎn)點(diǎn),而不是想直通濾鏡那樣直接返回。
lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
這行代碼會(huì)讓那些沒有學(xué)過(guò)線性代數(shù)或者很早以前在學(xué)校學(xué)過(guò)但是很少用過(guò)的人看起來(lái)不那么熟悉。我們是在使用 GLSL 中的點(diǎn)乘運(yùn)算。如果你記得在學(xué)校里曾用過(guò)點(diǎn)運(yùn)算符來(lái)相乘兩個(gè)數(shù)字的話,那么你就能明白是什么回事兒了。點(diǎn)乘計(jì)算以包含紋理顏色信息的vec4 為參數(shù),舍棄 vec4 的最后一個(gè)不需要的元素,將它和相對(duì)應(yīng)的亮度權(quán)重相乘。然后取出所有的三個(gè)值把它們加在一起,計(jì)算出這個(gè)像素綜合的亮度值。
lowp vec3 greyScaleColor = vec3(luminance);
為什么又聲明了一個(gè)vec3變量, 是為了后面調(diào)用mix函數(shù)時(shí)必須提供一個(gè)vec3的變量.
mix(greyScaleColor, textureColor.rgb, saturation)
關(guān)于mix()函數(shù)的簽名: anyFLoat mix(anyFLoat x,anyFloat y,anyFloat a) 返回x和y的線性混合,a從0到1變化
最后,我們把所有的片段組合起來(lái)。為了確定每個(gè)新的顏色是什么,我們使用剛剛學(xué)過(guò)的很好用的 mix 函數(shù)。mix 函數(shù)會(huì)把我們剛剛計(jì)算的灰度值和初始的紋理顏色以及我們得到的飽和度的信息相結(jié)合。
這就是一個(gè)很棒的,好用的著色器,它讓你用主函數(shù)里的四行代碼就可以把圖片從彩色變到灰色,或者從灰色變到彩色。還不錯(cuò),不是嗎?
我仍然不理解最后一句:gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w); 最后的那個(gè)textureColor.w是哪來(lái)的, 如果按照xyzw的方式標(biāo)記這個(gè)vec4變量, 那前面就不改使用rgb了, 如果前面使用了rgb這里不應(yīng)該使用a嗎?