OpenGL學(xué)習(xí)(二) GLSL語言基礎(chǔ)

前言

我們對頂點數(shù)組對象(VAO)和頂點緩存對象(VBO)有了初步的印象之后。我們可以繼續(xù)接觸另一個OpenGL有趣的模塊,GLSL語言。正是有了這門語言,才能在復(fù)雜的圖形編程中,做了不少簡化。

如果遇到問題請在這個地址找本人:http://www.itdecent.cn/p/9267e7b8640f

正文

接著上一篇文章,在聊GLSL之前,先介紹一個常用的工具。我們不可能每一次都在一個字符串中編寫一個GLSL的代碼。我們需要更加好用的工具,讓開發(fā)更加接近我們正常的編寫手段。

Shader的設(shè)計

實際上,在我們的編寫OpenGL過程中,發(fā)現(xiàn)有很多共性。為什么我們不把他抽象成一個類去處理呢?

我們這一次,需要一個類去控制整個著色器編譯流程。要控制其著色器程序讀取文件中的字符串,還有一個標(biāo)志位,用來判斷是否已經(jīng)編譯成功,能夠使用。

因為在OpenGL中,頂點著色器和片元著色器必須存在。因此作為構(gòu)造函數(shù)穿進去。后續(xù)就會繼續(xù)豐富這個類,加入更多的著色器。

Shader.hpp

class Shader{
public:
    unsigned int ID;
    bool isSuccess = false;
    const char* mVertexPath;
    const char* mFragmentPath;
    
public:
    Shader(const char* vertexPath,const char* fragmentPath);
    void compile();
    void use();
    
    inline bool isCompileSuccess() const{
        return isSuccess;
    }
    ~Shader();
    
private:
    bool checkComplieErrors(unsigned int shader,std::string type);
};

我們需要一個編譯所有傳到Shader中的GLSL文件compile方法。其次還需要一個使用著色器程序的方法,以及一個判斷當(dāng)前編譯是否正常的方法。以及一個私有的打印異常的方法。

Shader的實現(xiàn)

首先實現(xiàn)構(gòu)造函數(shù)。

Shader::Shader(const char* vertexPath,const char* fragmentPath){
    mVertexPath = vertexPath;
    mFragmentPath = fragmentPath;
}

接著實現(xiàn)編譯流程,我們需要讀取從文件中讀取字符串上來并且編譯

void Shader:: compile(){
    if(!mVertexPath || !mFragmentPath){
        cout<< "Error::Shader::please set file Path";
        return;
    }
    
    string vertexCode;
    string fragmentCode;
    ifstream vShaderFile;
    ifstream fShaderFile;
    
    vShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
    fShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
    
    try{
        vShaderFile.open(mVertexPath);
        fShaderFile.open(mFragmentPath);
        
        stringstream vShaderStream,fShaderStream;
        vShaderStream <<vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        
        vertexCode =vShaderStream.str();
        fragmentCode = fShaderStream.str();
        
        
    }catch(ifstream::failure e){
        cout<< "Error::Shader::file loaded fail";
    }
    
    if(vertexCode.empty()|| fragmentCode.empty()){
        return;
    }
    
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode =fragmentCode.c_str();
    
    GLuint vertexShader;
    //創(chuàng)建一個著色器類型
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //把代碼復(fù)制進著色器中
    glShaderSource(vertexShader, 1, &vShaderCode, NULL);
    //編譯頂點著色器
    glCompileShader(vertexShader);
    
    //判斷是否編譯成功
    if(!checkComplieErrors(vertexShader, "VERTEX")){
        return;
    }
    
    ///下一個階段是片段著色器
   
    GLuint fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    
    glShaderSource(fragmentShader, 1, &fShaderCode, NULL);
    
    glCompileShader(fragmentShader);
    
    if(!checkComplieErrors(fragmentShader, "Fragment")){
        return;
    }
    
    
    //鏈接,創(chuàng)建一個程序
    
    ID = glCreateProgram();
    
    //鏈接
    glAttachShader(ID, vertexShader);
    glAttachShader(ID, fragmentShader);
    glLinkProgram(ID);
    
    if(!checkComplieErrors(ID, "PROGRAM")){
        return;
    }
    
    //編譯好了之后,刪除著色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    
    
    isSuccess = true;
}

