webgl筆記-2.着色器和緩沖區


OpenGL的教程多以“畫一個點”開始:簡單的初始化過程后,調用glVertexXX()並傳入描述點信息的位置。下面就是一個典型的OpenGL的HelloWorld代碼。

glBegin(GL_POINTS); 
    glVertex3f(0.0f, 0.0f, 0.0f); 
glEnd();

開始學習WebGL的時候我試圖尋找這樣的代碼,之后我發現在WebGL中,即使要畫出一個點,也需要了解着色器和緩沖區的知識。好在對於嘗試編寫WebGL程序的人來說,關於着色器和緩沖區的知識是必要的。在研究了HiWebGL站點翻譯的WebGL教程前幾課的代碼,並且自己嘗試實現一個3D貪吃蛇程序之后,我對着色器和緩沖區的知識稍作整理,寫下這篇博文,以便以后查閱。如果你也在學習這方面的知識,希望這篇博文能夠幫助到你。

着色器

着色器,可以理解為運行在顯卡中的指令和數據。
完整的着色器包括頂點着色器和片元着色器。頂點着色器最基本的任務是接收三維空間中點的坐標,將其處理為二維空間中的坐標並輸出;片元着色器最基本的任務是對需要處理的屏幕上的每個像素輸出一個顏色值;將頂點着色器輸出的二維空間中的點坐標,轉化為需要處理的像素並傳遞給片元着色器的過程,稱為圖元光柵化。

        1

在WebGL中,着色器是用一種類似於C的語言x-shader編寫的。

1.頂點着色器

頂點着色器接受attribute變量和uniform變量。attribute變量存儲着關於點本身的數據,其中最重要的當然是點的位置。uniform變量存儲的數據僅僅幫助着色器完成任務,換言之,着色器僅僅是需要uniform變量而並不處理他們。頂點着色器需要輸出varying變量給片元着色器。
注意,attribute、uniform、varying並不是數據類型,而只是描述該變量在着色器中的作用。

最簡單的頂點着色器代碼如下:

<script id="shader-vs" type="x-shader/x-vertex">  
    attribute vec3 aVertexPosition;  
    uniform mat4 uMVMatrix; 
    uniform mat4 uPMatrix;  
    void main(void) { 
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 
    } 
</script>

定義vec3類型的attribute變量aVertexPosition(這里可以注意一下對變量的命名習慣),這個變量以三維向量的形式存儲了點在三維空間中的信息。 

定義mat4類型的uniform變量uMVMatrix和uPMatrix,分別表示模型視圖矩陣和投影矩陣。

頂點着色器的處理單元是單個頂點。對每一個頂點數據,都要執行一次main(void)函數來返回這個頂點在二維屏幕上的坐標和其他varying變量。准備給頂點着色器處理的attribute變量的數量需要和頂點的數量一致(比如頂點在三維空間中的位置的數量就和頂點的數量一致,很快這種表述就不那么像廢話了),而每個uniform變量只需要一個,對每個頂點的處理用到的是同一個uniform變量。
在主函數main(void)中可以看到很清晰的邏輯,即將三維空間中的點映射到屏幕上(實際上是CCV中)。
varying變量是頂點着色器的輸出,經過光柵化后作為片元着色器的輸入。最簡單的着色器沒有顯式定義varying變量。gl_Position是一個varying變量,由於它非常重要,已經被隱式定義了,也就是說,頂點着色器必須返回gl_Position。

2.片元着色器

片元着色器的唯一任務是,給出屏幕上每個像素的顏色。片元着色器接受varying變量——正是頂點着色器的輸出,但是不完全一樣。交給片元着色器的處理單元不是頂點,而是像素,將頂點轉化為像素的技術稱為“圖元光柵化”,這稍后再去了解,現在只關心對某一個像素,如何指定它的顏色。

最簡單的片元着色器代碼如下:

<script id="shader-fs" type="x-shader/x-fragment"> 
    precision mediump float;  
    void main(void) { 
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); 
    } 
</script>

這個片元着色器處理像素的方式果然很簡單,那就是:將待處理的像素的顏色指定為不透明的白色(注意是待處理的像素而不是屏幕上的所有像素)。片元着色器只接收了gl_Position這個varying變量,而gl_Position經過光柵化后已經用來指定“要處理的是哪個頂點”這條信息了,因此片元着色器沒有關於頂點顏色的任何信息,只能指定所有要處理的像元都是一個信息。

3.圖元光柵化

圖元光柵化將頂點着色器的輸出轉化(一系列頂點)為片元着色器的輸入(一系列像元)。

