1. 什么是著色器
簡單來說封字,著色器就是運(yùn)行在 GPU 上的一段程序代碼在渲染管線流程中黔州,運(yùn)行著色器是非常重要的步驟,可以這么理解:CPU 將需要渲染的數(shù)據(jù)提交到 GPU阔籽,著色器將這些數(shù)據(jù)作為輸出流妻,執(zhí)行著色器中的邏輯,最終輸出每個像素的顏色笆制。著色器是 GPU 程序合冀,那么用來編寫程序的語言是什么呢?最常見的著色器語言包括三種
- GLSL OpenGL Shading Language
OpenGL 提供的著色器語言项贺,以C語言為基礎(chǔ) - HLSL High Level Shading Language
高級著色語言君躺,由微軟開發(fā)并提供給微軟的 Direct3D 和 XNA 使用,HLSL 是 GLSL 的先輩开缎,不能與 OpenGL 兼容 - CG C for Graphics
轉(zhuǎn)為 GPU 編程設(shè)計的高級著色語言棕叫,由 Nvidia 公司開發(fā)
我們主要看OpenGL 中的 GLSL 著色語言
2. 著色器在什么時候執(zhí)行
著色器執(zhí)行是渲染管線中的可編程部分,頂點(diǎn)著色器和片段著色器是分開執(zhí)行的奕删,在繪制一個物體時俺泣,通常情況下GPU 會對物體的每一個頂點(diǎn)調(diào)用一次頂點(diǎn)著色器,得到 n 個頂點(diǎn)著色器的輸出數(shù)據(jù),然后對每一個被物體覆蓋到的片段(像素)調(diào)用一次片段著色器伏钠,它的輸入數(shù)據(jù)是這樣得到的:
根據(jù)該像素中心點(diǎn)距離物體上覆蓋到像素的三個頂點(diǎn)距離進(jìn)行插值得到横漏,最終片段著色器返回出一個該片段的顏色,進(jìn)入到下一個渲染階段
3. GLSL 結(jié)構(gòu)分析
3.1 使用文件中的著色器代碼
這篇文章我們主要針對著色器進(jìn)行分析熟掂,而渲染的頂點(diǎn)數(shù)據(jù)是固定的缎浇,為了便于分析,將著色器代碼解耦出來赴肚,我們添加一個頭文件 shader_reader.h
素跺,實(shí)現(xiàn)一個讀取著色器代碼的工具方法
#ifndef SHADER_READER_H
#define SHADER_READER_H
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
using namespace std;
const GLchar* read_shader(const GLchar* path)
{
stringstream sourceStream;
sourceStream << "../valor/shaders/" << path << ".shader";
ifstream reader;
stringstream shaderStream;
reader.exceptions(ifstream::badbit);
try {
reader.open(sourceStream.str().c_str());
shaderStream << reader.rdbuf();
cout << "read:" << shaderStream.rdbuf() << endl;
reader.close();
}
catch (ifstream::failure e)
{
cout << "exception:" << endl;
}
cout << shaderStream.str() << endl;
const char* result;
result = _strdup(shaderStream.str().c_str());
cout << result << endl;
return result;
}
#endif
新建 shaders 目錄并添加最簡單的頂點(diǎn)和片段著色器 simplest_vext.shader 和 simplest_frag.shader 文件
# version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.xyz, 1.0);
}
#version 330 core
void main()
{
// 返回紅色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
使用 [OpenGL]繪制三角形中的三角形繪制代碼,只不過將用于編譯的著色器字符串由原本寫死的改為從文件讀取的誉券,渲染部分的代碼:
int draw()
{
// 初始化 OpenGL
init_opengl();
const GLchar* vertexShader = read_shader("simplest_vert");
const GLchar* fragmentShader = read_shader("simplest_frag");
// 編譯和鏈接著色器
GLuint shaderProgram = compile_shader(vertexShader, fragmentShader);
// 頂點(diǎn)數(shù)據(jù)
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
// 申請緩沖區(qū)
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 綁定VAO指厌,表示
glBindVertexArray(VAO);
// 提交數(shù)據(jù)
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 使用Buffer中的數(shù)據(jù)在 VAO 生成 0 號頂點(diǎn)屬性的指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
// 啟用 0 號頂點(diǎn)屬性
glEnableVertexAttribArray(0);
// 解綁VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解綁VAO
glBindVertexArray(0);
while (!glfwWindowShouldClose(window))
{
// 處理事件
glfwPollEvents();
// 清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 指定著色器程序
glUseProgram(shaderProgram);
// 綁定VAO
glBindVertexArray(VAO);
// 繪制指令
glDrawArrays(GL_TRIANGLES, 0, 3);
// 解綁 VAO
glBindVertexArray(0);
// 雙緩沖交換
glfwSwapBuffers(window);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
使用讀取的著色器代碼來進(jìn)行繪制,得到了一個紅色的三角形:
3.2 分析著色器結(jié)構(gòu)
典型的著色器結(jié)構(gòu)如下
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
// 處理輸入并進(jìn)行一些圖形操作
...
// 輸出處理過的結(jié)果到輸出變量
out_variable_name = weird_stuff_we_processed;
}
-
version 聲明著色器版本號
-
in
表示著色器的輸入變量踊跟,頂點(diǎn)著色器輸入的是頂點(diǎn)屬性數(shù)據(jù)踩验,通常還會通過制定layout(location=n)
的方式來指定該變量讀取的是哪一個頂點(diǎn)屬性,片段著色器的輸入是頂點(diǎn)著色器的輸出數(shù)據(jù) -
out
表示著色器的輸出商玫,頂點(diǎn)著色器中輸出的通常是頂點(diǎn)的裁剪坐標(biāo)箕憾、頂點(diǎn)顏色、uv 等信息决帖,片段著色器輸出的是該片段的最終顏色值 -
uniform
是一種從 CPU 向GPU著色器發(fā)送數(shù)據(jù)的方式厕九,uniform是全局的,所有的著色器程序?qū)ο蠊蚕淼臄?shù)據(jù)地回,可以被著色器程序的任意著色器在任意階段訪問扁远,并且無論你把uniform值設(shè)置成什么,uniform會一直保存它們的數(shù)據(jù)刻像,直到它們被 CPU 端重置或更新
3.3 在著色器中加入頂點(diǎn)顏色
其實(shí)在之前的文章 [OpenGL]VBO畅买,VAO和EBO詳解我們已經(jīng)使用了頂點(diǎn)的顏色數(shù)據(jù),這里我們修改一下著色器程序细睡,使他支持頂點(diǎn)顏色數(shù)據(jù):
# version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec4 color;
out vec4 vert_color;
void main()
{
gl_Position = vec4(position.xyz, 1.0);
vert_color = color;
}
頂點(diǎn)著色器所做的修改包括:
- 增加
in
輸入變量 color谷羞,指定使用 1 號頂點(diǎn)屬性 - 增加
out
輸出變量,將 color 直接返回
#version 330 core
in vec4 vert_color;
void main()
{
gl_FragColor = vert_color;
}
片段著色器所做的修改包括:
- 增加一個輸入變量溜徙,接收頂點(diǎn)著色器輸出的值
- 將頂點(diǎn)顏色作為像素的顏色直接返回
三角形繪制代碼湃缎,主要是增加了顏色緩沖區(qū)數(shù)據(jù)的處理,并為 VAO 提供顏色數(shù)據(jù)的解析
int draw()
{
// 初始化 OpenGL
init_opengl();
const GLchar* vertexShader = read_shader("vertColor_vert");
const GLchar* fragmentShader = read_shader("vertColor_frag");
// 編譯和鏈接著色器
GLuint shaderProgram = compile_shader(vertexShader, fragmentShader);
// 三角形1頂點(diǎn)數(shù)據(jù)
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f, // Top
};
// 顏色數(shù)據(jù)
GLfloat colors[] = {
1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f
};
// 申請緩沖區(qū)
GLuint VBO, ColorVBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &ColorVBO);
// 綁定VAO
glBindVertexArray(VAO);
// 提交數(shù)據(jù)和解析規(guī)則
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
// 解綁VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 綁定VAO
glBindVertexArray(VAO);
// 解綁VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解綁VAO
glBindVertexArray(0);
while (!glfwWindowShouldClose(window))
{
// 處理事件
glfwPollEvents();
// 清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 指定著色器程序
glUseProgram(shaderProgram);
// 綁定VAO
glBindVertexArray(VAO);
// 繪制指令
glDrawArrays(GL_TRIANGLES, 0, 3);
// 解綁 VAO
glBindVertexArray(0);
// 雙緩沖交換
glfwSwapBuffers(window);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
得到的渲染結(jié)果:
3.4 在著色器代碼中加入 uniform 變量
保持頂點(diǎn)著色器不變蠢壹,修改片段著色器代碼嗓违,增加一個 float 類型的 uniform 變量,并將返回顏色的3個分量都設(shè)置為該變量的值:
#version 330 core
in vec4 vert_color;
uniform float r;
void main()
{
gl_FragColor = vec4(r, r, r, 1.0);
}
uniform 類型的變量需要我們從 CPU 端傳遞給 GPU图贸,我們考慮這樣的邏輯:將鼠標(biāo)屏幕坐標(biāo)的 x 分量映射到 [0, 1]之間蹂季,然后通過 uniform 變量傳遞給這段片段著色器代碼冕广,以返回不同的程度的灰色,在渲染循環(huán)中增加如下的邏輯來獲取鼠標(biāo)的坐標(biāo)
// 獲取光標(biāo)坐標(biāo)
GLdouble xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
那么該如何實(shí)時的將 uniform 變量傳遞到 GPU 端對應(yīng)的著色器程序呢偿洁?使用 glGetUniformLocation
返回某個 uniform 變量的位置撒汉,需要提供著色器程序和變量名,拿到位置后涕滋,使用 glUniform
系列的方法將參數(shù)傳遞過去
// 指定著色器程序
glUseProgram(shaderProgram);
// 獲取變量在著色器中的位置
GLint rColorLocation = glGetUniformLocation(shaderProgram, "r");
// 往對應(yīng)位置傳遞參數(shù)
glUniform1f(rColorLocation, xpos / WIDTH);
渲染結(jié)果如下圖所示睬辐,當(dāng)在水平方向移動鼠標(biāo)時,三角形的顏色將發(fā)生變化
3.5 將三角形上下倒置
只需要在頂點(diǎn)著色器中何吝,將返回的頂點(diǎn)坐標(biāo) y 分量取反(歸一化坐標(biāo) y 方向取值范圍是 [-1, 1]):
# version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec4 color;
out vec4 vert_color;
void main()
{
gl_Position = vec4(position.x, 0 - position.y, position.z, 1.0);
vert_color = color;
}
倒置后的三角形