OpenGL ES《三》,著色器GLSL基本介紹

初學(xué)OpenGL的一般都會(huì)因?yàn)橹鞅桓愕臅烆^轉(zhuǎn)向,稍微介紹下什么著色器,它有什么作用。

OpenGL著色語(yǔ)言(OpenGL Shading Language,GLSL)是用來(lái)在OpenGL中著色編程的語(yǔ)言,是一種具有C/C++風(fēng)格的高級(jí)過(guò)程語(yǔ)言,同樣也以main函數(shù)開(kāi)始,只不過(guò)執(zhí)行過(guò)程是在GPU上。GLSL使用類型限定符而不是通過(guò)讀取和寫(xiě)入操作來(lái)管理輸入和輸出。著色器主要分為頂點(diǎn)著色器(Vertex Shader)和片段著色器(Fragment Shader)兩部分。

渲染管線

下圖即是OpenGL 4.5版本的管線


管線.png
  • Vertex Data(頂點(diǎn)數(shù)據(jù)):OpenGL將所有數(shù)據(jù)保存到緩存對(duì)象當(dāng)中,正如上節(jié)當(dāng)中的glVertexAttribPointer()函數(shù)所做的工作,并調(diào)用glDrawArrays()函數(shù)請(qǐng)求渲染幾何圖元
  • Vertex Shader(頂點(diǎn)著色器):接受在頂點(diǎn)緩存對(duì)象中給出的頂點(diǎn)數(shù)據(jù),獨(dú)立處理每個(gè)頂點(diǎn)(對(duì)于繪制命令傳輸?shù)拿總€(gè)頂點(diǎn),OpenGL都會(huì)調(diào)用一個(gè)頂點(diǎn)著色器來(lái)處理頂點(diǎn)的相關(guān)數(shù)據(jù))。這個(gè)階段是必須的
  • Tessellationj shading stage(細(xì)分著色階段):這個(gè)階段是由Tessellation Control Shader(細(xì)分控制著色器)和Tessellation Evaluation Shader(細(xì)分賦值著色器)完成的。 這個(gè)階段啟用之后,會(huì)收到來(lái)自頂點(diǎn)著色階段的輸出數(shù)據(jù),并對(duì)收到的頂點(diǎn)進(jìn)行進(jìn)一步的處理,它會(huì)在OpenGL管線內(nèi)部生成新的幾何體。這是一個(gè)可選階段
  • Geometry Shader(幾何著色器):它會(huì)在OpenGL管線內(nèi)部對(duì)所有幾何圖元進(jìn)行修改,可以選擇輸入圖元生成更多的幾何體,改變幾何圖元的類型(將三角形轉(zhuǎn)化乘線段之類),或者放棄所有的幾何體。這是一個(gè)可選階段
  • Primitive Setup(圖元裝配):之前著色階段處理的都是頂點(diǎn)數(shù)據(jù),此外,這些頂點(diǎn)構(gòu)成幾何圖元的所有信息也會(huì)被傳遞到OpenGL當(dāng)中。圖元裝配階段將這些頂點(diǎn)與相關(guān)的幾何圖元之間組織起來(lái),準(zhǔn)備下一步的剪切和光柵化工作
  • Culling and Clipping(裁剪和剪切):頂點(diǎn)可能落在視口之外(即我們能夠繪制的窗口區(qū)域),此時(shí)頂點(diǎn)相關(guān)的圖元會(huì)做出改動(dòng),保證相關(guān)像素不會(huì)繪制在視口以外。由OpenGL自動(dòng)完成
  • Rasterization(光柵化):光柵化是判斷某一部分幾何體(點(diǎn)、線或者三角形)所覆蓋的屏幕空間。因?yàn)槠聊皇怯梢粋€(gè)個(gè)的像素點(diǎn)構(gòu)成的,如果要畫(huà)一條線,就要判斷這條線在哪幾個(gè)像素點(diǎn)表示,配合下圖理解:
    image.png
  • Fragment Shader(片元著色器):最后一個(gè)可以通過(guò)編程控制編程控制屏幕上顯示顏色的階段叫做片元著色階段。這個(gè)階段處理OpenGL光柵化之后生成的獨(dú)立片元,使用著色器計(jì)算片元的最終顏色和它的的深度值。這個(gè)階段是必須的
