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

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

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

目錄


概述


  • 譯者注:github

    一、Codea 是一款能夠在 Ipad 上直接編寫遊戲的 APP 應用軟件,它使用 Lua 的語法和庫,並針對 iPad 提供了一系列函數,諸如繪圖、觸摸、攝像頭、重力感應乃至網絡等等,Codea 支持 OpenGL ES,它編寫的程序能夠直接在 iPad 上運行,也能夠導出爲 Xcode 項目,再用 Xcode 編譯爲可發佈在 App Store 的應用程序。編程

    二、本教程講述的內容適用於 Codea 環境,跟 OpenGL ES 在其餘開發環境的使用會有一些不一樣。數組


Codea 建構於 OpenGL ES Shading Language(開放圖形庫 嵌入式系統 渲染語言)之上,它(OpenGL)提供了很是複雜的工具,以及一連串複雜處理,用來把像素繪製到屏幕上。安全

在這一系列處理中有兩個步驟:Vertex Shader(頂點着色) 和 Fragment Shader(片斷着色),Codea 容許咱們把這二者混合在一塊兒來使用。爲何應該爲此而興奮?由於它給了你訪問底層圖形的能力,容許你建立很是強有力的視覺效果。網絡

頂點着色器 --Vertex Shader

頂點着色器Vertex Shader 容許你一次修改一個頂點(一個頂點就是一個三角形的一個角,記住在計算機中全部的圖形都是由三角形組成的)app

譯者注:以下所示:dom

3D網格模型

片斷着色器 --Fragment shaders

片斷着色器Fragment shaders 容許你一次修改一個像素點的顏色(以及紋理貼圖的座標位置)。ide

這些聽起來都很複雜,不過別被嚇跑。函數

着色器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 shaderfragment-片斷 只是像素點-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 三件事:

一、Precision(小數的位數)-- 精度

有三個選項,highp, mediump, lowp,你能夠猜想它們的含義,若是你沒有指定一個,默認值是 highp。就如今而言,全部這些你均可以徹底忽略,由於咱們要作的任何事都不須要特別的精度。

二、變量的數據類型

若是你編寫過其餘程序,你會習慣於指出一個變量是不是一個整數,一個帶有小數的數,一個字符串,一個數組等等。Codea 本身計算出絕大部分數據類型而斷送掉咱們親自計算的機會。OpenGL 須要咱們確切地定義變量,不過它們都是至關明顯的。

  • vec2 = Codea 中的 vec2,如 vec2(3,4),還有 vec3vec4
  • bool = boolean(true 或 false) 布爾型,真值或假值
  • int = integer 整型
  • float = 帶小數的數 浮點型
  • sampler2D = 一個 2D 圖像
  • mat2 = 2*2 的表(mat3mat4 分別是 3*34*4 的表)

所以你必須在你添加的任何變量前面包括其中任意一種類型

三、這些變量用來作什麼?

OpenGL 須要知道你這些變量是拿來作什麼用的。一個出如今這個着色器中的變量有三個可能的緣由。

(a)attribute - 是一個輸入,提供關於這個特定頂點的信息,好比,它的值對於每一個頂點都是不一樣的。明顯的例子就是位置座標,顏色和紋理座標,而且你將會在上述代碼中看到它們所有。這些輸入由 Codea 自動爲每個頂點提供。

(b)uniform - 也是一個輸入,不過對於每一個頂點來講沒有變化。例如,Codea 中的 blend shader(譯者注:能夠在着色實驗室找到這個着色器)定義了第二幅圖像用來跟一般的 mesh 紋理貼圖圖像進行混合,而且這幅相同的圖像將會被用於全部的頂點,所以它是 uniform-統一的。在標準代碼中只有一個 uniform - modelViewProjection - 並且咱們如今不會討論它,由於它是 3D 黑盒子的一部分。

(c)varying - 這些是輸出,將被用於插值獨立像素點,還將會在 片斷着色器-fragment shader 中可用。這裏是它們中的兩個 vColorvTexCoord,你能夠添加更多的。

讓咱們再次總結一下:

  • 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 中你本身定製的 着色器中嘗試。咱們的目標是把 CodeaLogo 變成一個鏡像圖像。

