为新手准备的 Codea 着色器(Shader)教程
原文標題:《Shaders for dummies》?
作者:Ignatz?
譯者:FreeBlues?
譯文鏈接:http://my.oschina.net/freeblues/blog/336055?
PDF鏈接:http://pan.baidu.com/s/1c0xTUzI
概述
-
譯者注:
1、Codea?是一款可以在?Ipad?上直接編寫游戲的?APP?應用軟件,它使用?Lua?的語法和庫,并針對?iPad?提供了一系列函數,諸如繪圖、觸摸、攝像頭、重力感應乃至網絡等等,Codea?支持?OpenGL ES,它編寫的程序可以直接在?iPad?上運行,也可以導出為?Xcode?項目,再用?Xcode?編譯為可發布在?App Store?的應用程序。
2、本教程講述的內容適用于?Codea?環境,跟?OpenGL ES?在其他開發環境的使用會有一些不同。
Codea?建構于?OpenGL ES Shading Language(開放圖形庫 嵌入式系統 渲染語言)之上,它(OpenGL)提供了非常復雜的工具,以及一連串復雜處理,用來把像素繪制到屏幕上。
在這一系列處理中有兩個步驟:Vertex Shader(頂點著色) 和?Fragment Shader(片段著色),Codea?允許我們把這兩者混合在一起來使用。為什么應該為此而興奮?因為它給了你訪問底層圖形的能力,允許你創建非常強有力的視覺效果。
頂點著色器 –Vertex Shader
頂點著色器Vertex Shader?允許你一次修改一個頂點(一個頂點就是一個三角形的一個角,記住在計算機中所有的圖形都是由三角形組成的)
譯者注:如下所示:
片段著色器 –Fragment shaders
片段著色器Fragment shaders?允許你一次修改一個像素點的顏色(以及紋理貼圖的坐標位置)。
這些聽起來都很復雜,不過別被嚇跑。
著色器Shader?聽起來非常神秘和困難,但是實際上它們并非那樣的。
這個教程盡量不使用專業術語,也盡量不使用矩陣。不需要太多的數學。大多數是一些簡單的例子。
不過有一個警告:它并不是為所有的初學者準備的。
如果你不滿足下面這些描述就別再往后看了:
-
編程讓你感覺很輕松舒服
-
熟悉?Codea?中的?mesh(畫刷) 和?image texture(圖形的紋理貼圖),并且–>
-
準備好學習一丁點?C?語言(我保證只要一丁點!)
-
返回目錄
著色器是什么 –What are shaders?
我讀過一些關于著色器是什么的解釋,它們談到了?pipelines(管道)、vectors(向量)、rasterisation(圖形柵格化)、scissor tests(剪切測試),以及一些復雜的示意圖。這種討論直接讓我遠離了對著色器?的學習好幾個月。
我確信,像我一樣,你也會喜歡真正簡單明了的解釋,沒有任何上述的險惡術語。
管道 –Pipes
OpenGL?就像一個長長的管道。你從這一頭把你的繪圖指令(如sprite,mesh等)放進去,像素點會從另一頭出來,這些像素點會在你的屏幕上形成一幅 3D 圖像。在管道內部是一系列復雜處理,我們不會假裝理解。
因此讓我們聚焦到管道外。
在管道上有兩個位置被挖了兩個洞,因此你能通過這兩個洞看到管道里流過的信息,并且你還可以進到里面去修改這些信息,這些信息處于整個體系的最底層,下圖是細節:
在?OpenGL?管道上的兩個洞允許你改變里面的信息流,不過它們假定你知道自己在做什么,并且在這里寫的代碼不像?Codea?中那么簡單和寬容–任何錯誤都可能會簡單地導致你的屏幕一片空白。你無法做出任何打斷。
無論如何我們都會大膽地偷窺這兩個洞。不過首先,我們得了解更多關于這兩個洞里的信息流在干什么,這樣我們才能理解我們在洞里看到的。
從頂點到像素點 –Vertexes to pixels
在制作動畫片時,熟練的藝術家不會畫出每一個單獨幀。他們繪制關鍵幀,然后讓其他人(或者現在是計算機)去填充位于關鍵幀之間的中間幀,這個處理被稱為?tweening。
類似地,在 2D 和 3D 圖形處理中,我們必須指出我們所畫的位置和顏色,不過我們只需要對一些點的樣本集合(譯者注:類似于關鍵幀)進行操作,這些點被稱為?vertex(頂點)。實際上,我們創造了一個線框模型。
OpenGL?接著會添加插入這些樣本點之間的所有點。具體的方法是:把這些點組成三角形-因為這是最簡單的形狀,因此它用三個角的頂點值來計算三角形里所有點的值。
就像上圖一樣。看看紅色角、綠色角和藍色角的顏色是如何在三角形內部混合起來的。它確實很簡單。
并且這種方法不僅被應用在 3D 上,也被應用在 2D 上,并且不僅被用于 mesh,也被用于?sprite,因為?sprite?實際是以?mesh?為基礎的。
因此,Codea?中所有的一切都是由?mesh、三角形、頂點?繪制而成的。
OpenGL?需要知道每個頂點的?x,y,z?位置坐標,以及它的顏色?- 或者,假如你把一個紋理圖形?粘貼在線框模型上時,圖形的哪一部分會被繪制在這個頂點上。
所以,每個頂點都有三條關鍵信息:
- 頂點的?x,y,z?位置坐標
- 顏色(如果你設置過)
- 紋理映射(例如紋理貼圖中的哪一個?x,y?點被用于這個頂點)
OpenGL?然后就能插入這些信息用來計算三角形內部的每一個點的位置和顏色。
OpenGL?做了其他一大堆非常復雜、名字很長的事情,當然,我們所關注的僅僅是我們所提供的頂點集合的信息,以及?OpenGL?在屏幕上向這些頂點中插入的像素點和像素點的顏色。
因此,繼續:
- OpenGL?要你為你的?mesh?定義一組三角形
- 每個三角形都有三個頂角,或者說頂點
- 每個頂點有一個位置坐標、顏色,和(如果你正把一個紋理貼圖鋪展在你的?mesh?上面)一個(x,y)?值,用來描述紋理貼圖的哪一部分將會被繪制在這個頂點上
- OpenGL?接著會通過在頂點(頂角)值之間插值的辦法 在每個三角形的內部繪制出所有的點。
回到那個管道的洞上:
- 返回目錄
頂點著色器 –Vertex Shader
管道上的一個洞位于信息流中?mesh?被分離為獨立頂點的地方,并且每個頂點的全部信息都被收集在一起。OpenGL?正要插入位于三角形頂點之間的所有像素點(譯者注:也就是在幾個頂點坐標值的區間內進行插值)。
不過首先,我們獲得一次跟這些頂點玩耍的機會。
當我們通過這個洞向管道里看時,我們僅僅看到一個單獨的頂點。正如我所說,我們在這里工作于一個系統底層。頂點知道它的?x,y,z?位置坐標值,一個顏色值(如果你已經設置了一個),以及它在紋理貼圖上的位置坐標,除了這些就沒有更多的了。
我也說過我們只看到一個?vertex(頂點)。其他所有的頂點到哪里去了?好了,備份管道的某些地方是一個循環處理,一次只讓全部頂點的一個通過,并且把一個頂點發送到管道里去。因此?vertex?代碼將會為每個頂點獨立運行。(譯者注:也就是說處理?vertex?的代碼一次只處理一個頂點,處理所有頂點的循環由整個管道來實現,我們在寫代碼時按照一個頂點的處理邏輯寫就可以了)。
在這個洞中已經有了一些代碼,不過所有這些代碼看起來好像只是取得這些信息的一部分,并把它們不做任何改變地傳遞給其他變量,這些看起來都是相當不得要領的(譯者注:不容易理解)。
事實上,這些代碼正如下面所寫:
vColor = color; vTexCoord = texCoord; gl_Position = modelViewProjection * position;這句代碼?vColor = color;?是什么意思?
我猜測軟件開發者在說:
我們將會在一個名為?color?的輸入變量中,給你們每個頂點的顏色,你們可以對它做任何事情,然后把結果放在一個名為vColor?的輸出變量中,如果你們不打算改變這個頂點的顏色,那就讓那行代碼呆著別動好了。
同樣的事情發生在頂點位置和紋理映射坐標上。因此你能取得頂點數據(譯者注:包括顏色、頂點位置坐標、紋理映射坐標),編寫代碼篡改它們,然后把結果傳遞出去。
譯者注:簡單說就是,上述代碼中賦值號?=?右側的部分是由?Codea?自動傳遞進來到這個處理階段的輸入變量,?color?是頂點顏色,?position?是頂點位置坐標,texCoord?是紋理映射坐標;賦值號左側的部分就是準備由這個處理階段傳遞給下一道工序的輸出變量。
你放在這里的代碼就被稱為一個?vertex shader(頂點著色器)。
你打算如何來改變一個頂點?好,一個頂點主要跟位置坐標相關,因此,例如你可以制作一幅鏡像圖形(比如在?x?軸上翻轉)通過把?x?坐標翻過來(譯者注:上下翻轉,想象一下水中的倒影),這樣圖形的右手側就會被畫到左手側,反之亦然。或者你也可以創造一個爆炸物體,通過讓?x,y,z?坐標以一種特定路徑在一系列幀上飛離。
限制:
當你從事編寫?頂點著色器-vertex shader?代碼時,有很多限制:
-
你的代碼一次處理一個頂點,并且它沒有太多相關信息,僅僅只能影響到這個頂點。所以它不知道相鄰頂點的任何信息,例如–除非你傳遞額外的信息進去,它才可能知道(我們很快就會提到)。
-
這些代碼用它們自己的語言來編寫,基于?C,沒有很多函數可供使用。
-
如果有一個錯誤,你很可能會得到一塊空白的屏幕 – 或者混亂的屏幕,這會給調試工作帶來一些阻撓(盡管至少你無法破壞掉些什么,失敗是完全安全的)。Codea?有一個內建的?Shader Lab(著色器實驗室),它會顯示語法錯誤信息,作為一點幫助。
不過我們隨后將會返回上述全部這些,我剛剛意識到每一樣仍然有些混淆。
先在這里掛起,不久將會更清楚。
- 返回目錄
片段著色器 –Fragment Shaders
管道上的第二個洞位于這個位置,在這里?mesh?中的每個頂點的頂點信息已經被完成插值。
因此,所有這些在到達我們這個洞之前都已經發生了。向里看,我們看到一個單獨的像素點,比如,不論我們在這里放什么代碼,它都會為?mesh?中的每一個像素點而運行。
再一次,這里已經有代碼存在。并且所有這些代碼所做的,是取得插值顏色和紋理坐標位置,并且用它們指出應用到像素點上的顏色。這只會帶來兩行代碼。
lowp vec4 col = texture2D(texture, vTexCoord) * vColor ; gl_FragColor = col;乍看起來有點奇怪,不過看啊看啊你就習慣了。
命令?texture2D?相當于 Codea 中的?myImage:get(x,y),它取得紋理貼圖上面一個像素點的顏色,這個像素點位于x,y,由?vTexCoord?指定,最后把這個像素點的顏色放到一個名為?col?的變量中。
而且,如果你已經為頂點設置了顏色,它將會在這里應用那個插值顏色(vColor)。至于現在,還不必去擔心為什么會用顏色來相差。
第二行簡單地把顏色?col?賦值給一個名為?gl_FragColor?的東西。
因此再一次地,這段代碼沒有干太多事。不過,正如?頂點著色器-vertax shader?一樣,如果我們想,我們可以對像素點的顏色進行混合。于是結果就是我們可以通過各種有趣的方式來做這件事。事實上,幾乎所有?Codea?內建的著色器都是這種類型。
接著我們為這個洞編寫的任何代碼都被稱為?片段著色器-fragment shader?(fragment-片段?只是像素點-pixels的另一個叫法)。
因此:
- 頂點著色器-Vertex Shader?影響獨立的頂點們
- 片段著色器-Fragment Shader?影響獨立的像素點們
在關于它們是如何做的這一點上,將仍然是一個完全的秘密,不過我會給你一些例程來幫助你理解。
- 返回目錄
頂點著色器 –Vertex Shaders
現在我們看看基本的?頂點著色器-Vertex shader,并且學習一點?shader language(著色語言)。
我不能一直談論管道。某些時候,我不得不給你看一些真正的代碼并且解釋它們。不過我不會給出一個關于著色語言的課程。我只會簡單地解釋說那是什么,僅僅是你工作所需要知道的最少的那些原材料。
我想要從?shader lab?里開始。想找到它,進入你選擇項目的?Codea?主界面,點擊屏幕左上角的那個方形和箭頭的圖標,你就會發現?shader lab。選擇它,并且點擊?Documents,然后選擇?Create New Shader,給它起個名字。
現在你就可以看這些代碼了,在標簽頁?vertex:
// // A basic vertex shader // //This is the current model * view * projection matrix // Codea sets it automatically uniform mat4 modelViewProjection;//This is the current mesh vertex position, color and tex coord // Set automatically attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;//This is an output variable that will be passed to the fragment shader varying lowp vec4 vColor; varying highp vec2 vTexCoord;void main() {//Pass the mesh color to the fragment shader vColor = color;vTexCoord = texCoord;//Multiply the vertex position by our combined transformgl_Position = modelViewProjection * position; }這里有很多代碼,不過只有它們中的三部分完成所有工作!
因此現在,我將快速解釋你在哪里看到的一些奇怪的東西(譯者注:這些只是?C?語言的基本語法)。
- 注釋行以?//?為前綴,而不是 Codea 中的?--
- 每行代碼都要以分號?;?結束
- 有一個?main?函數,由?{}?包圍著,就像?Codea?中的?setup?函數
- 不過這里的?main?函數不像?Codea?一樣以?function?為前綴
- 位于?main?前面的?void?僅僅意味著當它執行時不會返回任何值
如果我們在?頂點著色器-vertex shader?中改動任何地方,它將大半落在?main?函數里。
現在如果你回顧上述?main?函數中的所有代碼行,(你會發現)這些代碼行定義了我們將會在代碼中用到的全部的輸入和輸出變量。你必須在使用它們之前先定義它們。
每一行最后一個詞是變量名,那么所有這些前綴–uniform, attributes, varying, lowp, highp, mat4, vec2, and vec4?又是什么呢?
不必擔心,它們都是合乎邏輯的。這些前綴告訴?OpenGL?三件事:
1、Precision(小數的位數)– 精度
有三個選項,highp, mediump, lowp,你可以猜測它們的含義,如果你沒有指定一個,默認值是?highp。就現在而言,所有這些你都可以完全忽略,因為我們要做的任何事都不需要特別的精度。
2、變量的數據類型
如果你編寫過其他程序,你會習慣于指出一個變量是否是一個整數,一個帶有小數的數,一個字符串,一個數組等等。Codea?自己計算出絕大部分數據類型而斷送掉我們親自計算的機會。OpenGL?需要我們確切地定義變量,不過它們都是相當明顯的。
- vec2?=?Codea?中的?vec2,如?vec2(3,4),還有?vec3?和?vec4
- bool?=?boolean(true 或 false) 布爾型,真值或假值
- int?=?integer?整型
- float?= 帶小數的數 浮點型
- sampler2D?= 一個 2D 圖像
- mat2?=?2*2?的表(mat3?和?mat4?分別是?3*3?和?4*4?的表)
因此你必須在你添加的任何變量前面包括其中任意一種類型
3、這些變量用來做什么?
OpenGL?需要知道你這些變量是拿來做什么用的。一個出現在這個著色器中的變量有三個可能的原因。
(a)attribute?- 是一個輸入,提供關于這個特定頂點的信息,比如,它的值對于每個頂點都是不同的。明顯的例子就是位置坐標,顏色和紋理坐標,并且你將會在上述代碼中看到它們全部。這些輸入由?Codea?自動為每一個頂點提供。
(b)uniform?- 也是一個輸入,不過對于每個頂點來說沒有變化。例如,Codea?中的?blend shader(譯者注:可以在著色實驗室找到這個著色器)定義了第二幅圖像用來跟通常的?mesh?紋理貼圖圖像進行混合,并且這幅相同的圖像將會被用于所有的頂點,因此它是?uniform-統一的。在標準代碼中只有一個?uniform?-?modelViewProjection?- 而且我們現在不會討論它,因為它是 3D 黑盒子的一部分。
(c)varying?- 這些是輸出,將被用于插值獨立像素點,還將會在?片段著色器-fragment shader?中可用。這里是它們中的兩個?vColor?和?vTexCoord,你可以添加更多的。
讓我們再次總結一下:
- attribute?- 輸入 為每一個頂點輸入一個不同的值,如?position
- uniform?- 輸入 對于所有頂點都是相同的,如第二幅圖像
- varying?- 輸出 將會被提供給?片段著色器-fragment shader?使用
因此,讓我們看看下面這一些變量,看看能否指出它們的定義。
attribute vec4 color;變量?color?是一個?vec4(r,g,b,a)(譯者注:紅,綠,藍,透明率) 和一個?attribute,這意味著它是一個輸入,并且對于每個頂點都不同,這正是我們所期待的。
attribute vec2 texCoord;變量?texCoord?是一個?vec2?以及一個?attribute(因此它對于每個頂點都不同),我們可以根據它的名字來猜測:它保留了應用于這個點的紋理貼圖的坐標位置。
varying highp vec2 vTexCoord;變量?vTexCoord?是一個高精度的?vec2,它還是一個?varying,這意味著它是一個輸出,因此它將會被插值到每個像素點,并且發送給?片段著色器-fragment shader。你可以從 main 函數中的代碼看到,vTexCoord = texCoord,因此所有這些代碼所做的就是傳遞貼圖的位置坐標給?片段著色器-fragment shader。
因此我們回到所有這個著色器所做的事實,它取得位置坐標,紋理和顏色信息(來自?attribute?輸入變量),然后把它們未做改動地賦值給輸出(varying)變量.
基本上,它什么也沒做(除了一個神秘的矩陣相乘)。
現在該由我們來改變它了。
- 返回目錄
改變頂點著色器 –Changing the vertex shader
是時候來改變那個?頂點著色器-vertex shader?了。這也正是它存在的意義。
首先,我想分享關于用一種你一無所知的語言編寫代碼時的我的規則
不論何地,盡可能地,竊取一行已經存在的能工作的代碼
(譯者注:大意是,對于一門陌生的語言,盡量參考引用別人寫好的完善的代碼)
這將會有點困難,當我們被給了這么少的幾行代碼作為開始時,不過?Shader Lab?包含了大約 15 個著色器的代碼,并且其中不少代碼我們都可以偷來(以及研究)用。
翻轉圖像 –Flipping a image
首先,讓我們試著翻轉一幅圖像,這樣我們就會得到一個鏡像圖像。在?Shader Lab?中你自己定制的 著色器中嘗試。我們的目標是把?Codea?的?Logo?變成一個鏡像圖像。
翻轉圖像最簡單的辦法是改變紋理貼圖的所有坐標,這樣?OpenGL?就會由從右到左繪制換成從左到右繪制。你應該記得紋理貼圖的位置坐標是介于?0 到 1?之間的分數,0 位于左邊(或者底部),1 位于右邊(或者頂部)。如果我們用 1 減去x?值,我們將會得到想要的結果,因為位置(0,0)(左下角)會被改變為(1,0)(右下角),反之亦然。
因此,讓我們看看?頂點著色器-vertex shader?中?main?的內部,這就是我們要改的那一行
vTexCoord=texCoord;我們只想翻轉?x?值,因此改動如下:
texCoord.x = 1 - texCoord.x; //change the x value vTexCoord = texCoord;好了,你已經犯了兩個錯誤。一個是?texCoord?是一個輸入,它不能被改動。另一個是?texCoord?包含分數(浮點)值,不能跟整數混合使用,因此你應該用?1.0 或 1. 而不是?1
這是一個真正的”我抓到你了“的小圈套來愚弄你(它仍然得到我的一絲不茍),所以,盡量記住這個玩笑中的兩個錯誤。
任何定義為?float?的變量在跟其他數字一起計算時,該數字必須包含一個小數點,所以換掉?d = 1,你應該說?d = 1.0或者僅僅是?d = 1.?,否則它就不會工作。
所以我們換一個:
vTexCoord = vec2(1.-texCoord.x,texCoord.y);這句代碼定義了一個新的?vec2(正是?vTexCoord?想要的),并且把它賦值為?1-x?的值和?y?的值。
它生效了,并且應該在?Shader Lab?把?Logo?翻轉為一個鏡像圖像。
現在來看看你能否用相同的方法翻轉?y?值。。。
你能用它來做什么?假定你有一個圖像來指向一條路,而且你希望能用它指向另一條路。現在你只需要一個圖像就可以實現了。
- 返回目錄
給用戶提供翻轉圖像的可選項 –Giving the user the option to flip the image
我們如何為用戶提供翻轉圖像的可選項?這將會是一個對于所有定點都相同的輸入,因此,它將會是?uniform?,對不對?
它也是?true?或?false,所以它是?boolean,或者著色器語言中的?bool
那么我們只有當收到要求翻轉的請求時,才需要讓紋理貼圖的?x?值翻轉。下面是新的?頂點著色器-vertex shader,修改部分用紅色,我去掉了注釋語句以便節省空間。
uniform mat4 modelViewProjection; uniform bool flip; // 紅色attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;lowp vec4 vColor; varying highp vec2 vTexCoord;void main() {vColor = color;if (flip) vTexCoord = vec2(1.0-texCoord.x,texCoord.y); //紅色else vTexCoord = texCoord; //紅色gl_Position = modelViewProjection * position; }C?中的?if?判斷跟 Codea 中的相似,除了判斷條件被放在圓括號中,并且,如果?if?或?else?代碼超過1行,你需要用大括號?{}?把它們包圍起來。
如果你用上面這些替換了?Shader Lab?里的?vertex?標簽頁的代碼,什么也不會發生,因為?flip?的默認值是?false。不過如果你到了?binding?標簽頁(在這里你可以設置測試值),你將會看到一個項目?flip?已經被添加,并且如果你把它設置為?true,Codea Logo?將會翻轉。
這個例子展示給我們的是我們可以通過非常少的代碼來實現很酷的效果,而且我們可以通過命令來讓 著色器去做各種不同的事情。當然了,我意識到你想知道如何在?Codea?代碼中設置?flip?的值。我們很快就會講到這一點。
下一步我們會去看?片段著色器-fragment shader,擁有多得多的用于改造的潛力。
- 返回目錄
片段著色器 –Fragment Shaders
現在我們會去查看 `片段著色器-fragment shader- 的更多細節。
如果你查看了?Shader Lab?中你定制的著色器中的?片段著色器-fragment shader?標簽頁,你將會看到這些代碼:
// // A basic fragment shader ////Default precision qualifier precision highp float;//This represents the current texture on the mesh uniform lowp sampler2D texture;//The interpolated vertex color for this fragment varying lowp vec4 vColor;//The interpolated texture coordinate for this fragment varying highp vec2 vTexCoord;void main() {//Sample the texture at the interpolated coordinate lowp vec4 col = texture2D( texture, vTexCoord ) ; gl_FragColor = col; }這些看起來跟?頂點著色器-vertex shader?的代碼沒有太多不同,并且如果你看了上述?main?函數中的變量的話,你就會看到一些老朋友,vColor,vTexCoord,而且它們確實用了相同的方式來定義。
不論如何,它們確實不一樣,因為在?頂點著色器-vertex shader,它們為一個特定的頂點給出一個值,然而在這里,他們為一個像素點給出一些值(插值)。而且,你可能只有?10?個使用?頂點著色器-vertex shader?的頂點,但是你可能會有?1000?個像素點來使用?片段著色器-fragment shader。
這里有一個新變量,定義為?uniform(因此它被應用于所有的像素點)和?sampler2D(比如在?Codea?中一個 2D 圖像之類的東西)。這是將被用于為像素點選擇顏色的紋理貼圖圖像。(它沒有在?頂點著色器-vertex shader?中被提及,因為那里不需要它)
我曾經解釋過一次那些代碼,不過現在我要再做一次。
lowp vec4 col = texture2D( texture, vTexCoord ) ;main?中的第一行代碼定義了一個名為?col?的新變量,它是一個帶有低精度的 vec4(這些不是我們的關注點)。注意你不需要為它出現在那里而給?OpenGL?一個理由(例如?attribute,?varying?或?uniform),因為對?main?函數而言,它是一個純粹的局部變量。
函數?texture2D?就像?Codea?中的?myImage:Get(i,j)。它取得紋理貼圖圖像中位于?x,y?處的顏色,x,y?的取值范圍是?0~1
gl_FragColor = col;第二行簡單地把它傳遞給用于輸出的變量?gl_FragColor。
這是相當無聊的,所以讓我們試著改動它。
- 返回目錄
改變顏色 –Making a colour change
在你的?Shader Lab?的例子里,在這兩行之間添加一行,如下:
lowp vec4 col = texture2D( texture, vTexCoord ); col.g=1.0-col.g; // <===== 新加的行 gl_FragColor = col;接著你會看到這個:
我們所做的是把綠色翻轉,因此如果它原來是低的,現在變成了高的,反之亦然。
你可能會疑惑為什么我們會用?1?去減,與此同時,顏色值的范圍應該在?0 到 255?之間。好吧,不在?OpenGL?中時它們不是那樣。它們被轉換為數值范圍位于?0 到 1(=255)?之間的小數。
這就是為什么,如果我們為頂點設置顏色,像一個紋理貼圖一樣,使用:
mesh:setColors(color(255)) --set to white的原因。它將會被轉換為?0 到 1?之間的數字,例如淡黃色(255,255,0,128)將會在?片段著色器-fragment shader?中變為?(1.0, 1.0, 0.0, 0.5)。
我們可以把這個顏色應用到我們的像素點上,通過如下乘法:
gl_FragColor = col * vColor;譯者注:這里的?vColor?的值就是上一句中通過?setColor(color(255))?設置好的。
相乘的結果為:
col * vColor = vec4(col.r * vColor.r, col.g * vColor.g,...等等)例如?col?的 r,g,b,a 的值會跟對應的?vColor?的?r,g,b,a?的值相乘。
你能通過一個簡單的實驗來理解這些。我們將會使?Logo?變黑。
把最后一行改為:
gl_FragColor = col * 0.2; //把所有值的亮度都降到 20%這會有效果,因為?0.2?會跟?col?中的每個?r,g,b,a?相乘。
現在,能從?Codea?去做這些將會真的很酷,比如讓你的景色從亮變暗。
那么,這一次就讓我們從?Codea?來做這些吧,OK?
- 返回目錄
在 Codea 代碼中使用著色器 –Using your shader from Codea
你大概一直跟我在?Shader Lab?流連,并且現在你已經有了一個你自己的改變了一些東西(頂點或片段)的著色器。
使用你自己的著色器 –Using your own shader
你可以很容易地試驗它。返回到?Codea?程序的主界面,并且調用那個著色器示例項目。在第 20 行有一個著色器被命名為Effects:Ripple。點擊這個名字,并且從彈出菜單的?Documents?區域選擇你的著色器來代替。然后運行,你就會在屏幕上看到你的著色器做出的改變。
這意味著對一個普通的著色器做出簡單的改變是相當容易的,立刻在你的代碼中使用你的著色器版本。事實上,僅僅需要一行代碼來把你的著色器?和?mesh?關聯起來。
myMesh.shader=('Documents:MyCoolShader')在 Codea 中設置變量 –Setting variables from Codea
讓我們更進一步,創建一個著色器,在我們畫面中實時改變亮度。
首先,回到?Shader Lab,在?Documents?增加一個新著色器,我把它叫做我的?lighting
到?片段-fragment?標簽頁,在?main?之前,把這行代碼加入到定義中去。
uniform float lighting;通過把?lighting?定義為?uniform,我們告訴?OpenGL?這個值由管道外部來提供(比如,來自?Codea),并且應用到全部像素點。因此我們將需要從?Codea?來設置?lighting(它是一個?0~1?之間的分數)這個值。
現在,在?main?函數中,改動最后一行為:
gl_FragColor = col*lighting;位于右側的小測試屏幕將會變黑,因為我們的新變量?lighting?默認為?0,意味著所有的像素點都會被設置為黑色。
為了測試我們的著色器是否起作用,轉到?Binding?標簽頁,你將會看到?lighting?的一個條目,值為?0.0。讓它變大一些,如?0.6,然后測試的圖像會再次出現。值?1.0?會讓它完全變亮。這說明我們的著色器正常工作。
所以,為了告訴?OpenGL?我們想從?Codea?提供一個值,我們在著色器中把它定義為?uniform,并且標簽頁?Binding?為我們提供了一個測試它的方法,在我們在?Codea?中實際使用它之前。
不過現在讓我們返回到?Codea?并且嘗試它。下面是一些代碼用來調用資源庫里的一個圖像,并且為我們提供一個參數用來調節亮度。我已經把我的著色器叫做?lighting,因此,只要改為任何你用過的著色器的名字就可以了。
function setup()img=readImage('Small World:House White')m=mesh()m.texture=img--double size image so we can see it clearlyu=m:addRect(0,0,img.width*2,img.height*2) m:setRectTex(u,0,0,1,1)--assign our shader to this mesh (use your own shader name)m.shader=shader('Documents:Lighting')--allow user to set lighting level parameter.integer('Light',0,255,255) endfunction draw()background(200)perspective()camera(0,50,200,0,0,0)pushMatrix()translate(0,0,-100)--here we set lighting as a fraction 0-1 m.shader.lighting=Light/255m:draw() popMatrix() end特別注意這些:
1、在?draw?函數中,恰好在繪制?mesh?之前,我基于?parameter?的值設置了?lighting?變量,把它當做一個除以?255?的分數
2、你需要把變量?lighting?關聯到?m.shader(比如一個實際的著色器)上,而不是?m(mesh)。
當我們運行它同時改變?light?參數時,圖像慢慢地如下圖所示般變淡,你可以寫一個循環讓它平滑地去做。
因為我們創造了一個淡入淡出的著色器,或者叫霧化。非常簡潔。
- 返回目錄
一個替代選擇 –An alternative
你還能用一個我們的著色器里已有的變量-不過該變量還沒有使用過-來嘗試,就是?color(或者?vColor,片段著色器-fragment Shader?知道它)。Codea?有一個專有的函數用于這個 - 既然我們使用了?setRect?創建了?mesh,那么我們需要使用?setRectColor,如下:
:setRectColor(u,color(Light))但是好像沒效果。
圖像沒有淡化,而是變黑了。發生了什么?
實際上,一切都很好并且工作正常。發生現在這種情況是因為?alpha(控制顏色的透明率) 值在這兩種場景下是不一樣的。我們使用?color(Light)?來設置?setRectColor,當我們只為?color?函數提供一個值時,它把這個值用于前三個參數?r,g,b,但是讓第四個參數?a = 255。所以,當你減少?light?值時,它們每一個都變黑了,而不是透明(譯者注:alpha=0?是全部透明,alpha =255?是全部不透明)。
如果你想要得到淡化/霧化效果,你需要讓?alpha?值跟著一起變化,通過設置全部的?r,g,b,a
m:setRectColor(u,color(Light,Light,Light,Light))你可以使用這個經驗來實現翻轉,回到上述的著色器代碼即可,并且由白天變為黑夜,而不是霧化。所有需要我們做的只是通過?light?把?r,g,b?的值乘起來,不過不包括?a
所以我們的?main?函數變為:
owp vec4 col = texture2D( texture, vTexCoord ) * vColor; col.rgb=col.rgb*lighting; //新行 - 或者, 用 C, 可以寫成 col.rgb *= lighting; gl_FragColor = col;想一想上面我們如何能只選擇改變?r,g,b?的值,而保持?a?不變。這就是我期望?Codea?能做的事。
現在當?light?減少時圖像變黑了(如果你想讓你的背景同時變黑,只要在?Codea?的?background?函數中改變顏色就可以了)。
因此你現在應該明白如何新建一個著色器,它可以制造霧化效果,淡化你的圖像,或者讓你的圖像變黑。你可以通過內建的color?變量來實現,也可以使用你自己新建的變量來實現。這種效果對于僅用幾行代碼來說是相當強大的。
如果你給著色器兩個?uniform?變量,你就能實現霧化、暗化。
不過我猜你也能看到這些都花費了一些時間去習慣和實踐。不過我向你保證,我也沒有一兩次就把所有代碼寫對。(譯者注:第一句感覺含義不大清楚,結合上下文,大概就是說上面的例子都經過反復調試,不影響理解)
- 返回目錄
把著色器代碼嵌入你的代碼中 –Embedding Shaders in your code
我想開始給你很多例子,不過首先,我想向你演示如何把著色器代碼包含在你的?Codea?代碼中。這是因為盡管?Shader Lab?很有用,它也是保存在你的?iPad?中以致你的著色器不能分享給其他人。
把著色器代碼嵌入到你的代碼中是相當容易的。
--this is how you attach your shader to a mesh MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader) --and this is how you "wrap" your shader (in a table) so Codea can read it --this can go anywhere in your code. Choose any name you like. MyShader = { vertexShader = [[ //vertex shader code here ]], fragmentShader = [[ //fragment shader code here ]]}你把你的?頂點著色器-vertex shader?和?片段著色器-fragment shader?代碼放到一個文本字符串中(兩對方括號[[]]?只是一種書寫多行文本字符串的方式),并且接著把它們保存到一個表中(譯者注:就是再用大括號?{}?包起來)。最后,你告訴?Codea?到哪里去找你的著色器 – 注意你給?頂點著色器-vertex shader?和?片段著色器-fragment shader?都起了名字。
你可以在多個?mesh?中使用相同的著色器,你也可以在同一個?mesh?中使用不同的著色器(當然是在不同的時間)。
哪種方式更好? –Which way is better?
我通常把著色器嵌入我的代碼中,因此它們是可移植的。不過如果你有了一個錯誤,你不得不自己去找,然而,如果你在?Shader Lab?中創建了著色器,它會對語法錯誤做出警告,這很有幫助。所以一切取決于你。你可以先從?Shader Lab?起步,后面代碼沒問題了再把它們拷貝到?Codea?中嵌入。
- 返回目錄
著色器例程 –Examples of shaders
我現在準備給你相當一些著色器例子。因為它們中的很多都很簡單,并且只涉及?頂點著色器-vertex shader?或者?片段著色器-fragment shader?中的一種,- 而不是同時包括兩者 - 我覺得沒有改變的代碼沒必要重復。
所以我準備從那些我建議你拷貝到?Codea?的標準代碼開始,然后是每一種著色器(譯者注:就是先?頂點-vertex?再?片段-fragment)。我會演示給你看,在標準代碼中改變哪些內容,來讓著色器生效。我將會把著色器嵌入到?Codea?代碼中。
接下來就是起步的代碼,包括一個仍然什么都不做的著色器。我們主要目標是把顏色改為紅色。
建議 –Suggestions
你可以為每個著色器起一個不同的名字,不過也別忘了同時在?setup?中修改把?shader?和?mesh?關聯起來的那行代碼。
譯者注:就是這個:
MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader)我的建議是保持這些位于?Codea?左手邊標簽頁的代碼不要改變。當我們試驗每一個新例程時,在右邊新建一個標簽頁并把所有標準代碼都拷貝進去,然后在那里修改它們。這意味著你將建立自己的著色器庫,當你摸爬滾打在不同的例程中。
注意 - 如果你終止于 8 個標簽頁時(最多使用 8 個時),每個標簽頁都有自己的?setup?和?draw,沒什么關系。當?LUA在運行前編譯,它會從左到右執行,并且如果它找到重復的函數,它僅僅替換掉它們。因此位于右側標簽頁的代碼是最終被執行的那個 - 你也可以把任何一個標簽頁拖到最右側來讓它執行。
譯者注:Codea?有一個使用技巧,它在拷貝/粘貼到項目時可以把位于同一個文件中的不同標簽分開,只要你在每個標簽頁的代碼最前面用?--#標簽頁1?來標識即可
請注意另外一些事情。在下面提到的任何著色器例程中,我會把著色器用到的變量放在?draw?函數中,例如:
m.shader.visibility=0.5唯一的理由是我要使用參數來改變設置,在任何時候用戶都要能設置,因此?draw?函數需要始終獲得最新值。然而,如果設置一直都不變,例如,如果你正使用霧化/暗化化著色器,并且你只需要霧化,那么你就可以在你第一次把?shader?和?mesh?關聯時就把設置好的值發送給著色器,你就不需要在?draw?里去做這些(一旦你設置好了,它會一直保持同一個值,直到你再次改變)。
最后一句,你會很驚訝這些解釋和?Codea?代碼某種程度上比任何實際著色器代碼的改動都要長。不會一直是這樣的,當然了,這樣會確保你能夠理解這些例程。
為了更容易一些,在寫這份教程時,我已經完成了全部的例程代碼,而且你可以在這個項目里找到它們:
https://gist.github.com/dermotbalson/7443057不過如果你用的是?iPad 1,那就用這個:
https://gist.github.com/dermotbalson/7443577直接選擇你想要運行的著色器然后運行它。它們中的每一個都位于自己的代碼標簽頁內,并且可以被拷貝到其他項目,不需要任何改動就可以運行。
- 返回目錄
標準代碼 –Standard Code
function setup() m=mesh()img=readImage("Small World:Icon") --Choose another if you prefer m:addRect(WIDTH/2,HEIGHT/2,img.width*3,img.height*3) -- I tripled its size m:setColors(color(255))m.texture=img m.shader=shader(DefaultShader.vertexShader,DefaultShader.fragmentShader) endfunction draw() background(40, 40, 50) m:draw() endDefaultShader = { vertexShader = [[uniform mat4 modelViewProjection;attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying lowp vec4 vColor; varying highp vec2 vTexCoord;void main() {vColor=color;vTexCoord = texCoord;gl_Position = modelViewProjection * position;} ]], fragmentShader = [[precision highp float;uniform lowp sampler2D texture;varying lowp vec4 vColor; varying highp vec2 vTexCoord;void main() {lowp vec4 col = texture2D( texture, vTexCoord) * vColor;gl_FragColor = col; } ]]}- 返回目錄
霧化/模糊 –Fog/mist
讓我們從我們做過的開始。我們會讓圖像在朦朧不清的霧中淡入淡出。
我打算把我們的著色器叫做?FogShader,而且我準備使用一個參數,讓我們設置能見度,位于?0(什么也看不到)到1(全部都能清晰看到) 之間的一個顏色值。
因此,這就是我需要在?setup?中修改的內容:
m.shader=shader(FogShader.vertexShader,FogShader.fragmentShader) parameter.number("visibility",0,1,1)在?draw?中也有一點小改變。我把背景設置為跟朦朧不清一樣的顏色,把能見度系數發送給著色器
background(220) m.shader.visibility = visibility在?頂點著色器-vertex shader?中我改了兩行代碼。加入了能見度系數,通過跟這個系數相乘來調整顏色。
//put this with the other uniform item(s) above main uniform float visibility;//replace the line that sets vColor, with this vColor=vec4( color.rgb, color.a ) * visibility;就是它了,你現在可以跟這個能見度參數小伙伴一起好好玩耍了。
- 返回目錄
明暗 –Light/dark
我們已經到了這里,讓我們制作一個能把一幅圖像變亮、變暗的版本。這跟霧化著色器很相似,除了我們沒有調整像素點顏色的?alpha?值。
因此我們可以使用霧化著色器的代碼,只改變其中一行:
vColor=vec4( color.rgb * visibility, color.a );讓我們勇敢地把它們結合起來,既然它們如此相似。
我會在?Codea?的?setup?中放入一個參數,這樣我們就可以在它們之間切換,如果沒錯,我們的 著色器將會繪制霧,或者它會把一幅圖像亮化或暗化。
parameter.boolean("Fog",true)把它放到?draw?中:
m.shader.fog=Fog再把它作為另一個?uniform?變量放到?頂點著色器-vertex shader?中:
uniform bool fog;接著改變?頂點著色器-vertex shader?中?main?函數中的代碼,這樣它要么用能見度系數乘以整個顏色(譯者注:即r,g,b,a),要么只乘以?r,g,b:
if (fog) vColor=vec4( color.rgb, color.a ) * visibility; else vColor=vec4( color.rgb * visibility, color.a );- 返回目錄
基于霧或黑暗的距離 –Distance based fog or dark
這樣是不是很酷,當物體遠去時霧會變得更濃(在一個 3D 畫面里)?或者如果你模擬一個火把或者燈籠,它們會隨著遠去而光亮被遮住直到變黑?
好了,我們可以用我們已有的東西來實現這種效果,不用改動著色器。我們可以繪制一些物體在 3D 場景中,然后讓我們的能見度由距離來決定,就像這樣。
在?setup?中,我會加入一個距離參數,它讓我們指定物體在變得完全透明(或者黑暗)之前需要多遠(用像素點計算)。我會讓我們的圖像在?100 到 1000?的距離之間重復地前進和后退,使用一個?tween?動畫,這樣我們就可以看到效果了。
parameter.integer("distance",0,2000,1000) parameter.boolean("Fog",true) dist={z=200} --we have to use a table of pairs with tweens tween(10, dist, {z=1500}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )我刪掉了之前的能見度參數,因為我們打算自己來計算它。
我替換掉了全部的?draw?代碼,因為我需要在 3D 中繪制(需要?perspective?和?camera?命令),我還想讓背景的明暗由是否使用霧化來決定。我還需要在當前距離繪制一個照片(由?tween?設置,在?dist.z?中)
function draw()if Fog then background(220) else background(0) end perspective()camera(0,0,0,0,0,-1000)m.shader.visibility = 1 - math.min(1,dist.z/distance) m.shader.fog=FogpushMatrix()translate(0,0,-dist.z)m:draw() popMatrix() end- 返回目錄
翻轉著色器 –Flip shader
我們最開始的第一個著色器,翻轉一幅圖像來制作鏡像。我們也可以把它包含進來,通過標準代碼來實現。
我們將會在?setup?中新建?2?個參數由你操作,這樣你就能翻轉?x 或 y,或者兩者同時。
parameter.boolean("Flip_X",false) parameter.boolean("Flip_Y",false)我們將會在?draw?中把它們發送給著色器
m.shader.flipX=Flip_X m.shader.flipY=Flip_Y同時要在?頂點著色器-vertex shader?代碼的頂部加入我們的新變量:
uniform bool flipX; uniform bool flipY;并且調整紋理貼圖的坐標,如下:
vec2 t = texCoord; if (flipX) t.x = 1.0 - t.x; if (flipY) t.y = 1.0 - t.y; vTexCoord = t;是不是覺得變得更容易了?因為我們做了更多的練習。
或許,現在是做一些?片段著色器-fragment shader?的時候了。
- 返回目錄
拼貼著色器 –Tile shader
這是一個極其有用的著色器,有很多用途 - 并且相當簡單!
我第一次需要它是在繪制一個大型 3D 場景時,嘗試把像草、磚塊、柵欄等紋理貼圖覆蓋到不同的物體上。在互聯網上很容易找到合適的紋理圖像,但是它們通常都是錯誤的比例(例如放大太多或縮小太多),和尺寸。太大了還好說,但是太小了就意味著你需要用紋理貼圖像馬賽克一樣貼滿你的圖像(就像一堆瓷磚)。
例如,假設你想要畫一個巨大的 2D 草地,有?2000 * 1000?個像素點,而你有一個大小為?400 * 300?的草的圖像, 這就需要被一個大概?10?倍的系數來進行比例縮放(例如草的葉子會非常巨大)。怎么做?
困難的方法是把你的地面分割成多個跟草的圖像大小一樣的矩形,再把每一個矩形加入你的?mesh?中,用草的圖像作為它們的紋理貼圖。然而,如果我用系數?10?把草的圖像縮放為?40 * 30?像素點,我就需要準備一個數目巨大的矩形集來覆蓋2000 * 1000?的區域。
假設我可以用下面這么實現:
- 一個矩形(哪怕地面大小超過了?Codea?最大的圖像尺寸限制,2048?個像素點)
- 在片段著色器中改變一行代碼
結果如此令人驚訝,甚至讓我欽佩。
它基于一個簡單的技巧。你知道紋理貼圖被映射到每個頂點,用一對介于?0~1?之間的?x,y?值(例如,0,0?是左下角,1,1?是右上角)。
假定我們用兩個三角形新建了一個矩形,生成了整個地面,我們用紋理貼圖做了映射,這樣四個角的?x,y?位置為(使用上面那個例子):
左下角 x = 0, y = 0 右下角 x = 50, y = 0 左上角 x = 0, y = 33.33 右上角 x = 50, y = 33.33x?值為?50,是由?地面寬度/貼圖寬度 = 2000/40?計算得到的,y?值采用相似的計算?1000/30。因此我的?x 和 y?的最大值就是我的貼圖的重復次數。
如果只完成上述工作,我們的片段著色器將會變得混亂,因為它期待介于?0~1?之間的值。不過我們還有更多的事情要做。
在片段著色器中,改動?main?中的第一行代碼如下:
lowp vec4 col = texture2D( texture, vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));它做了什么?它對每個?x 和 y?的紋理值用了一個?mod?函數,計算小數部分,忽略掉整數。所以值?23.45?會變為?.45
如果你好好想想,這將確實是最合適的方法,我們想把小的紋理圖像貼到地面上。
下面的代碼示范了怎么做。我把創建?mesh?的代碼放到一個獨立的函數中,這樣你就能使用參數改變比例同時看看它的樣子。(你也可以試著下載一個草或磚的圖像作為紋理貼圖來玩玩)。
現在我意識到我說過只有兩行代碼被改動,我已經增加了更多的代碼來創建?mesh,因為?addRect?無法設置紋理映射,除了?1?之外,因此我不得不“手動”創建?mesh。不過在大多數項目中,你將至少用這種方式制造你的?mesh。
下面的代碼包括了所有的?Codea?代碼,不過沒有對著色器進行任何修改。需要你自己親自去做修改:
function setup() parameter.number("Scale",0.01,1,.5) parameter.action("Apply change",CreateMesh) CreateMesh() endfunction CreateMesh() m=mesh()img=readImage("Cargo Bot:Starry Background")--create mesh to cover the whole screenlocal v,t={},{}meshWidth,meshHeight=WIDTH,HEIGHT --whole screenimgScale=Scale --use the image at this fraction of its normal size, ie reduce it--now calculate how many times the image is used along the x and z axes --use these as the maximum texture settings--the shader will just use the fractional part of the texture mapping--(the shader only requires one line to change, to do this)local tilesWide=WIDTH/(img.width*imgScale) local tilesHigh=HEIGHT/img.height/imgScale local x1,x2,y1,y2=0,WIDTH,0,HEIGHTlocal tx1,tx2,tz1,tz2=0,tilesWide,0,tilesHigh v[1]=vec3(x1,y1,0) t[1]=vec2(tx1,tz1) v[2]=vec3(x2,y1,0) t[2]=vec2(tx2,tz1) v[3]=vec3(x2,y2,0) t[3]=vec2(tx2,tz2) v[4]=vec3(x1,y2,0) t[4]=vec2(tx1,tz2) v[5]=vec3(x1,y1,0) t[5]=vec2(tx1,tz1) v[6]=vec3(x2,y2,0) t[6]=vec2(tx2,tz2) m.vertices=vm.texCoords=tm:setColors(color(255))m.texture=img m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader) endfunction draw() background(40, 40, 50) m:draw() end輪廓著色器 –Panorama shader
我們可以在更多的場合使用拼貼著色器,而不僅僅用來拼貼巨大的表面。假定你正在制作一個平臺游戲,你想要讓一個背景連續卷動,產生移動著的視覺暗示(譯者注:比如橫版卷軸游戲)。你的背景圖像需要自己重復自己,比如當你走到頭時再次開始動,這跟把一個圖像拼貼滿一個大型區域非常相似。
所以這段?Codea?代碼創建了一個被稱為舞臺布景的圖像,通過一個使用灰色矩形的簡單城市的輪廓,把它加入到一個?mesh?中。
然后,在?draw?中,我們有一個計數器告訴我們以多快的速度卷動。我們計算了需要卷動的圖像的小數(= 被卷動的像素點/圖像的寬度)并且把它發給著色器。
function setup()--create background scenery image--make it a little wider than the screen so it doesn't start repeating too soon scenery=image(WIDTH*1.2,150)--draw some stuff on itsetContext(scenery)pushStyle()strokeWidth(1)stroke(75)fill(150)local x=0rectMode(CORNER)while x<scenery.width dolocal w=math.random(25,100) local h=math.random(50,150) rect(x,0,w,h)x=x+wendpopStyle()setContext()--create meshm=mesh() m:addRect(scenery.width/2,scenery.height/2,scenery.width,scenery.height) m:setColors(color(255))m.texture=scenery m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader) --initialise offsetoffset=0 endfunction draw()background(40, 40, 50) offset=offset+1 m.shader.offset=offset/scenery.width m:draw() --sprite(scenery,WIDTH/2,100) end在著色器中,我們在頂點著色器代碼頂部加入?offset
uniform float offset;并且改變了?vTexCoord?的計算,讓它加上了?offset?的小數值
vTexCoord = vec2(texCoord.x+offset,texCoord.y);當偏移量?offset?增加時,紋理貼圖的?x?的值將會比?1?大,不過我們在片段著色器中的?mod?函數只會保留小數,因此圖像會被拼貼,從而給出一個很平滑的連續不斷的城市背景。
透明著色器 –Transparency shader
一旦你開始使用多幅圖像,一個常見的問題是?OpenGL?不認識透明像素點。我的意思是,如果你先在屏幕上創建了一個完全空白的圖像,接著在它后面繪制了另一個圖像,你希望看到那個圖像 - 但是你看不到。OpenGL?知道它已經在前面畫了些什么(哪怕什么內容也沒有),同時錯誤地假定在它的后面一個點也不畫,因為你看不到它。(譯者注:這種處理是為了減少不必要的計算量)。
當然,這只是 3D 中的一個問題,因為在 2D 中你無法在其他圖像后面畫圖。
對此有不少解決方案,一個是通過距離為你的圖像?mesh?排序,然后按照先遠后近的順序來繪制它們(這樣你就絕不會在其他圖像后面繪制任何圖像)。
另一個辦法是讓?OpenGL?停止繪制那些空白像素點。有一個著色器命令?discard?告訴它不要畫某個像素點,如果你使用它,OpenGL?將會隨后在那些被丟棄掉的像素點后面繪制另外的圖像。
所以我們的透明著色器將會丟棄掉那些?alpha?值低于一個由用戶設置的數字的像素點。我打算把這個數字命名為?minAlpha(范圍?0~1),并且把它包含到著色器中,如下:
uniform float minAlpha; //把這個放在片段著色器中, main 之前//替換掉 gl_FragColor = col; 用這兩行 if ( col.a < minAlpha ) discard; else gl_FragColor = col;為了測試它,我打算在一個藍色星球前面繪制一艘火箭船。我先畫火箭船,然后畫星球。如果透明閥值被設置為?1,我不會丟棄任何東西,這樣你就會看到這個問題了 - 火箭圖像擋住了后面的星球。當你降低閥值時,著色器開始丟棄像素點 - 大概設置為?0.75?看起來效果最好。
function setup() m=mesh() img=readImage("SpaceCute:Rocketship") m:addRect(0,0,img.width,img.height) m:setColors(color(255)) m.texture=img m.shader=shader(DefaultShader.vertexShader,DefaultShader.fragmentShader) parameter.number("Transparency",0,1,1) endfunction draw()background(40, 40, 50)perspective()camera(0,0,0,0,0,-1000)pushMatrix()translate(0,0,-400) --rocketship first m.shader.minAlpha = 1 - Transparencym:draw()translate(0,0,-400) --draw the planet further away fill(140, 188, 211, 255)ellipse(0,0,500)popMatrix() end蒙版著色器 –Stencil shader
假定你想讓一幅圖像像面具一樣半遮半掩在另一幅圖像上面,例如你想從一幅圖像里剪切出一個形狀來,或者可能僅僅畫一幅圖像的一部分來覆蓋到第二幅圖像上。
看看下圖的例子:
在第一幅圖像中,一個小公主的形狀被用于從圖像上剪切了一個剪影洞。
在第二幅圖像中,一個小公主的形狀用一個紅色五星圖像畫了出來。
譯者注:小公主形狀來自?Codea?素材庫里的小公主圖像。
正如前一個例程一樣,大多數代碼改動都在?Codea?里,我們從讀入兩幅圖像,并用五星狀背景創建?mesh?開始。這里有三個參數 -?Invert?讓我們在上述兩類蒙版之間選擇,Offset_X?和?Offset_Y?讓我們把蒙版準確地放置到你想要放置的地方(好好跟它們玩玩看看它們怎么做)。
function setup() img=readImage("Cargo Bot:Starry Background") stencilImg=readImage("Planet Cute:Character Princess Girl") m=mesh() u=m:addRect(0,0,img.width,img.height) m.texture=img m.shader = shader(stencilShader.vertexShader, stencilShader.fragmentShader) m.shader.texture2=stencilImg parameter.boolean("Invert",false) parameter.number("Offset_X",-.5,.5,0) parameter.number("Offset_Y",-.5,.5,0) endfunction draw() background(200) pushMatrix() translate(300,300) m.shader.negative=Invert m.shader.offset=vec2(Offset_X,Offset_Y) m:draw() popMatrix() end片段著色器需要定義額外的圖像,和變量,這個變量告訴它通過什么方式去應用蒙版,以及蒙版的偏移位置。
蒙版本身是很簡單的。你將會看到我們首先從兩幅圖中讀入兩個像素點顏色(涉及第二幅圖像時使用?offset),然后我們或者
- 用第一個像素點去畫本來第二個像素點應該位于的位置(僅當它不是空白時)
或者
- 用第一個像素點去畫僅當那個位置上沒有第二個像素點
代碼:
uniform lowp sampler2D texture2; uniform bool negative; uniform vec2 offset;lowp vec4 col1 = texture2D( texture, vTexCoord ); lowp vec4 col2 = texture2D( texture2, vec2(vTexCoord.x-offset.x,vTexCoord.y-offset.y)); if (negative) {if (col2.a>0.) gl_FragColor = col1; else discard;} else if (col2.a==0.) gl_FragColor = col1; else discard;積木著色器(Codea內建) –Brick shader (built into Codea)
由?Codea?提供的著色器非常值得一看,看看你是否能學到些什么。它們有些充滿數學,不過其他的非常有趣。
打開積木著色器,例如,它沒有使用任何紋理貼圖畫了一個磚墻圖案。
頂點著色器非常普通,除了:
- 紋理貼圖變量?vTexCoord?被遺忘了
- 在?main?中有一行額外代碼
代碼:
vPos = position;我們能夠理解為什么?vTexCoord?會缺少(這里沒有紋理貼圖圖像),不過即使這樣仍然很有趣,因為它展示了你僅須傳遞片段著色器需要的變量。
額外的一行傳遞頂點位置坐標的代碼,更有趣。通常它不會被傳遞給片段著色器,不過很明顯的,在這個例子里我們需它。OpenGL?將會對每個像素點進行插值,所以片段著色器會知道每個像素點的確切位置。
片段著色器有?4?個來自?Codea?的輸入 - 磚塊顏色,灰泥(水泥)顏色,磚塊的尺寸(xyz,所以它可以是 2D 或 3D),以及磚塊在整體規模中的比例(剩下的是水泥)。
uniform vec4 brickColor; uniform vec4 mortarColor; uniform vec3 brickSize; uniform vec3 brickPct;main?函數如下:
void main() {vec3 color;vec3 position, useBrick;我們計算了磚塊上的像素點的位置。這將是一個像是?0.43?或者?5.36?的數字(如果我們在第六塊磚塊上),以此類推。
position = vPos.xyz / brickSize.xyz;如果磚塊數目是偶數,它就以半塊磚為單位來移動?x?和?z(深度)的位置,所以磚塊的間隔行的偏移以半塊磚為單位。
if( fract(position.y * 0.5) > 0.5 ) {position.x += 0.5;position.z += 0.5; }接下來我們決定如果我們位于磚塊或者水泥上。C?里的函數?step?返回 0 如果?position < brickPct.xyz,否則返回1(例如,它一直只是?0 或 1)。這看起來跟下面這句一樣:
if position < brickPct.xyz, useBrick = 0 else useBrick=1但是要注意,對于每個?x,y 和 z,它都會分別進行計算,例如?useBrick?是一個?vec3
position = fract(position); useBrick = step(position, brickPct.xyz);現在我們使用?mix?函數來把水泥和磚塊的顏色組合起來,應用?useBrick。我們對?useBrick?里的?x,y 和 z?的值進行相乘,因為我們只想繪制磚塊的顏色當我們在?3?個方向上都位于磚塊區域內時。命令?mix?等價于 Codea 中的?color:mix。
結果被用來跟為?mesh?設置的全局顏色(vColor)相乘。
color = mix(mortarColor.rgb, brickColor.rgb, useBrick.x * useBrick.y * useBrick.z);color *= vColor.rgb;//Set the output color to the texture colorgl_FragColor = vec4(color, 1.0); }我發現這個著色器有趣的地方是如何把你不想要的東西扔出去,而把你想要的其他東西包括進來 – 只要你足夠小心!!!
- 返回目錄
學習更多
沒有比例程代碼更好的辦法來學習著色器了。Codea?有一批內建的著色器可供你把玩,而且在互聯網上有更多的,盡管它可能會引起混淆因為我們使用的是一種叫做?GLSL?的特殊的?OpenGL?著色器語言,所以最好把它們加入搜索關鍵詞。
我也用一種方便的關于?GLSL暗化 可用命令的概要參考,來自這里:
http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf
只用最后兩頁。
全文結束 – End
總結
以上是生活随笔為你收集整理的为新手准备的 Codea 着色器(Shader)教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GLSL着色器周记02
- 下一篇: 三大缓存框架ehcache、memcac