頂點(diǎn)著色器
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsOut;

void main(void)
{
    //用來(lái)展現(xiàn)紋理的多邊形頂點(diǎn)
    gl_Position = Position;
    //表示使用的紋理的范圍的頂點(diǎn),因?yàn)槭?D紋理,所以用vec2類型
    TextureCoordsOut = TextureCoords;
}

可以控制頂點(diǎn)的輸入和輸出

片段著色器
precision mediump float;

uniform sampler2D Texture;
varying vec2 TextureCoordsOut;

void main(void)
{
    //獲取紋理的像素
    vec4 mask = texture2D(Texture, TextureCoordsOut);
    
    gl_FragColor = vec4(mask.rgb, 1.0);
}

控制顏色的出輸出

數(shù)據(jù)類型

和其他編程語(yǔ)言一樣,GLSL有數(shù)據(jù)類型可以來(lái)指定變量的種類。GLSL中包含C等其它語(yǔ)言大部分的默認(rèn)基礎(chǔ)數(shù)據(jù)類型:int、float、double、uint和bool。GLSL也有兩種容器類型,分別是向量(Vector)和矩陣(Matrix)。

向量

GLSL中的向量是一個(gè)可以包含有1、2、3或者4個(gè)分量的容器,分量的類型可以是前面默認(rèn)基礎(chǔ)類型的任意一個(gè)。它們可以是下面的形式(n代表分量的數(shù)量):


向量.png

大多數(shù)時(shí)候我們使用vecn,因?yàn)閒loat足夠滿足大多數(shù)要求了。

一個(gè)向量的分量可以通過(guò)vec.x這種方式獲取,這里x是指這個(gè)向量的第一個(gè)分量。你可以分別使用.x、.y、.z和.w來(lái)獲取它們的第1、2、3、4個(gè)分量。GLSL也允許你對(duì)顏色使用rgba,或是對(duì)紋理坐標(biāo)使用stpq訪問(wèn)相同的分量。

向量這一數(shù)據(jù)類型也允許一些有趣而靈活的分量選擇方式,叫做重組(Swizzling)。重組允許這樣的語(yǔ)法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用上面4個(gè)字母任意組合來(lái)創(chuàng)建一個(gè)和原來(lái)向量一樣長(zhǎng)的(同類型)新向量,只要原來(lái)向量有那些分量即可;然而,你不允許在一個(gè)vec2向量中去獲取.z元素。我們也可以把一個(gè)向量作為一個(gè)參數(shù)傳給不同的向量構(gòu)造函數(shù),以減少需求參數(shù)的數(shù)量:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

輸入與輸出

雖然著色器是各自獨(dú)立的小程序,但是它們都是一個(gè)整體的一部分,出于這樣的原因,我們希望每個(gè)著色器都有輸入和輸出,這樣才能進(jìn)行數(shù)據(jù)交流和傳遞。GLSL定義了in和out關(guān)鍵字專門(mén)來(lái)實(shí)現(xiàn)這個(gè)目的。每個(gè)著色器使用這兩個(gè)關(guān)鍵字設(shè)定輸入和輸出,只要一個(gè)輸出變量與下一個(gè)著色器階段的輸入匹配,它就會(huì)傳遞下去。但在頂點(diǎn)和片段著色器中會(huì)有點(diǎn)不同。

另一個(gè)是片段著色器,它需要一個(gè)vec4顏色輸出變量,因?yàn)槠沃餍枰梢粋€(gè)最終輸出的顏色。如果你在片段著色器沒(méi)有定義輸出顏色,OpenGL會(huì)把你的物體渲染為黑色(或白色)。