我們這邊繼續(xù)按照原來的編譯著色器程序的流程。每個著色器的都需要經(jīng)歷三個步驟,創(chuàng)建,代碼拷貝,編譯的順序。最后把所有的著色器都綁定到著色器程序上,并且鏈接起來。完成之后,需要刪除著色器。

異常警報提示如下:

bool Shader::checkComplieErrors(unsigned int shader, std::string type){
    int success;
    char infoLog[1024];
    if (type != "PROGRAM")
    {
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(shader, 1024, NULL, infoLog);
            std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            isSuccess = false;
            return false;
        }
    }else{
        glGetProgramiv(shader, GL_LINK_STATUS, &success);
        if (!success)
        {
            glGetProgramInfoLog(shader, 1024, NULL, infoLog);
            std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            isSuccess = false;
            return false;
        }
    }
    
    return true;
}

這樣就能完成了Shader的基礎(chǔ)編譯功能。

最后再提供一個use的方法,使用里面的著色器程序

void Shader:: use(){
    glUseProgram(ID);
}

通過這種方式,我們就能把冗余的代碼簡化到如下這種情況:

Shader *shader = new Shader(vPath,fPath);
    shader->compile();
    if(shader&&shader->isCompileSuccess()){
        loop(window,VAO,VBO,shader);
    }

GLSL的介紹

有了上面的工具之后,我們就能把注意力集中到編寫GLSL的語言中。讓我們介紹一下GLSL。

GLSL基礎(chǔ)類型

先介紹GLSL的支持的基礎(chǔ)類型


GLSL的支持的基礎(chǔ)類型.png

能看到除了支持我們常用的幾種基本類型,OpenGL的GLSL語言本身就支持向量和矩陣。GPU這種設(shè)計比起CPU來說優(yōu)勢太大了。因為在圖形學(xué)中矩陣可以說是極其基本的操作單位。

在這里面,我們能夠看到一些OpenGL的設(shè)計。vec(n)是指向量,向量中的是什么類型的數(shù)據(jù)通過前綴i/u/b/d來確定,默認是float類型;向量中的n是指這個向量是的分量是多少。

同理在矩陣也是一樣mat(m)(n),前綴確定了矩陣中的數(shù)據(jù)是什么類型,而M*N是指這是一個怎么樣的行列矩陣。

基本類型和C中用法差不多,這里說一下在GLSL中矩陣的操作:

      vec3 v = vec3(0.0,2.0,3.0);
     ivec3 s = ivec3(v);
     
     
     vec4 color;
     vec3 rgb = vec3(color);//這樣就能截斷前三個數(shù)據(jù)

這里有這么一個規(guī)律,當(dāng)分量更多的向量賦值給更加少的向量賦值的時候,其行為就和我們寫c語言時候,int往char轉(zhuǎn)化一樣,丟失位數(shù)。通過這種做法能夠獲取截斷的數(shù)據(jù)。

向量的賦值也很自由:

     vec3 white = vec3(1.0);//white = (1.0,1.0,1.0)
     vec4 t = vec3(white,0.5);//(1.0,1.0,1.0,0.5)

能夠任意的傳入向量到另一個向量,從而做到賦值或者添加新的變量。當(dāng)傳入單一值的時候,就會默認讓向量中所有的值都是一致。

矩陣的設(shè)置:

     //相當(dāng)于為對角賦值4.0
     m =  mat3(4.0) = (4.0,0,0
                       0,4.0,0
                       0,0,4.0)
     
     mat3 M = mat3(1.0,2.0,3.0,
                    4.0,5.0,6.0,
                    7.0,8.0,9.0);
   
     