頂點着色器處理了一個空間中三角形的數據,得到CCV中的三角形的三個頂點,在圖元光柵化發生之前,gl_Position是一個有三個元素的數組,每個元素表示一個頂點的齊次坐標。

$$\begin{bmatrix}x_{1} & x_{2} & x_{3}\\ y_{1} & y_{2} & y_{3}\\ z_{1} & z_{2} & z_{3}\\ 1 & 1 & 1 \end{bmatrix}$$

 

Untitled-1

圖元光柵化發生之后,gl_Position成為了具有許多個元素的數組(下圖中是16個),每個元素表示一個像素。

$$\begin{bmatrix}x_{1} & ...... & x_{16}\\ y_{1} & ...... & y_{16}\\ z_{1} & ...... & z_{16}\\ 1 & ...... & 1 \end{bmatrix}$$

Untitled-2

柱狀物表示分割出的需要處理的像素,高度可以認為是z坐標值,由線形內插而成。事實上,除了gl_Position的x、y分量,其他所有varying變量都會進行光柵化,而值都是線形內插得到的。我們可能需要一個varying變量表示頂點的顏色(我們要在javascript中為三個頂點准備三個顏色),光柵化的時候,也會為16個像素線形內插出16個顏色。

線形內插的方法很簡單,對於三角形上具有x、y確定坐標的像素點(xp,yp,zp)有:

$$\begin{bmatrix}x_{p}-x_{1}\\ y_{p}-y_{1}\\ z_{p}-z_{1}\end{bmatrix}=a\begin{bmatrix}x_{2}-x_{1}\\ y_{2}-y_{1}\\ z_{2}-z_{1}\end{bmatrix}+b \begin{bmatrix}x_{3}-x_{1}\\ y_{3}-y_{1}\\ z_{3}-z_{1}\end{bmatrix}$$

只有a、b、zp是未知數,因此是很容易解出zp的。

4.簡單的實現顏色

頂點着色器代碼:

<script id="shader-vs" type="x-shader/x-vertex"> 
    attribute vec3 aVertexPosition; 
    attribute vec4 aVertexColor;  
    uniform mat4 uMVMatrix; 
    uniform mat4 uPMatrix;  
    varying vec4 vColor;  
    void main(void) { 
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 
        vColor = aVertexColor; 
    } 
</script>

 

片元着色器代碼:

<script id="shader-fs" type="x-shader/x-fragment"> 
    precision mediump float;  
    varying vec4 vColor;  
    void main(void) { 
        gl_FragColor = vColor; 
    } 
</script>

 

最簡單的實現顏色的方法是,為每個頂點准備一個顏色。如果希望一個面具有單一的顏色,那么就為這個表面的三個頂點准備同一個顏色。顏色通過新建的attribute變量aVertexColor傳入頂點着色器;在處理每個頂點(通過aVertexPosition計算出gl_Position)的同時,處理那個頂點所對應的顏色(通過aVertexColor計算出vColor)。這個例子直接將前者賦值給后者,當然可以有其他的處理方法,比如你想取反色的時候。

片元着色器里的vColor變量已經光柵化過了,也就是說,如果頂點着色器處理的是上文說到的那個三角形,頂點着色器里的vColor還是具有3個元素的數組,到片元着色器里,vColor已經是具有16個元素的數組了。顏色的RGBA值全部類似於gl_Position中的z值一樣被線形內插過了。

5.實現紋理

實現紋理和實現簡單顏色的不同在於,像素點的顏色不是單純計算出的,而是從樣本紋理上查詢得到。每個像素點的顏色都從樣本紋理上查詢到了,也就是將紋理貼上表面了。

頂點着色器代碼

<script id="shader-vs" type="x-shader/x-vertex">  
    …… 
    attribute vec2 aTextureCoord;  
    varying vec2 vTextureCoord;  
    void main(void) {  
        …… 
        vTextureCoord = aTextureCoord; 
    } 
</script>

 

片元着色器代碼

<script id="shader-fs" type="x-shader/x-fragment"> 
    precision mediump float;  
    varying vec2 vTextureCoord;  
    uniform sampler2D uSampler;  
    void main(void) { 
        gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); 
    } 
</script>

 

在頂點着色器中定義attribute變量aTextureCoord和相應的varying變量,表示對應的頂點的紋理坐標。紋理坐標是兩個分量都在區間[0,1]之間的二維向量,是從紋理上“取色”必不可少的參數。在某種意義上,紋理就是從紋理坐標到顏色的映射。在頂點着色器看來,除了分量的個數,aTextureCoord和aVertexColor沒有區別,都是直接賦給對應的varying變量。