所以,如果我們打算從一個(gè)著色器向另一個(gè)著色器發(fā)送數(shù)據(jù),我們必須在發(fā)送方著色器中聲明一個(gè)輸出,在接收方著色器中聲明一個(gè)類似的輸入。當(dāng)類型和名字都一樣的時(shí)候,OpenGL就會(huì)把兩個(gè)變量鏈接到一起,它們之間就能發(fā)送數(shù)據(jù)了(這是在鏈接程序?qū)ο髸r(shí)完成的)。

Uniform

Uniform是一種從CPU中的應(yīng)用向GPU中的著色器發(fā)送數(shù)據(jù)的方式,但uniform和頂點(diǎn)屬性有些不同。首先,uniform是全局的(Global)。全局意味著uniform變量必須在每個(gè)著色器程序?qū)ο笾卸际仟?dú)一無(wú)二的,而且它可以被著色器程序的任意著色器在任意階段訪問(wèn)。第二,無(wú)論你把uniform值設(shè)置成什么,uniform會(huì)一直保存它們的數(shù)據(jù),直到它們被重置或更新。

我們可以在一個(gè)著色器中添加uniform關(guān)鍵字至類型和變量名前來(lái)聲明一個(gè)GLSL的uniform。從此處開(kāi)始我們就可以在著色器中使用新聲明的uniform了。

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代碼中設(shè)定這個(gè)變量

void main()
{
    FragColor = ourColor;
}

我們?cè)谄沃髦新暶髁艘粋€(gè)uniform vec4的ourColor,并把片段著色器的輸出顏色設(shè)置為uniform值的內(nèi)容。因?yàn)閡niform是全局變量,我們可以在任何著色器中定義它們,而無(wú)需通過(guò)頂點(diǎn)著色器作為中介。頂點(diǎn)著色器中不需要這個(gè)uniform,所以我們不用在那里定義它。

如果你聲明了一個(gè)uniform卻在GLSL代碼中沒(méi)用過(guò),編譯器會(huì)靜默移除這個(gè)變量,導(dǎo)致最后編譯出的版本中并不會(huì)包含它,這可能導(dǎo)致幾個(gè)非常麻煩的錯(cuò)誤,記住這點(diǎn)!

這個(gè)uniform現(xiàn)在還是空的;我們還沒(méi)有給它添加任何數(shù)據(jù),所以下面我們就做這件事。我們首先需要找到著色器中uniform屬性的索引/位置值。當(dāng)我們得到uniform的索引/位置值后,我們就可以更新它的值了。這次我們不去給像素傳遞單獨(dú)一個(gè)顏色,而是讓它隨著時(shí)間改變顏色:

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

這可以在你主代碼中進(jìn)行控制,而不需要去著色器中編寫(xiě)代碼。

首先我們通過(guò)glfwGetTime()獲取運(yùn)行的秒數(shù)。然后我們使用sin函數(shù)讓顏色在0.0到1.0之間改變,最后將結(jié)果儲(chǔ)存到greenValue里。

接著,我們用glGetUniformLocation查詢uniform ourColor的位置值。我們?yōu)椴樵兒瘮?shù)提供著色器程序和uniform的名字(這是我們希望獲得的位置值的來(lái)源)。如果glGetUniformLocation返回-1就代表沒(méi)有找到這個(gè)位置值。最后,我們可以通過(guò)glUniform4f函數(shù)設(shè)置uniform值。注意,查詢uniform地址不要求你之前使用過(guò)著色器程序,但是更新一個(gè)uniform之前你必須先使用程序(調(diào)用glUseProgram),因?yàn)樗窃诋?dāng)前激活的著色器程序中設(shè)置uniform的。

因?yàn)镺penGL在其核心是一個(gè)C庫(kù),所以它不支持類型重載,在函數(shù)參數(shù)不同的時(shí)候就要為其定義新的函數(shù);glUniform是一個(gè)典型例子。這個(gè)函數(shù)有一個(gè)特定的后綴,標(biāo)識(shí)設(shè)定的uniform的類型??赡艿暮缶Y有:


