認識著色器

文章推薦指數: 80 %
投票人數:10人

在〈準備WebGL Canvas〉中談到,WebGL 的組成中需要著色器程式,初學WebGL 時,著色器程式中基本上會有頂點著色器(Vertex shader)及片段著色器(Fragment sh... 回WebGL 在〈準備WebGLCanvas〉中談到,WebGL的組成中需要著色器程式,初學WebGL時,著色器程式中基本上會有頂點著色器(Vertexshader)及片段著色器(Fragmentshader),前者主要負責頂點的運算,將頂點對應至畫面上的二維座標,後者則是計算出需要繪製的像素顏色值。

著色器使用GLSL撰寫,並透過WebGL的JavaScriptAPI編譯、繫結(attach)、鏈結(link)等動作成為著色器程式,接下來會藉由在Canvas的正中央繪製一個點,認識一下這個流程。

頂點著色器的作用是產生裁剪空間(Clipspace)座標,例如,底下的頂點著色器始終生成裁剪空間中心座標: voidmain(void){ gl_Position=vec4(0.0,0.0,0.0,1.0); } 在被要求計算裁剪空間座標時,這個頂點著色器實際上沒有從屬性(Attribute)或緩衝區(Buffer)取得資料來進行計算,只是單純地生成固定的座標值(0.0,0.0,0.0),分別代表(x,y,z),裁剪空間三維座標的分量值必定介於-1.0~1.0,超過範圍的資料會被裁剪不被繪製,至於(x,y,z)的正方向如下(中心為(0,0,0)): 如果要將Canvas的顯示空間對應至裁剪空間,那麼通常可以將Canvas顯示空間最左邊對應至裁剪空間的x的-1.0,最右邊對應至1.0,顯示空間最下方對應至裁剪空間y的-1.0,最上方對應至1.0,而z代表著深度,繪製像素時,z的資訊會轉換為0~1寫入深度緩衝,在啟用深度測試的情況下,預設像素的深度輸入值小於深度緩衝中對應位置的值才會進行繪製,也就是說,近物遮蓋遠物(可以改變這個行為)。

在GLSL中,gl_開頭的變數意謂著保留的變數,gl_Position用來指定裁剪空間中的座標,它需要指定GLSL中vec4型態的值,也就是具有四個分量的浮點數,實際上座標只需要用到前三個分量,第四個分量就座標本身來說用不上,然而慣例上會設為1.0,這在一些向量計算時會比較方便。

那麼一個片段著色器,看起來會是如何呢? voidmain(void){ gl_FragColor=vec4(1.0,0.0,0.0,1.0); } 這個片段著色器實際上也沒有從屬性或緩衝區取得資料來進行計算,只是單純地生成固定的的顏色值(1.0,0.0,0.0),也就是RGB,三個分量必定介於-1.0~1.0,同樣地,在這邊vec4的第四個分量慣例上會設為1.0。

在這邊要先知道的是,在被要求渲染時,這邊頂點的頂點著色器直接設定裁剪空間座標為空間的中心,之後片段著色器將直接將之繪製為紅色,之後會看到如何從屬性或緩衝區取得資料進行計算。

著色器要寫在哪呢?可以是個(透過Ajax或FetchAPI)下載的檔案,或者是寫在JavaScript字串裏(透過ES6的模版字串會比較方便),或者是個寫在裏的文字。

例如:

ShaderProgram voidmain(void){ gl_Position=vec4(0.0,0.0,0.0,1.0); //預設的點太小了,設大一些看得清楚些 gl_PointSize=5.0; } voidmain(void){ gl_FragColor=vec4(1.0,0.0,0.0,1.0); } type的設定值"x-shader/x-vertex"或"x-shader/x-fragment"為自訂的,要設為別的值也可以,反正這表示它不是JavaScript,之後只要取得對應的script的DOM元素,透過textContent就可以取得著色器程式碼。

例如,在shader-1.js中寫個函式: functionshaderSourceById(id){ returndocument.getElementById(id).textContent; } 要編譯著色器的話很簡單,寫個如下的函式,glContext是WebGLRenderingContext實例,type會是個常數VERTEX_SHADER或FRAGMENT_SHADER,表示要建立哪種著色器,source是著色器程式的原始碼字串,而API名稱應該是很清楚地提示了它在做些什麼: functionshader(glContext,type,source){ constshader=glContext.createShader(type); glContext.shaderSource(shader,source); glContext.compileShader(shader); if(!glContext.getShaderParameter(shader,glContext.COMPILE_STATUS)){ throw'編譯著色器時發生錯誤:'+glContext.getShaderInfoLog(shader); } returnshader; } 接下來,必須將建立的著色器組合為一個著色器程式,並指定給WebGLRenderingContext實例使用: functioninstallProgram(glContext,vertexSource,fragSource){ constvertexShader=shader(glContext,glContext.VERTEX_SHADER,vertexSource); constfragShader=shader(glContext,glContext.FRAGMENT_SHADER,fragSource); constprog=glContext.createProgram(); glContext.attachShader(prog,vertexShader); glContext.attachShader(prog,fragShader); glContext.linkProgram(prog); if(!glContext.getProgramParameter(prog,glContext.LINK_STATUS)){ throw'編譯著色器時發生錯誤:'+glContext.getProgramInfoLog(prog); } glContext.useProgram(prog); returnprog; } 為了方便,將〈準備WebGLCanvas〉中取得WebGLRenderingContext實例的程式碼也封裝為一個函式: functiongetGLContext(glCanvas){ glCanvas.width=glCanvas.clientWidth; glCanvas.height=glCanvas.clientHeight; constgl=glCanvas.getContext('webgl'); if(!gl){ throw'無法初始化WebGL,您的瀏覽器不支援'; } gl.clearColor(0.0,0.0,0.0,1.0); gl.clear(gl.COLOR_BUFFER_BIT); returngl; } 接下來,只要取得WebGLRenderingContext實例,安裝著色器程式,就可以來畫個點了: constgl=getGLContext(document.getElementById('glCanvas')); installProgram(gl, shaderSourceById('vertex-shader'), shaderSourceById('fragment-shader') ); gl.drawArrays(gl.POINTS,0,1); WebGLRenderingContext的drawArrays基於頂點的向量陣列資料來繪製,第一個參數指定了POINTS常數,這表示只繪製頂點,第二個參數指定從陣列中哪個索引開始,第三個參數指定要繪製幾筆資料,每取得一筆資料,就會執行一次頂點著色器,計算出裁剪空間中的座標,要進行顏色渲染時,就會執行一次片段著色器。

實際上,至今並沒有指定過陣列資料,而著色器本身的程式碼也沒有要求資料,需要用到的值都是直接寫死在著色器,因此第二個參數實際上沒做用,這邊只要求繪製一個點,因此第三個參數設定為1。

費盡千辛萬苦,點一下範例網頁吧!你會看到全黑背景正中央有個紅點,嗯?長的像正方形?一個像素點就是一個正方形啊!


請為這篇文章評分?