着色器shaders
本文整理自LearnOpenGL 及LearnOpenGL CN ,後者為前者的中文版, 完整學習的話建議前往原網站。
着色器隻是一種運作在GPU上,把輸入轉化為輸出的程式。着色器也是一種非常獨立的程式,因為它們之間不能互相通信;它們之間唯一的溝通隻有通過輸入和輸出。
GLSL(OpenGL Shader Language)
類似C的語言,必備組成部分:聲明版本,輸入和輸出變量、uniform和main函數(main函數隻能為void)。
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
{
// 處理輸入并進行一些圖形操作
...
// 輸出處理過的結果到輸出變量
out_variable_name = weird_stuff_we_processed;
}
頂點着色器中,輸入變量又叫頂點屬性(Vertex Attribute),可以聲明的頂點屬性是有限的,與硬體有關
擷取頂點屬性:
GLint nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
通常為16個。
資料類型
包含普通語言的大部分預設資料類型,
int
、
float
、
double
、
uint
、
bool
。
容器類型,向量(Vector)和矩陣(Matrix)。
向量
類型 | 含義 |
---|---|
vecn | 包含n個float分量的預設向量 |
bvecn | 包含n個bool分量的向量 |
ivecn | 包含n個int分量的向量 |
uvecn | 包含n個unsigned int分量的向量 |
dvecn | 包含n個double分量的向量 |
vecn
已經足夠用了
可以分别使用.x、.y、.z和.w來擷取它們的第1、2、3、4個分量。GLSL也允許你對顔色使用
rgba
,或是對紋理坐标使用
stpq
通路相同的分量
向量這一資料類型也允許一些有趣而靈活的分量選擇方式,叫做重組(Swizzling)
vec2 vect = vec2(, );
vec4 result = vec4(vect, , );
vec4 otherResult = vec4(result.xyz, );
輸入與輸出
着色器之間都是獨立的小程式子產品,連成一個整體,我們希望每個着色器都有輸入和輸出,這樣才能進行資料交流和傳遞。關鍵字
in
和
out
可以實作這個目的。隻要一個輸出變量與下一個着色器階段的輸入比對,它就會傳遞下去。但在頂點和片段着色器中會有點不同。
頂點着色器比較特殊,它從頂點資料中直接讀取資料,
location
這一進制資料指定輸入變量,
layout (location = 0)
。頂點着色器需要為它的輸入提供一個額外的
layout
辨別,這樣我們才能把它連結到頂點資料。
也可以忽略`layout (location = )`辨別符,通過在OpenGL代碼中使用glGetAttribLocation查詢屬性位置值(Location),這樣可以節省你(和OpenGL)的工作量。
另一個例外是片段着色器,它需要一個
vec4
顔色輸出變量,因為片段着色器需要生成一個最終輸出的顔色,如果每天定義,則會渲染成黑色或(白色)。
連結例子
頂點着色器
#version 330 core
layout (location = ) in vec3 position; // position變量的屬性位置值為0
out vec4 vertexColor; // 為片段着色器指定一個顔色輸出
void main()
{
gl_Position = vec4(position, ); // 注意我們如何把一個vec3作為vec4的構造器的參數
vertexColor = vec4(, , , ); // 把輸出變量設定為暗紅色
}
gl_Position為内建變量,表示變換後點的空間位置。頂點着色器從應用程式中獲得原始的頂點位置資料,這些原始的頂點資料在頂點着色器中經過平移、旋轉、縮放等數學變換後,生成新的頂點位置。新的頂點位置通過在頂點着色器中寫入gl_Position傳遞到渲染管線的後繼階段繼續處理。
片段着色器
#version 330 core
in vec4 vertexColor; // 從頂點着色器傳來的輸入變量(名稱相同、類型相同)
out vec4 color; // 片段着色器輸出的變量名可以任意命名,類型必須是vec4
void main()
{
color = vertexColor;
}
結果
Uniform
全局的(Global):可了解為c的全局變量,允許各個着色器随時通路,
警告:如果你聲明了一個uniform卻在GLSL代碼中沒用過,編譯器會靜默移除這個變量,導緻最後編譯出的版本中并不會包含它,這可能導緻幾個非常麻煩的錯誤,記住這點!
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
尋找着色器中uniform的位置
glUniform()
設定uniform值
因為OpenGL在其核心是一個C庫,是以它不支援類型重載,在函數參數不同的時候就要為其定義新的函數, glUniform
後面分别跟f、i、ui、3f、fv時表示需要一個float、int、unsigned int、3個float、float向量。
例子
while(!glfwWindowShouldClose(window))
{
// 檢測并調用事件
glfwPollEvents();
// 渲染
// 清空顔色緩沖
glClearColor(, , , );
glClear(GL_COLOR_BUFFER_BIT);
// 記得激活着色器
glUseProgram(shaderProgram);
// 更新uniform顔色
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / ) + ;
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, , greenValue, , );
// 繪制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, , );
glBindVertexArray();
}
代碼位址
更多屬性
把顔色加進頂點數組中,把三角形的三個角分别指定為紅色、綠色和藍色:
GLfloat vertices[] = {
// 位置 // 顔色
, -, , , , , // 右下
-, -, , , , , // 左下
, , , , , // 頂部
};
調整頂點着色器
用
layout
把color屬性的位置設定為1
#version 330 core
layout (location = ) in vec3 position; // 位置變量的屬性位置值為 0
layout (location = ) in vec3 color; // 顔色變量的屬性位置值為 1
out vec3 ourColor; // 向片段着色器輸出一個顔色
void main()
{
gl_Position = vec4(position, );
ourColor = color; // 将ourColor設定為我們從頂點資料那裡得到的輸入顔色
}
使用
outColor
代替uniform傳遞顔色
修改片段着色器
#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
color = vec4(ourColor, );
}
因為我們添加了另一個頂點屬性,并且更新了VBO的記憶體,我們就必須重新配置頂點屬性指針。更新後的VBO記憶體中的資料現在看起來像這樣:
知道了現在使用的布局,我們就可以使用glVertexAttribPointer函數更新頂點
格式,
// 位置屬性
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * sizeof(GLfloat), (GLvoid*));
glEnableVertexAttribArray();
// 顔色屬性
glVertexAttribPointer(, , GL_FLOAT, GL_FALSE, * sizeof(GLfloat), (GLvoid*)(* sizeof(GLfloat)));
glEnableVertexAttribArray();
由于我們現在有了兩個頂點屬性,我們不得不重新計算步長值。為獲得資料隊列中下一個屬性值(比如位置向量的下個x分量)我們必須向右移動6個float,其中3個是位置值,另外3個是顔色值。這使我們的步長值為6乘以float的位元組數(=24位元組)。
同樣,這次我們必須指定一個偏移量。對于每個頂點來說,位置頂點屬性在前,是以它的偏移量是0。顔色屬性緊随位置資料之後,是以偏移量就是3 * sizeof(GLfloat),用位元組來計算就是12位元組。
結果:
這是在片段着色器中進行的所謂片段插值(Fragment Interpolation)的結果。當渲染一個三角形時,光栅化(Rasterization)階段通常會造成比原指定頂點更多的片段。光栅會根據每個片段在三角形形狀上所處相對位置決定這些片段的位置。
基于這些位置,它會插值(Interpolate)所有片段着色器的輸入變量。比如說,我們有一個線段,上面的端點是綠色的,下面的端點是藍色的。如果一個片段着色器線上段的70%的位置運作,它的顔色輸入屬性就會是一個綠色和藍色的線性結合;更精确地說就是30%藍 + 70%綠。
這正是在這個三角形中發生了什麼。我們有3個頂點,和相應的3個顔色,從這個三角形的像素來看它可能包含50000左右的片段,片段着色器為這些像素進行插值顔色。如果你仔細看這些顔色就應該能明白了:紅首先變成到紫再變為藍色。片段插值會被應用到片段着色器的所有輸入屬性上。
編寫自己的着色器類
從磁盤檔案讀取着色器
定義類結構
#ifndef SHADER_H
#define SHADER_H
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <GL/glew.h>; // 包含glew來擷取所有的必須OpenGL頭檔案
class Shader
{
public:
// 程式ID
GLuint Program;
// 構造器讀取并建構着色器
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用程式
void Use();
};
#endif
實作檔案
Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
{
// 1. 從檔案路徑中擷取頂點/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保證ifstream對象可以抛出異常:
vShaderFile.exceptions(std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::badbit);
try
{
// 打開檔案
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 讀取檔案的緩沖内容到流中
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 關閉檔案
vShaderFile.close();
fShaderFile.close();
// 轉換流至GLchar數組
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const GLchar* vShaderCode = vertexCode.c_str();
const GLchar* fShaderCode = fragmentCode.c_str();
檢查是否失敗,列印錯誤日志
// 2. 編譯着色器
GLuint vertex, fragment;
GLint success;
GLchar infoLog[];
// 頂點着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, , &vShaderCode, NULL);
glCompileShader(vertex);
// 列印編譯錯誤(如果有的話)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, , NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片段着色器也類似
[...]
// 着色器程式
this->Program = glCreateProgram();
glAttachShader(this->Program, vertex);
glAttachShader(this->Program, fragment);
glLinkProgram(this->Program);
// 列印連接配接錯誤(如果有的話)
glGetProgramiv(this->Program, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(this->Program, , NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器,它們已經連結到我們的程式中了,已經不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
Use()
函數
void Use()
{
glUseProgram(this->Program);
}
調用
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag");
...
while(...)
{
ourShader.Use();
glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), );
DrawStuff();
}