image.png

現(xiàn)在你知道如何設(shè)置uniform變量的值了,我們可以使用它們來(lái)渲染了。如果我們打算讓顏色慢慢變化,我們就要在游戲循環(huán)的每一次迭代中(所以他會(huì)逐幀改變)更新這個(gè)uniform,否則三角形就不會(huì)改變顏色。

    // 渲染
    // 清除顏色緩沖
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 記得激活著色器
    glUseProgram(shaderProgram);

    // 更新uniform顏色
    float timeValue = glfwGetTime();
    float greenValue = sin(timeValue) / 2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

    // 繪制三角形
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

指定顏色到頂點(diǎn)數(shù)據(jù)

我們將把顏色數(shù)據(jù)添加為3個(gè)float值至vertices數(shù)組,三角形的三個(gè)角分別指定為紅色、綠色和藍(lán)色:

float vertices[] = {
    // 位置              // 顏色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 頂部
};

由于現(xiàn)在有更多的數(shù)據(jù)要發(fā)送到頂點(diǎn)著色器,我們有必要去調(diào)整一下頂點(diǎn)著色器,使它能夠接收顏色值作為一個(gè)頂點(diǎn)屬性輸入。需要注意的是我們用layout標(biāo)識(shí)符來(lái)把a(bǔ)Color屬性的位置值設(shè)置為1:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置變量的屬性位置值為 0 
layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值為 1

out vec3 ourColor; // 向片段著色器輸出一個(gè)顏色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // 將ourColor設(shè)置為我們從頂點(diǎn)數(shù)據(jù)那里得到的輸入顏色
}

由于我們不再使用uniform來(lái)傳遞片段的顏色了,現(xiàn)在使用ourColor輸出變量,我們必須再修改一下片段著色器:

#version 330 core
out vec4 FragColor;  
in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

因?yàn)槲覀兲砑恿肆硪粋€(gè)頂點(diǎn)屬性,并且更新了VBO的內(nèi)存,我們就必須重新配置頂點(diǎn)屬性指針。更新后的VBO內(nèi)存中的數(shù)據(jù)現(xiàn)在看起來(lái)像這樣:


image.png

知道了現(xiàn)在使用的布局,我們就可以使用glVertexAttribPointer函數(shù)更新頂點(diǎn)格式,

// 位置屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 顏色屬性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

可以參考第一節(jié)你好,三角形里面對(duì)于這函數(shù)的使用

由于我們現(xiàn)在有了兩個(gè)頂點(diǎn)屬性,我們不得不重新計(jì)算步長(zhǎng)值。為獲得數(shù)據(jù)隊(duì)列中下一個(gè)屬性值(比如位置向量的下個(gè)x分量)我們必須向右移動(dòng)6個(gè)float,其中3個(gè)是位置值,另外3個(gè)是顏色值。這使我們的步長(zhǎng)值為6乘以float的字節(jié)數(shù)(=24字節(jié))。
同樣,這次我們必須指定一個(gè)偏移量。對(duì)于每個(gè)頂點(diǎn)來(lái)說(shuō),位置頂點(diǎn)屬性在前,所以它的偏移量是0。顏色屬性緊隨位置數(shù)據(jù)之后,所以偏移量就是3 * sizeof(float),用字節(jié)來(lái)計(jì)算就是12字節(jié)。


我挑選了,認(rèn)為比較重要的知識(shí)點(diǎn)。具體可以查看LearnOpenGL CN-著色器
這網(wǎng)站寫(xiě)的詳細(xì),內(nèi)容也很多。只是這些內(nèi)容都是OpenGL的第三版,可能在學(xué)習(xí)過(guò)程會(huì)和OpenGL Es2有點(diǎn)區(qū)別,不過(guò)知識(shí)都是一樣的,稍微變化而已。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容