參考官方文檔:https://learnopengl-cn.github.io/
我們知道,着色器是運行在GPU上的小程序。這些小程序爲圖形渲染管線的某個特定部分而運行。從基本意義上來說,着色器只是一種把輸入轉化爲輸出的程序。下面我們將進一步瞭解着色器以及着色器語言GLSL。
GLSL
GLSL是一種類C語言。它的開頭聲明版本,然後是輸入輸出變量、uniform和main函數。每個着色器的入口點都是main函數。
一個典型的着色器的結構如下:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main(){
//處理輸入並進行一些圖形操作
....
//輸出處理過的結果到輸出變量
out_variable_name=weird_stuff_we_processed;
}
對於頂點着色器,每個輸入變量也叫頂點屬性。我們能聲明的頂點屬性是有上限的,它由硬件來決定。OpenGL確保至少有16個包含4分量的頂點屬性可用,可以查詢GL_MAX_VERTEX_ATTRIBS
來獲取上限的具體數值。
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS,&nrAttributes);
std::cout<<"Maximum nr of vertex attributes supported:"<<nrAttributes<<std::endl;
數據類型
GLSL包含的基本數據類型:int、float、double、uint、bool。GLSL有兩種容器類型:Vector和Matrix。
向量
GLSL中的向量爲一個可以包含1-4個分量的容器,分量的類型可以是基本類型中的一種。類型如:vecn、bvecn、ivecn、uvecn、dvecn。首字母代表基本數據類型中的哪一種,字母n代表分量的數量。通過.x
、.y
、.z
、.w
來獲取各個分量。比較神奇的是向量允許特殊的分量選擇方式:重組。如:
vec2 someVec;
vec4 differentVec=someVec.xyxx;
vec3 anotherVec=differentVec.zyw;
vec4 otherVec=someVec.xxxx+anotherVec.yxzy;
它也允許:
vec2 vect=vec2(0.5,0.7);
vec4 result=vec4(vect,0.0,0.0);
vec4 otherResult=vec4(result.xyz,1.0);
就是非常靈活的。
輸入和輸出
GLSL通過in
和out
關鍵字來實現輸入輸出,進行數據交流和傳遞。對於頂點着色器,它的輸入比較特殊,我們用location
這一一元數據指定輸入變量,如layout(location=0)
,需要提供一個額外的layout
標識。而片段着色器,需要一個vec4
的顏色輸出變量。當我們需要從一個着色器向另一個着色器發送數據時,我們在發送方着色器聲明一個輸出,接收方着色器聲明一個輸入,當類型和名字都一樣時OpenGL會把兩個變量鏈接到一起,這樣它們就能發送數據了。
如:頂點着色器:
#version 330 core
layout(location =0) in vec3 aPos;
out vec4 vectexColor;
void main(){
gl_Position=vec4(aPos,1.0);
vectexColor=vec4(0.5,0.0,0.0,1.0);
}
片段着色器
#version 330 core
out vec4 FragColor;
in vec4 vertexColor;
void main(){
fragColor=vertexColor;
}
Uniform
可以在着色器中通過在類型和變量名之前添加關鍵字uniform
來聲明一個GLSL的uniform。如
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor;//在OpenGL程序代碼中設定這個變量
void main(){
FragColor=outColor;
}
我們可以在程序中:
float timeValue=glfwGetTime();
float greenValue=(sim(timeValue)/2.0f)+0.5f;
int vertexColorLocation=glGetUniformLocation(shaderProgram,"ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);
這樣最終的顏色會隨時間改變。
實際的代碼如下:
#include<iostream>
#include<glad/glad.h>
#include<GLFW/glfw3.h>
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
//用着色器語言GLSL編寫頂點着色器,然後編譯它,下面是一個非常基礎的GLSL頂點着色器的源代碼
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
//片段着色器,計算像素最後的顏色輸出
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\n\0";
int main() {
//initialize
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GlAD" << std::endl;
return -1;
}
//編譯着色器
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//檢測編譯是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILEATION_FAILED\n" << infoLog << std::endl;
}
//編譯片段着色器
int fragMentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragMentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragMentShader);
glGetShaderiv(fragMentShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragMentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILEATION_FAILED\n" << infoLog << std::endl;
}
//着色器程序。要使用前面編譯的着色器我們必須把它們鏈接爲一個着色器程序對象,然後再渲染對象時激活這個着色器程序。
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragMentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
//檢測鏈接着色器程序是否失敗
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED/n" << infoLog << std::endl;
}
//把着色器對象鏈接到程序對象之後,要刪除着色器對象
glDeleteShader(vertexShader);
glDeleteShader(fragMentShader);
//頂點輸入
float vertices[] = {
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f,
0.0f,0.5f,0.0f,
};
//頂點數組對象VAO,創建
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
//索引緩衝對象
glBindVertexArray(VAO);
//把頂點數組複製到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glViewport(0, 0, 800, 600);
void framebuffer_size_callback(GLFWwindow * window, int width, int height);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
while (!glfwWindowShouldClose(window))
{
processInput(window);
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 vectexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vectexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);//交換顏色緩衝,它在每一次迭代中被用來繪製,並作爲輸出顯示在屏幕上
glfwPollEvents();//檢測有沒有觸發什麼事件、更新窗口狀態,並調用對應的回調函數
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
實際運行時窗口呈現的三角形的顏色是會慢慢變化的,看似很神奇的操作,實際要實現卻不難。
更多屬性
但如果我們想要爲每個頂點設置一個顏色,此時可以聲明和頂點數量一樣多的uniform。但跟好的 解決方案是在頂點屬性中包含更多的數據。
例如,頂點着色器和片段着色器分別爲:
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout(location=1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
" ourColor=aColor;\n"
"}\0";
//片段着色器,計算像素最後的顏色輸出
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor,1.0);\n"
"}\n\0";
然後頂點數組和頂點格式爲:
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 // 頂部
};
...;
//位置屬性
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);
其它的不變,運行程序的結果爲:
這個結果是由於在片段着色器中進行的所謂的片段插值的結果。渲染三角形時,光柵化階段會造成比原比例指定頂點更多的片段,光柵會根據每個片段在三角形形狀上所處的相對位置決定這些片段的位置。基於這些位置,他會插值所有片段着色器的輸入變量。
寫一個着色器類
我們可以新建一個頭文件命名爲shader.h
,代碼如下:
#ifndef SHADER_H
#define SHADER_H
#include<glad/glad.h>;
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
class Shader {
public:
unsigned int ID;
Shader(const GLchar* vertexPath, const GLchar* fragmentPath) {
//1.讀取文件,獲取頂點着色器和片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
vShaderFile.close();
fShaderFile.close();
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure e) {
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
//2. 編譯着色器
unsigned int vertex, fragment;
int success;
char infolog[512];
//頂點着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertex, 512, NULL, infolog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog << std::endl;
}
//片段着色器
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragment, 512, NULL, infolog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infolog << std::endl;
}
//着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(ID, 512, NULL, infolog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILD\n" << infolog << std::endl;
}
glDeleteShader(vertex);
glDeleteShader(fragment);
}
void use() {
glUseProgram(ID);
}
void setBool(const std::string& name, bool value)const {
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string& name, int value)const {
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string& name, float value)const {
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
void del() {
glDeleteProgram(ID);
}
};
#endif
整個類沒有新的東西,就是把源程序中和着色器相關的代碼封裝到一個着色器類中,另外着色器的源代碼放到文本文件中,通過這個類來讀取文件然後編譯和鏈接。
然後主程序main.cpp
爲:
#include<iostream>
#include<glad/glad.h>
#include<GLFW/glfw3.h>
#include"shader.h"
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
//用着色器語言GLSL編寫頂點着色器,然後編譯它,下面是一個非常基礎的GLSL頂點着色器的源代碼
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout(location=1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
" ourColor=aColor;\n"
"}\0";
//片段着色器,計算像素最後的顏色輸出
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor,1.0);\n"
"}\n\0";
int main() {
//initialize
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GlAD" << std::endl;
return -1;
}
//Shader類
Shader ourShader("shader.vs", "shader.fs");
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 // 頂部
};
//頂點數組對象VAO,創建
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
//索引緩衝對象
glBindVertexArray(VAO);
//把頂點數組複製到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//位置屬性
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);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glViewport(0, 0, 800, 600);
void framebuffer_size_callback(GLFWwindow * window, int width, int height);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//激活着色器程序對象
ourShader.use();
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);//交換顏色緩衝,它在每一次迭代中被用來繪製,並作爲輸出顯示在屏幕上
glfwPollEvents();//檢測有沒有觸發什麼事件、更新窗口狀態,並調用對應的回調函數
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
ourShader.del();
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
新建兩個文本文件:shader.vs
和shader.fs
分被作爲頂點着色器和片段着色器的源代碼:
shader.vs
#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;
}
shader.fs
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main(){
FragColor=vec4(ourColor,1.0);
}
這樣就能成功運行了,結果和前面那個程序的結果相同。
這樣做確實要更好一點,讓程序的結構更清晰,也更方便我們修改。(ps:有問提的可以在博客下面評論討論交流)
練習:
- 修改頂點着色器讓三角形上下顛倒。
這個簡單,讓輸入向量的y分量變爲-y就行了。
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
out vec3 ourColor;
void main(){
gl_Position=vec4(aPos.x,-aPos.y,aPos.z,1.0);
ourColor=aColor;
}
運行結果如下:
- 使用uniform定義一個水平偏移量,在頂點着色器中使用這個偏移量把三角形移動到屏幕的右側。
頂點着色器shader.vs
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
out vec3 ourColor;
uniform float delta;
void main(){
gl_Position=vec4(aPos.x+delta,aPos.y,aPos.z,1.0);
ourColor=aColor;
}
main.cpp
...;
while(...){
...;
ourShader.use();
ourShader.setFloat("delta", 0.5f);
...;
}
- 用
out
關鍵字把頂點位置輸出到片段着色器,並將片段的顏色設置爲與頂點位置相等(來看看連頂點位置值都在三角形中被插值的結果)。做完這些後,嘗試回答下面的問題:爲什麼在三角形的左下角是黑的?
shader.vs
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
out vec3 ourColor;
void main(){
gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0);
ourColor=aPos;
}
運行結果:
顏色的RGB參數都爲0時呈現的顏色就是黑色,負數應該是當成0處理的,所以就是這樣了。左下角的座標爲:(-0.5f,-0.5f,0.0f)
。