爲新手準備的 Codea 着色器(Shader)教程


爲新手準備的 Codea 着色器(Shader) 教程

原文標題:《Shaders for dummies》 
作者:Ignatz 
譯者:FreeBlues 
譯文鏈接:http://my.oschina.net/freeblues/blog/336055 
PDF鏈接:http://pan.baidu.com/s/1c0xTUzI

目錄


  • 概述
    • 頂點着色器 –Vertex Shaders
    • 片段着色器 –Fragment shaders
  • 着色器是什麼? –What are shaders?
    • 管道 –Pipes
    • 從頂點到像素點 –Vertexes to pixels
    • 頂點着色器 –Vertex shaders
    • 片段着色器 –Fragment shaders
  • 頂點着色器 –Vertex Shaders
  • 改變頂點着色器 –Changing the vertex shader
    • 翻轉圖像 –Flipping an image
    • 給用戶翻轉圖像選項 –Giving the user the option to flip the image
  • 片段着色器 –Fragment Shaders
    • 改變顏色 –Making a colour change
  • 在 Codea 代碼中使用着色器 –Using your shader from Codea
    • 使用你自己的着色器 –Using your own shader
    • 在 Codea 中設置變量 –Setting variables from Codea
    • 一個替代選擇 –An alternative
  • 把着色器代碼嵌入你的代碼中 –Embedding Shaders in your code
    • 哪種方式更好? –Which way is better?
  • 着色器例程 –Examples of shaders
    • 建議 –Suggestions
    • 標準代碼 –Standard Code
    • 霧化/模糊–Fog/mist
    • 明暗 –Light/Dark
    • 基於霧或黑暗的距離 –Distance based fog or dark
    • 翻轉着色器 –Flip shader
    • 拼貼着色器 –Tile shader
    • 輪廓着色器 –Panorama shader
    • 透明着色器 –Transparency shader
    • 蒙版着色器 –Stencil shader
    • 積木着色器(Codea內建)–Brick shader (built into Codea)
  • 學習更多

概述


  • 譯者注:

    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 允許你一次修改一個頂點(一個頂點就是一個三角形的一個角,記住在計算機中所有的圖形都是由三角形組成的)

譯者注:如下所示:

3D網格模型

片段着色器 –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 transform
    gl_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 已經被添加,並且如果你把它設置爲 trueCodea 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 函數中的變量的話,你就會看到一些老朋友,vColorvTexCoord,而且它們確實用了相同的方式來定義。

不論如何,它們確實不一樣,因爲在 頂點着色器-vertex shader,它們爲一個特定的頂點給出一個值,然而在這裏,他們爲一個像素點給出一些值(插值)。而且,你可能只有 10 個使用 頂點着色器-vertex shader 的頂點,但是你可能會有 1000 個像素點來使用 片段着色器-fragment shader

這裏有一個新變量,定義爲 uniform(因此它被應用於所有的像素點)和 sampler2D(比如在 Codea 中一個 2D 圖像之類的東西)。這是將被用於爲像素點選擇顏色的紋理貼圖圖像。(它沒有在 頂點着色器-vertex shader 中被提及,因爲那裏不需要它)

我曾經解釋過一次那些代碼,不過現在我要再做一次。

lowp vec4 col = texture2D( texture, vTexCoord ) ;

main 中的第一行代碼定義了一個名爲 col 的新變量,它是一個帶有低精度的 vec4(這些不是我們的關注點)。注意你不需要爲它出現在那裏而給 OpenGL 一個理由(例如 attributevarying 或 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 clearly
    u=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)
end

function 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/255
    m: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)
end

function draw() 
    background(40, 40, 50) 
    m:draw()
end

DefaultShader = { 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=Fog
    pushMatrix()
    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.33

x 值爲 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()
end

function CreateMesh() 
    m=mesh()
    img=readImage("Cargo Bot:Starry Background")
    --create mesh to cover the whole screen
    local v,t={},{}
    meshWidth,meshHeight=WIDTH,HEIGHT --whole screen
    imgScale=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,HEIGHT
    local 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=v
    m.texCoords=t
    m:setColors(color(255))
    m.texture=img 
    m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader)
end

function 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 it
    setContext(scenery)
    pushStyle()
    strokeWidth(1)
    stroke(75)
    fill(150)
    local x=0
    rectMode(CORNER)

    while x<scenery.width do
        local w=math.random(25,100) 
        local h=math.random(50,150) rect(x,0,w,h)
        x=x+w
    end

    popStyle()
    setContext()
    --create mesh
    m=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 offset
    offset=0
end

function 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)
end

function draw()
    background(40, 40, 50)
    perspective()
    camera(0,0,0,0,0,-1000)
    pushMatrix()
    translate(0,0,-400) --rocketship first 
    m.shader.minAlpha = 1 - Transparency
    m: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)       
end

function 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 color
    gl_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




原文鏈接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章