翻轉圖像最簡單的辦法是改變紋理貼圖的全部座標,這樣 OpenGL 就會由從右到左繪製換成從左到右繪製。你應該記得紋理貼圖的位置座標是介於 0 到 1 之間的分數,0 位於左邊(或者底部),1 位於右邊(或者頂部)。若是咱們用 1 減去 x 值,咱們將會獲得想要的結果,由於位置(0,0)(左下角)會被改變爲(1,0)(右下角),反之亦然。

所以,讓咱們看看 頂點着色器-vertex shadermain 的內部,這就是咱們要改的那一行

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 LabLogo 翻轉爲一個鏡像圖像。

如今來看看你可否用相同的方法翻轉 y 值。。。

你能用它來作什麼?假定你有一個圖像來指向一條路,並且你但願能用它指向另外一條路。如今你只須要一個圖像就能夠實現了。

給用戶提供翻轉圖像的可選項 --Giving the user the option to flip the image

咱們如何爲用戶提供翻轉圖像的可選項?這將會是一個對於全部定點都相同的輸入,所以,它將會是 uniform ,對不對?

它也是 truefalse,因此它是 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 中的類似,除了判斷條件被放在圓括號中,而且,若是 ifelse 代碼超過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 一個理由(例如 attribute, varyinguniform),由於對 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 的值會跟對應的 vColorr,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

特別注意這些:

一、在 draw 函數中,剛好在繪製 mesh 以前,我基於 parameter 的值設置了 lighting 變量,把它當作一個除以 255 的分數

二、你須要把變量 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))

你可使用這個經驗來實現翻轉,回到上述的着色器代碼便可,而且由白天變爲黑夜,而不是霧化。全部須要咱們作的只是經過 lightr,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 減小時圖像變黑了(若是你想讓你的背景同時變黑,只要在 Codeabackground 函數中改變顏色就能夠了)。

所以你如今應該明白如何新建一個着色器,它能夠製造霧化效果,淡化你的圖像,或者讓你的圖像變黑。你能夠經過內建的 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 中修改把 shadermesh 關聯起來的那行代碼。

譯者注:就是這個:

MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader)

個人建議是保持這些位於 Codea 左手邊標籤頁的代碼不要改變。當咱們試驗每個新例程時,在右邊新建一個標籤頁並把全部標準代碼都拷貝進去,而後在那裏修改它們。這意味着你將創建本身的着色器庫,當你摸爬滾打在不一樣的例程中。

注意 - 若是你終止於 8 個標籤頁時(最多使用 8 個時),每一個標籤頁都有本身的 setupdraw,沒什麼關係。當 LUA 在運行前編譯,它會從左到右執行,而且若是它找到重複的函數,它僅僅替換掉它們。所以位於右側標籤頁的代碼是最終被執行的那個 - 你也能夠把任何一個標籤頁拖到最右側來讓它執行。

譯者注:Codea 有一個使用技巧,它在拷貝/粘貼到項目時能夠把位於同一個文件中的不一樣標籤分開,只要你在每一個標籤頁的代碼最前面用 --#標籤頁1 來標識便可

請注意另一些事情。在下面提到的任何着色器例程中,我會把着色器用到的變量放在 draw 函數中,例如:

m.shader.visibility=0.5

惟一的理由是我要使用參數來改變設置,在任什麼時候候用戶都要能設置,所以 draw 函數須要始終得到最新值。然而,若是設置一直都不變,例如,若是你正使用霧化/暗化化着色器,而且你只須要霧化,那麼你就能夠在你第一次把 shadermesh 關聯時就把設置好的值發送給着色器,你就不須要在 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 );

讓咱們勇敢地把它們結合起來,既然它們如此類似。

我會在 Codeasetup 中放入一個參數,這樣咱們就能夠在它們之間切換,若是沒錯,咱們的 着色器將會繪製霧,或者它會把一幅圖像亮化或暗化。

parameter.boolean("Fog",true)

把它放到 draw 中:

m.shader.fog=Fog

再把它做爲另外一個 uniform 變量放到 頂點着色器-vertex shader 中:

uniform bool fog;

接着改變 頂點着色器-vertex shadermain 函數中的代碼,這樣它要麼用能見度係數乘以整個顏色(譯者注:即 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 中繪製(須要 perspectivecamera 命令),我還想讓背景的明暗由是否使用霧化來決定。我還須要在當前距離繪製一個照片(由 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;

若是磚塊數目是偶數,它就以半塊磚爲單位來移動 xz(深度)的位置,因此磚塊的間隔行的偏移以半塊磚爲單位。

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

相關文章
相關標籤/搜索