我們設(shè)置矩陣的時候可以設(shè)置單一的值,這樣就能設(shè)置矩陣的一條對角線上所有的值。

     vec3 c1 = vec3(1.0,2.0,3.0);
     vec3 c2 = vec3(4.0,5.0,6.0);
     vec3 c3 = vec3(7.0,8.0,9.0);
     mat3 M = mat3(c1,c2,c3);
     
     vec2 c1 = vec2(1.0,2.0);
     vec2 c2 = vec2(4.0,5.0);
     vec2 c3 = vec2(7.0,8.0);
     
     mat3 M = mat3(c1,3.0,
                    c2,6.0,
                    c3,9.0);
     
     先設(shè)置列,在設(shè)置行
     這些結(jié)果都是矩陣:(1.0 4.0 7.0
                     2.0 5.0 8.0
                    3.0 4.0 9.0)

OpenGL中還能通過幾個向量組成一個mat矩陣。不過,其設(shè)置的順序是按照列的順序豎直的向后擺放。也就是按照順序設(shè)置行列式中的列。

當(dāng)然,向量的分量或者矩陣,都可以看成一段特殊數(shù)組,可以通過[]和.訪問到每個分量。
實際上為了便于分辨每個向量的作用,因此將每個每個位置設(shè)置為:


image.png
 vec3 l = color.rrr
 這樣相當(dāng)于取了3次r位置
 
 color = color.abgr//反轉(zhuǎn)每個color位置
 
 但是 vec2 pos;
 float zPos = pos.z;//2d不存在z
 
 還能如此:
 mat4 m = mat4(2.0);
 vec4 = z = m[2];  //這樣就能獲得4*4矩陣的第二列
 
 float y = m[1][1].這樣能獲得第一列第一行的數(shù)據(jù)。

能夠看到的是,這種方式其實和Octave和Matlab十分相似。

當(dāng)然還能構(gòu)造結(jié)構(gòu)體,并且用“.”訪問里面的屬性

struct pos{
  vec3 pos;
  vec3 v;
  float life;
};

pos p = pos(pos,v,10.0);

數(shù)組

float c[3];//3個float數(shù)組
float[3] c;
int i[];//為定義數(shù)組維度,可以稍后定義。

for(int i = 0;i < c.length;i++){
    c[i] *= 2.0;
}


同理,我們可以把矩陣看成n*n維度的數(shù)組
因此:
mat3x4 m;
int c = m.length();
int r = m[0].length();//獲取第一列的長度

mat4 m;
float d[m.length()];//設(shè)置長度為矩陣大小。

float x[gl_in.length()].設(shè)置數(shù)組大小為頂點數(shù)組大小

實際上數(shù)組還是和Java語言中的差不多。

存儲限制符

存儲限制符是十分重要的一個概念,其控制了變量的作用。其中uniform尤為的重要。

  • const 讓一個變量變成只讀。如果初始化是編譯時常量,那么本身就是編譯時常量。

  • in 設(shè)置這個變量為著色器階段的輸入變量

  • out 設(shè)置這個變量為著色器階段輸出變量

  • uniform 設(shè)置這個變量為用戶應(yīng)用傳遞給著色器數(shù)據(jù),他對于給定的圖元而言,是一個常量。uniform變量在所有可用的著色階段之間共享,必須定義為全局變量。任何變量的變量都可以是uniform變量。著色器無法寫入,也無法改變。

uniform vrc4 color;

在著色器中,可以根據(jù)名字color來引用這個變量,但如果需要在用戶應(yīng)用中設(shè)置他的值,需要一些工作。

GLSL編譯器會在鏈接著色器程序時創(chuàng)建一個uniform變量表。如果需要設(shè)置應(yīng)用程序中的color值,首先獲得color在列表中的索引,通過下面這個這個函數(shù)完成。

GLuint glGetuniformLocation(GLuint program,const char* name)

返回著色器程序中uniform變量name對應(yīng)的索引值。name是一個以NULL結(jié)尾的字符串,不存在空格。如果name與啟用的著色器程序所有的uniform都不相符或者name是一個內(nèi)部保留的變色齊變量名稱,則返回-1.

name 可以是單一變量名稱,數(shù)組一個元素,或者結(jié)構(gòu)體中一個域變量。

對于uniform變量數(shù)組,也可以只通過制定數(shù)組的名稱來獲取數(shù)組的第一個元素。

除非重新鏈接程序,不然這里的返回值不會變。

得到uniform變量對應(yīng)索引值之后,我們可以通過glUnform()或者glUniformMatrix()系列函數(shù)來設(shè)置uniform變量值。

例子:

//GLSL中:
float time;

//opengl編程中:
GLint timeLoc;
GLfloat timevalue;
int timeLoc = glGetUniformLocation(program,"time");
glUniform(timeLoc,timevalue);

獲取以及寫入 uniform

void glUniform{1234}{fdi ui}(GLint location,TYPE value);

void glUniform{1234}{fdi ui}v (GLint location,GLsizei count,TYPE value);

void glUniformMatrix{234}{fdi ui}v (GLint location,GLsizei count,GLboolean transpose,GLfloat *value);

void glUniformMatrix{2x2,2x4,3x2,3x4,4x2,4x3}{fdi ui}v (GLint location,GLsizei count,GLboolean transpose,GLfloat *value);

設(shè)置與location索引位置對應(yīng)的uniform變量的值。其中向量形式的函數(shù)會載入count個數(shù)據(jù)的集合,并寫入location位置的uniform變量。如果location是數(shù)組的起始索引,那么數(shù)組之后的連續(xù)count會被載入。

GLfloat形式的函數(shù),可以載入但精度或者雙精度的。

tranpose設(shè)置為GL_TRUE,那么values中的數(shù)據(jù)是以行順序讀入,否則按照行。

  • buffer 和uniform很相似,設(shè)置應(yīng)用共享一塊可讀寫的內(nèi)存,成為著色器的存儲緩存

  • shared 設(shè)置變量時本地工作組中共享,只能用于計算著色器。

可以看到uniform和buffer都是OpenGL中GLSL語言和我們在CPU編程的程序傳遞數(shù)據(jù)的接口,十分重要。

控制流

和普遍的語言一模一樣

但是多了discard終止著色器程序執(zhí)行。

參數(shù)限制符

盡管GLSL中函數(shù)可以在運行之后修改和返回數(shù)據(jù)。但是它與C中不一樣,沒有引用和指針。不過與之對應(yīng),此時函數(shù)參數(shù)可以制定一個參數(shù)限定符號,來表明它是否需要在函數(shù)運行時將數(shù)據(jù)拷貝到函數(shù),或者從函數(shù)中返回修改的數(shù)據(jù)。

image.png

如果我們寫出到一個沒有設(shè)置上述修飾符的變量,會產(chǎn)生編譯時錯誤。

如果我們需要在編譯時驗證函數(shù)是否修改了某個輸入變量,可以使用const in類型變量來組織函數(shù)對變量進行寫操作。不這么做,那么在函數(shù)中寫入一個in類型的變量,相當(dāng)于變量的局部拷貝進行了修改,因此只在函數(shù)自身范圍內(nèi)產(chǎn)生作用。

計算不變性

很有趣的是,glsl無法保證在不同著色器中,兩個完全相同的計算世會得到完全一樣的結(jié)果。這個情形與cpu端應(yīng)用進行計算問題相同,即不同優(yōu)化的方式會導(dǎo)致結(jié)果非常細微的差異。

為了確定不變性有兩個關(guān)鍵字:

  • invariant
  • precise

這兩個方法都需要在圖形設(shè)備上完成計算過程,來確保同一表達式的結(jié)果可以保證重復(fù)性。但是,對于宿主計算機和圖形硬件格子計算,這兩個方法無法保證結(jié)果完全一致。

著色器編譯時的常量表達式是由編譯器的宿主計算機計算的,因此我們無法保證宿主計算機計算的結(jié)果與圖形硬件的結(jié)果完全相同。

uniform float ten;//假設(shè)應(yīng)用程序設(shè)置這個值為10.0
const float f = sin(10.0);//宿主編譯器負責(zé)計算
float g = sin(ten); //圖形硬件負責(zé)計算
void main(){
    if(g == f){
        //這兩個不一定相等
    }
}

invariant

invariant限制符可以設(shè)置任何著色器的輸出變量。他可以確保兩個著色器的輸出變量使用了同樣的表達式,并且表達式變量也是相同,計算結(jié)果也是相同。

invariant gl_Position;
invariant centroid out vec3 Color;

輸出變量作用是將一個著色器的數(shù)據(jù)從一個階段傳遞到下一個??梢栽谥饔玫侥硞€變量或者內(nèi)置變量之前的任何位置,對該變量設(shè)置關(guān)鍵字invariant。

在調(diào)試過程中可能需要全部變量編程invariant

#pragma STDGL invariant(all)

但是這樣對于著色器也會有所影響。

precise

precise 限制符可以設(shè)置任何計算中的變量或者函數(shù)的返回值。不是增加精確度,而是增加計算的可復(fù)用。

我們通常在細分著色器用它避免幾何形狀的裂縫。

總體來說,如果必須保證某個表達式產(chǎn)生結(jié)果是一致的,即使表達式中的數(shù)據(jù)發(fā)生了變化也是如此,那么此時我們此時應(yīng)該用precise而非invariant。

下面a和b的值發(fā)生交換,得到的結(jié)果也是不變。
此外即使c和d的值發(fā)生變換,或者a和c同時與b和d發(fā)生交換,也要計算相同結(jié)果。

Location = a * b + c *d;

precise可以設(shè)置內(nèi)置變量,用戶變量或者函數(shù)的返回值。

precise gl_Position;
precise out vec3 Location;
precise vec3 subdivide(vec3 p1,vec3 p2){...}

著色器,關(guān)鍵字precise可以使用某個變量的任何位置設(shè)置個變量,并且可以修改已經(jīng)聲明過的變量。

編譯器使用precise 一個實際影響,類似上面的表達是不能在使用兩種不同的乘法命令同時參與計算。

例如第一次是普通乘法,第二次相乘使用混合乘加預(yù)算,因為這兩個命令對于同一組值的計算可能可能出現(xiàn)微笑差異,而這種差異precise是不允許,因此編譯會組織你在代碼這么做。

但是混乘對性能提升很重要,因此GLSL提供了一個內(nèi)置函數(shù)fma()代替原來的操作。

小結(jié)

可能這樣還是很抽象,為什么GPU需要計算不變性而CPU不需要呢?這里有個神圖,一看就懂。


cpu和gpu硬件邏輯結(jié)構(gòu)對比.png

其中Control是控制器、ALU算術(shù)邏輯單元、Cache是cpu內(nèi)部緩存、DRAM就是內(nèi)存。
能看到GPU有許許多多的小的計算單元。而CPU則是有一個很大的強大計算模塊,而GPU則是有很多不是那么強大的計算單元。

舉個例子,CPU的計算單元相當(dāng)于是一個大學(xué)教授在計算,而GPU則是成千上百和小學(xué)生在計算。由于計算機大部分都是由簡單的計算邏輯組成,一個強大的大腦當(dāng)然架不住很多簡單大腦共同計算的速度快,因此GPU作為大量的并行計算工具是首選。這也是為什么在人工智能,大數(shù)據(jù)選擇GPU作為計算工具也是這個原因。

話題又轉(zhuǎn)回來。由于GPU本身含有大量的計算單元,這樣就造成了,計算可能會因為每個計算單元的情況而出現(xiàn)微小的差異。因此GLSL需要計算不變性。

invariant 保證了每個階段著色器的輸出在使用相同的表達式,結(jié)果一致。
precise 保證了在著色器編程內(nèi)部使用相同的表達式,結(jié)果一致。

實戰(zhàn)演練 Uniform數(shù)據(jù)接口

介紹到這里,一個粗略的GLSL的語言總覽已經(jīng)有了。接下來就讓我們實戰(zhàn)演練一番。

我們在上一篇文章基礎(chǔ)上,讓整個Uniform增加隨著時間的顏色變化而變化。

我們先編寫一個頂點著色器的GLSL:
文件:veritex.glsl

#version 330 core
layout(location = 0) in vec3 aPos;
void main(){
    gl_Position = vec4(aPos,1.0);
}

這樣我就把頂點著色的輸出設(shè)置為分量為4的向量,在原來的方位的向量上擴展為一個w分量(暫時不用理)。
文件:fragment.glsl

#version 330 core
out vec4 mFragColor;
uniform vec4 mChangingColor;
void main(){
    mFragColor = mChangingColor;
}

在片元著色器上,渲染的顏色由片元著色器決定。

完成這兩個之后,我們只需要編寫如下代碼,編譯著色器程序,利用C++ 11特性回調(diào)Loop中的事件調(diào)用。

 const char* vPath = "/Users/yjy/Desktop/iOS workspcae/first_opengl/glsl/veritex.glsl";
    const char* fPath = "/Users/yjy/Desktop/iOS workspcae/first_opengl/glsl/fragment.glsl";
    Shader *shader = new Shader(vPath,fPath);
    shader->compile();
    if(shader&&shader->isCompileSuccess()){
        mixColorTri(VAO,VBO);
        
        loop(window,VAO,VBO,shader,[](Shader *shader){
            //更新uniform顏色
            float timeValue = glfwGetTime();
            float colorValue = sin(timeValue) / 2.0 + 0.5f;
            //拿到uniform的位置索引
            int vertexColorLocation = glGetUniformLocation(shader->ID,"mChangingColor");
            glUniform4f(vertexColorLocation, colorValue, 0.0f, 0.0f, 1.0f);
        });
    }

flushTriangleToOpengl這個方法就是和上一篇文章創(chuàng)造并綁定頂點緩存對象和頂點數(shù)組對象,最后設(shè)定步長。

void flushTriangleToOpengl(GLuint& VAO,GLuint& VBO){
    //我們要繪制三角形
    float vertices[] = {
        //位置             
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f,
        0.0f,  0.5f, 0.0f
    };
    
    //生成分配VAO
    glGenVertexArrays(1,&VAO);
    //生成一個VBO緩存對象
    glGenBuffers(1, &VBO);
    
    //綁定VAO,注意在core模式,沒有綁定VAO,opengl拒絕繪制任何東西
    glBindVertexArray(VAO);
    
    
    //綁定VBO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //復(fù)制頂點給opengl緩存
    //類型為GL_ARRAY_BUFFER 第二第三參數(shù)說明要放入緩存的多少,GL_STATIC_DRAW當(dāng)畫面不懂的時候推薦使用
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    //設(shè)定頂點屬性指針
    //第一個參數(shù)指定我們要配置頂點屬性,對應(yīng)vertex glsl中l(wèi)ocation 確定位置
    //第二參數(shù)頂點大小,頂點屬性是一個vec3,由3個值組成,大小是3
    //第三參數(shù)指定數(shù)據(jù)類型,都是float(glsl中vec*都是float)
    //第四個參數(shù):是否被歸一化
    //第五參數(shù):步長,告訴我們連續(xù)的頂點屬性組之間的間隔,這里是每一段都是3個float,所以是3*float
    //最后一個是偏移量
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,  3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0);
    
}

最后,我們利用C++ 11特性創(chuàng)建一個回調(diào)把,在創(chuàng)建一個事件循環(huán):

void loop(GLFWwindow *window,const GLuint VAO,const GLuint VBO,Shader *shader,function<void(Shader*)> handle){
    while(!glfwWindowShouldClose(window)){
        processInput(window);
        //交換顏色緩沖,他是一個存儲著GLFW窗口每一個像素顏色值的大緩沖
        //會在這個迭代中用來繪制,并且顯示在屏幕上
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        //我們已經(jīng)告訴,程序要數(shù)據(jù)什么數(shù)據(jù),以及怎么解釋整個數(shù)據(jù)
        //數(shù)據(jù)傳輸之后運行程序
        //glUseProgram(shaderProgram);
        shader->use();
        
        if(handle){
            handle(shader);
        }
        
        
        
        //綁定數(shù)據(jù)
        glBindVertexArray(VAO);
        //繪制一個三角形
        //從0開始,3個
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        //繪制矩形
        //glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
        
        glBindVertexArray(0);
        
        glfwSwapBuffers(window);
        //檢查有沒有觸發(fā)事件,鍵盤輸入,鼠標(biāo)移動,更新噶 u 那個口耦
        glfwPollEvents();
    }
    
    glDeleteVertexArrays(1,&VAO);
    glDeleteBuffers(1, &VBO);
    
    glfwTerminate();
}

其核心就是這里,通過glGetUniformLocation獲取uniform的索引,接著通過glUniform4f方法設(shè)置一個4個分量的顏色向量到片元著色器中著色。

這樣就能做到,讓整個三角形顏色隨著時間動起來,從淺紅一直變成深紅。


example_red.gif

實戰(zhàn)演練 GLSL頂點著色器layout的妙用

實際上在頂點著色器中有這么一行:

layout(location = 0) in vec3 aPos;

layout中有l(wèi)ocation的字段。接下來,來看看這個字段是怎么運作。

還記得,glVertexAttribPointer這個方法指定了OpenGL如何解析讀取頂點數(shù)據(jù)的方法。

這個方法的第一個參數(shù)實際上就是指定的是,將會寫入到哪一個location的對應(yīng)的in數(shù)據(jù)。

假設(shè)我們有這么一堆頂點數(shù)據(jù):

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
    };

我們想要設(shè)置前3個是位置信息,而后三個是顏色信息的坐標(biāo)參數(shù),又是該如何解析呢?

void mixColorTri(GLuint& VAO,GLuint& VBO){
    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
    };
    
    glGenVertexArrays(1,&VAO);
    glGenBuffers(1,&VBO);
    
    //綁定
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    //綁定數(shù)據(jù)
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
    
    //告訴opengl 每6個頂點往后讀取下一個數(shù)據(jù)
    //這個0代表了 layout(location = 0)
    //最后一個參數(shù)是偏移量
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),0);
    glEnableVertexAttribArray(0);
    
    //走3個float的偏移量,開始讀取數(shù)據(jù)
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    
    
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0);
    
}
glVertexAttribPointer讀取顏色頂點原理

那么對應(yīng)的頂點著色器呢?

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
out vec3 ourColor;

void main(){
    gl_Position = vec4(aPos,1.0);
    ourColor = aColor;
}

片元著色器:

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

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

記住每個著色器傳遞數(shù)據(jù)out和in之間的變量命名要一致,不然會出現(xiàn)著色器程序鏈接異常。

image.png

實戰(zhàn)演練 移動三角形

當(dāng)我們變化顏色,處理更多的頂點信息了,就會想到怎么把這個三角形動起來。
編寫一個頂點著色器

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
uniform float offset;
out vec3 ourColor;
void main(){
    gl_Position = vec4(aPos.x + offset,aPos.y,aPos.z,1.0);
    ourColor = aColor;
}

接著為了方便我們處理uniform的數(shù)據(jù)設(shè)置,我們在Shader類新增下面方法:

void Shader::setBool(const std::string &name,bool value) const{
    glUniform1i(glGetUniformLocation(ID,name.c_str()),(int)value);
}
void Shader:: setInt(const std::string &name,int value) const{
    glUniform1i(glGetUniformLocation(ID,name.c_str()),value);
}
void Shader:: setFloat(const std::string &name,float value) const{
    glUniform1f(glGetUniformLocation(ID,name.c_str()),value);
}

接著把這行改造成如下:

if(shader&&shader->isCompileSuccess()){
        mixColorTri(VAO,VBO);
        static float init = 0.0f;
        loop(window,VAO,VBO,shader,[](Shader *shader){
            //更新uniform顏色
            init += 0.005;

            shader->setFloat("offset", init);
        });
    }

就能看到這個三角形快速的移動。


移動的三角形

總結(jié)

經(jīng)過這幾個實戰(zhàn)演練,是不是對OpenGL的GLSL的理解,uniform以及對頂點數(shù)組對象又有了更加深刻的理解呢?

實際上GLSL語言還有不少特性,如buffer,子程序,獨立的著色器程序等高級應(yīng)用還沒涉及到。看來革命尚未成功,繼續(xù)努力才是

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

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