片元着色器接受經過光柵化的vTextureCoord,並根據其所載有的紋理坐標從紋理uSampler中讀取顏色,作為對應像素在屏幕上顯示的顏色。

6..實現環境光和漫反射下的平行光

環境光,即空氣分子和分子團的散射光,均一地影響每個像素;漫反射下的平行光均一地影響着同一個平面的所有像素,但是收到該平面與平行光的夾角的影響。

頂點着色器代碼

<script id="shader-vs" type="x-shader/x-vertex">  
    ……  
    attribute vec3 aVertexPosition; 
    attribute vec3 aVertexNormal;       
    uniform vec3 uAmbientColor; 
    uniform vec3 uLightingDirection; 
    uniform vec3 uDirectionalColor;  
    void main(void) {  
        …… 
        float directionalLightWeighting = max(dot(transformedNormal, uLightingDirection), 0.0);  
        vLightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting; 
    } 
</script>

 

片元着色器代碼

<script id="shader-fs" type="x-shader/x-fragment"> 
    precision mediump float;  
    varying vec2 vTextureCoord; 
    varying vec3 vLightWeighting;  
    uniform sampler2D uSampler;  
    void main(void) { 
        vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); 
        gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a); 
    } 
</script>

 

頂點着色器中定義了每個點的法線方向aVertexNormal(實際上是點所在平面的發現方向,但是必須對應到點上,因為頂點着色器以頂點為處理單元),還有若干uniform變量:環境光強度、平行光強度和平行光方向。根據設定的物理模型,對每個頂點返回vLightWeighting值,這個varying變量表示在上述各個變量的作用下,這個頂點顏色的各分量收到的影響。上述各個變量是如何“作用”的,不是這里討論的重點,如果你急切了解,可以獨立思考或者仔細閱讀代碼(如果了解着色器中三種不同地位的變量,相信這不是難事)。 

片元着色器逐像素地對從紋理中提取出的顏色再乘以光柵化過的vLightWeighting。

緩沖區

緩沖區是駐存於內存中的javascript對象,存儲着即將推送到着色器中的attribute對象。
最常用的attribute對象莫過於記錄了空間中點位置信息的aVertexPosition了。緩沖區如同一個長長的隊列,着色器每處理完一個頂點(或和頂點對應的其他attribute對象),緩沖區就提供下一個頂點給着色器處理。

建立一個緩沖區:

// 創建緩沖區 
vertexPositionBuffer = gl.createBuffer(); 
// 綁定緩沖區為“當前緩沖區” 
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer); 
// 為緩沖區填充數據 
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

 

值得注意的是,WebGL中只有將一個緩沖區綁定為“當前緩沖區”時,才可以對其進行操作。為緩沖區填充數據時,需要傳入一個Float32Array對象,該對象是基於數組vertices建立的,該數組存儲着所有頂點文本形式的坐標。Javascript中,數組是一個文本對象,而Float32Array對象是一個二進制對象,顯然二進制對象工作效率更高。

vertices = [ 
    -1.0, -1.0,  1.0, 
     1.0, -1.0,  1.0, 
     1.0,  1.0,  1.0, 
    -1.0,  1.0,  1.0,  
    …… 
    -1.0, -1.0, -1.0, 
    -1.0, -1.0,  1.0, 
    -1.0,  1.0,  1.0, 
    -1.0,  1.0, -1.0, 
];

 

將緩沖區中的數據推送到着色器中還需要涉及到“着色器程序”,一個負責聯系着色器和緩沖區的的Javascript對象。真是麻煩,畫一個點就需要這么多准備,不過還好着色器程序不是我們的重點,而且代碼雖長但卻很好理解。

着色器程序大致做了這樣的事情:從html文檔中讀取用x-shader語言編寫的着色器腳本,並且根據腳本生成程序,測試程序是否能夠正常運行,然后將程序中所有uniform變量和attribute變量的地址存儲到更加友好的其他Javascript對象中(通常是着色器程序自己的屬性),比如把aVertexPosition變量的地址存儲到shaderprogram.vertexPositionAttribute中:

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

 

這樣就可以將緩沖區的數據推送到着色器中了。

gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer); 
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

 

其中參數3表示緩沖區的每一個元素(這里就是每一個頂點位置了)由3個分量組成——LearningWebGL教程的例子里,將普通坐標轉化為齊次坐標的工作在頂點着色器中進行。

總之,緩沖區將結構化的三維模型數據(往往還是文本形式的)處理成着色器能夠理解變量類型,着色器運行在針對浮點運算做特殊優化的顯卡上,在片元着色器逐像素地生成顏色時,我們就要開始繪制“幀”了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM