1. 程式人生 > >OpenGL學習之路4----使用著色器(shader)

OpenGL學習之路4----使用著色器(shader)

本文根據教程:ogldev進行擴充學習,一步步從零開始,記錄學習歷程

一、OpenGL 渲染管線

這節相比上一節有了本質上的區別,OpenGL實際上是通過渲染管線(rendering pipeline),經過一系列的資料處理,將應用程式的資料轉換到最終渲染的影象。

在《OpenGL Programming Guide 9th》講解了渲染管線,下圖即是OpenGL 4.5版本的管線:

這裡寫圖片描述

  • Vertex Data(頂點資料):OpenGL將所有資料儲存到快取物件當中,正如上節當中的glVertexAttribPointer()函式所做的工作,並呼叫glDrawArrays()函式請求渲染幾何圖元
  • Vertex Shader(頂點著色器):接受在頂點快取物件中給出的頂點資料,獨立處理每個頂點(對於繪製命令傳輸的每個頂點,OpenGL都會呼叫一個頂點著色器來處理頂點的相關資料)。這個階段是必須的
  • Tessellationj shading stage(細分著色階段):這個階段是由Tessellation Control Shader(細分控制著色器)和Tessellation Evaluation Shader(細分賦值著色器)完成的。 這個階段啟用之後,會收到來自頂點著色階段的輸出資料,並對收到的頂點進行進一步的處理,它會在OpenGL管線內部生成新的幾何體。這是一個可選階段
  • Geometry Shader(幾何著色器):它會在OpenGL管線內部對所有幾何圖元進行修改,可以選擇輸入圖元生成更多的幾何體,改變幾何圖元的型別(將三角形轉化乘線段之類),或者放棄所有的幾何體。這是一個可選階段
  • Primitive Setup(圖元裝配):之前著色階段處理的都是頂點資料,此外,這些頂點構成幾何圖元的所有資訊也會被傳遞到OpenGL當中。圖元裝配階段將這些頂點與相關的幾何圖元之間組織起來,準備下一步的剪下和光柵化工作
  • Culling and Clipping(裁剪和剪下):頂點可能落在視口之外(即我們能夠繪製的視窗區域),此時頂點相關的圖元會做出改動,保證相關畫素不會繪製在視口以外。由OpenGL自動完成
  • Rasterization(光柵化):光柵化是判斷某一部分幾何體(點、線或者三角形)所覆蓋的螢幕空間。因為螢幕是由一個個的畫素點構成的,如果要畫一條線,就要判斷這條線在哪幾個畫素點表示,配合下圖理解:
    這裡寫圖片描述
  • Fragment Shader(片元著色器):最後一個可以通過程式設計控制程式設計控制螢幕上顯示顏色的階段叫做片元著色階段。這個階段處理OpenGL光柵化之後生成的獨立片元,使用著色器計算片元的最終顏色和它的的深度值。這個階段是必須的

二、GLSL構建頂點著色器和片元著色器

GLSL是OpenGL的著色語言,跟使用C語言作為基礎高階著色語言,通過了解渲染管線我們知道了頂點著色器和片元著色器必不可少的,所以我們用GLSL語言構建頂點著色器和片元著色器。

shader.vs(頂點著色器):

#version 330

layout (location = 0) in vec3 Position;

void main()
{
    gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
}

shader.fs(片元著色器):

#version 330

out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
#version 330
void main()
{
    // code
}
  • 每一個著色器程式與C程式類似,都是從main()函式開始
  • #version 這個預處理命令設定當前使用GLSL版本名稱,這裡#version 330 代表使用3.3版本
layout (location = 0) in vec3 Position;
  • layout(location=0) 是一個佈局限定符,作用是把緩衝區裡索引的資料繫結到我們輸入和輸出變數上,即glVertexAttributePointer()函式第一個引數所代表的索引
  • in vec3 Position:意思是宣告一個三個浮點數的輸入變數為Position
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
  • gl_Position是一個內建變數,作為輸出變數,用來儲存頂點位置的齊次座標,即就是頂點著色器輸出的頂點位置資訊儲存的地方
  • gl_Position是一個四維向量,需要4個分量,所以用vec4()構造,而引數是剛才從頂點緩衝區傳來的Position的xyz三個分量的一半,和1.0一起構成,第四個分量W其實代表透明度,設為1.0即不透明。
out vec4 FragColor;

FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  • 片段著色器裡面聲明瞭一個四維輸出向量(float型別)FragColor,用來輸出最後的顏色值
  • 直接通過vec4(1.0,0.0,0.0,1.0)賦值給FragColor,紅(R/X):1.0,綠(G/Y):0.0,藍(B/Z):0.0, 透明度(W): 1.0,所以是紅色不透明

通過上面的學習,應該對頂點著色器和片元著色器有了一個直觀的感受: 頂點著色器決定要繪製圖形的位置,片元著色器決定要繪製圖形的顏色

三、程式碼解釋

opengl_math.h

#ifndef __OPENGL_MATH_H
#define __OPENGL_MATH_H

//向量        
typedef float   Vector3f[3];                

//向量賦值
inline void LoadVector3(Vector3f v, const float x, const float y, const float z)
{
    v[0] = x; v[1] = y; v[2] = z;
}

#endif

這段數學標頭檔案沒有改變,因為還是畫一個三角形,只是這回用到了著色器,以後會有很多東西要往上新增

main.cpp:

#include <stdio.h>
#include <string>
#include <gl/glew.h>
#include <gl/freeglut.h>
#include <fstream>
#include "opengl_math.h"

using namespace std;
GLuint VBO;

const char* pVSFileName = "shader.vs";
const char* pFSFileName = "shader.fs";



static void Render()
{
    glClear(GL_COLOR_BUFFER_BIT);

    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    glDrawArrays(GL_TRIANGLES, 0, 3);

    glDisableVertexAttribArray(0);

    glutSwapBuffers();
}


static void InitializeGlutCallbacks()
{
    glutDisplayFunc(Render);
}

static void CreateVertexBuffer()
{
    Vector3f Vertices[3];

    LoadVector3(Vertices[0], -1.0f, -1.0f, 0.0f);
    LoadVector3(Vertices[1], 1.0f, -1.0f, 0.0f);
    LoadVector3(Vertices[2], 0.0f, 1.0f, 0.0f);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}

bool ReadFile(const char* pFileName, string &outFile)
{
    ifstream f(pFileName);

    bool ret = false;

    if (f.is_open()) {
        string line;
        while (getline(f, line)) {
            outFile.append(line);
            outFile.append("\n");
        }
        f.close();
        ret = true;
    }
    else {
        fprintf(stderr, "%s:%d: unable to open file `%s`\n", __FILE__,__LINE__,pFileName);
    }
    return ret;
}

static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
    GLuint ShaderObj = glCreateShader(ShaderType);
    //check if it is successful
    if (ShaderObj == 0) {
        fprintf(stderr, "Error creating shader type %d\n", ShaderType);
        exit(0);
    }

    //define shader code source
    const GLchar* p[1];
    p[0] = pShaderText;
    GLint Lengths[1];
    Lengths[0] = strlen(pShaderText);
    glShaderSource(ShaderObj, 1, p, Lengths);
    //Compiler shader object
    glCompileShader(ShaderObj);

    //check the error about shader
    GLint success;
    glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
    if (!success) {
        GLchar InfoLog[1024];
        glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
        fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
        exit(1);
    }
    //bound the shader object to shader program
    glAttachShader(ShaderProgram, ShaderObj);
}

static void CompilerShaders()
{
    //Create Shaders
    GLuint ShaderProgram = glCreateProgram();

    //Check yes or not success
    if (ShaderProgram == 0) {
        fprintf(stderr, "Error creating shader program\n");
        exit(1);
    }

    //the buffer of shader texts
    string vs, fs;
    //read the text of shader texts to buffer
    if (!ReadFile(pVSFileName, vs)) {
        exit(1);
    }
    if (!ReadFile(pFSFileName, fs)) {
        exit(1);
    }

    //add vertex shader and fragment shader
    AddShader(ShaderProgram, vs.c_str(), GL_VERTEX_SHADER);
    AddShader(ShaderProgram, fs.c_str(), GL_FRAGMENT_SHADER);

    //Link the shader program, and check the error
    GLint Success = 0;
    GLchar ErrorLog[1024] = { 0 };
    glLinkProgram(ShaderProgram);
    glGetProgramiv(ShaderProgram,GL_LINK_STATUS,&Success);
    if (Success == 0) {
        glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
        fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
        exit(1);
    }

    //check if it can be execute
    glValidateProgram(ShaderProgram);
    glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
    if (!Success) {
        glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
        fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
        exit(1);
    }

    //use program
    glUseProgram(ShaderProgram);

}

int main(int argc, char **argv)
{
    glutInit(&argc, argv);

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
    glutInitWindowSize(1024, 768);
    glutInitWindowPosition(10, 10);
    glutCreateWindow("Shader");

    InitializeGlutCallbacks();

    GLenum res = glewInit();
    if (res != GLEW_OK) {
        fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
        return 1;
    }

    printf("GL version: %s\n", glGetString(GL_VERSION));

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

    CreateVertexBuffer();

    CompilerShaders();

    glutMainLoop();

    return 0;
}

3.1 編譯著色器

CompilerShaders();

main()函式裡多了一個CompilerShaders()函式,這個函式是我們自定義的,用來完成著色器的編譯工作

我們主要任務是建立GLSL著色器物件,編譯和連結來生成可執行著色器程式。下圖給出了具體過程
這裡寫圖片描述

對於每個著色器物件,我們都需要進行以下步驟

(1) 建立一個著色器物件

GLuint ShaderObj = glCreateShader(ShaderType);
    if (ShaderObj == 0) {
        fprintf(stderr, "Error creating shader type %d\n", ShaderType);
        exit(0);
    }

GLuint glCreateShader(GLenum type)用來建立一個著色器物件,type必須是

type 說明
GL_VERTEX_SHADER 頂點著色器
GL_FRAGMENT_SHADER 片元著色器
GL_TESS_CONTROL_SHADER 細分控制著色器
GL_TESS_EVALUATION_SHADER 細分賦值著色器
GL_GEOMETRY_SHADER 幾何著色器
GL_COMPUTE_SHADER 計算著色器

(2) 將著色器原始碼編譯為物件

const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0] = strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);
glShaderSource((GLuint shader , GLsizei count ,
const GLchar **string ,const GLint *length) //將著色器原始碼關聯到一個著色器物件上
  • 第一個引數是著色器物件
  • 第二個引數是兩個陣列的元素個數,這裡只有一個
  • 第三個引數是原始碼資料(實際是一個長度為count的陣列,數組裡每個元素都是一個字串)
  • 第四個引數是對應原始碼的長度(實際上就是長度count的數組裡面每個元素字串的長度)
  • 這裡為了簡化操作就用了一個字串儲存所有的shader原始碼,用一個整形陣列儲存了原始碼長度
  • 這裡解釋的比較繞,不知道怎麼很好的表達,如果不想深究,就只用後面兩個引數記住一個是原始碼,一個是原始碼長度即可
glCompileShader(ShaderObj);
  • glCompileShader():即編譯著色器物件

(3) 驗證著色器編譯是否成功

GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
        GLchar InfoLog[1024];
        glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
        fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
        exit(1);
    }
  • 使用glGetShaderiv,並且第二個引數為GL_COMPILE_STATUS即返回編譯過程的狀態
  • glGetShaderInfoLog((GLuintshader , GLsizei bufSize,
    GLsizei *length ,char *infoLog):返回shader的編譯結果,返回一個以NULL結尾的字串,儲存在infoLog快取中,長度為length個字串。日誌返回最大值用bufSize來定義

之後要將多個著色器物件連結為一個著色器程式

(1) 建立一個著色器程式

GLuint ShaderProgram = glCreateProgram()

glCreateProgram建立一個空的著色器程式,返回值是一個非零的整數,如果為0則說明發生了錯誤

(2) 將著色器物件關聯到著色器程式

glAttachShader(ShaderProgram, ShaderObj);

將著色器物件Shaderobj關聯到著色器程式program上

(3) 連結著色器程式

glLinkProgram(ShaderProgram);

處理所有與ShaderProgram關聯的著色器物件來生成一個完整的著色器程式

(4) 判斷著色器的連結過程是否成功完成

glGetProgramiv(ShaderProgram,GL_LINK_STATUS,&Success);
if (!Success) {
        glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
        fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
        exit(1);
    }
  • glGetProgramiv()函式,使用GL_LINK_STATUS引數來查詢連結操作的結果,如果返回值是0則錯誤,通過glGetProgramInfoLog()獲取連結日誌資訊判斷錯誤原因

(5)檢查當前管線狀態,程式是否能被執行

glValidateProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
if (!Success) {
        glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
        fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
        exit(1);
    }

(6)使用著色器

glUseProgram(ShaderProgram);

3.2 渲染回撥函式

glEnableVertexAttribArray(0);
...
glDisableVertexAttribArray(0);
  • glEnableVertexAttribArray()開啟一個頂點的屬性,這裡引數為0,即開啟索引為0的頂點屬性,在渲染管線中的頂點著色器“layout (location = 0) in vec3 Position”相對應,只有開啟了索引為0的頂點屬性,頂點著色色器才能過去快取區索引為0的資料。
  • glDisableVertexAttribArray()跟上面的使能函式相對應,在進行了指定頂點著色器變數與我們儲存在快取物件中資料的關係(即著色管線裝配)和繪製命令夠,用來禁用索引相關聯的陣列

3.3 讀取GLSL原始碼

bool ReadFile(const char* pFileName, string &outFile)
{
    ifstream f(pFileName);

    bool ret = false;

    if (f.is_open()) {
        string line;
        while (getline(f, line)) {
            outFile.append(line);
            outFile.append("\n");
        }
        f.close();
        ret = true;
    }
    else {
        fprintf(stderr, "%s:%d: unable to open file `%s`\n", __FILE__,__LINE__,pFileName);
    }
    return ret;
}
  • 函式輸入引數是檔名稱和要把原始碼儲存到的字串地址
  • 函式返回一個布林型別的值,成功則為TRUE,不成功則為FALSE
  • 大概操作就是開啟檔案,之後一行一行讀取原始檔,並存儲到字串內

執行結果:

這裡寫圖片描述