OpenGL-03-着色器
前文的文本来自:https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/
本文主要做个人总结与简化
GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。
数据类型与向量
和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:
int
、
float
、
double
、
uint
和
bool
。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
向量
GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(
n
代表分量的数量):
类型 | 含义 |
---|---|
|
包含
个float分量的默认向量 |
|
包含
个bool分量的向量 |
|
包含
个int分量的向量 |
|
包含
个unsigned int分量的向量 |
|
包含
个double分量的向量 |
大多数时候我们使用
vecn
,因为float足够满足大多数要求了。
一个向量的分量可以通过
vec.x
这种方式获取,这里
x
是指这个向量的第一个分量。你可以分别使用
.x
、
.y
、
.z
和
.w
来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用
rgba
,或是对纹理坐标使用
stpq
访问相同的分量。
向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
你可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个
vec2
向量中去获取
.z
元素。我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
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
这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。
我们已经在前面的教程看过这个了,
layout (location = 0)
。顶点着色器需要为它的输入提供一个额外的
layout
标识,这样我们才能把它链接到顶点数据。
另一个例外是片段着色器,它需要一个
vec4
颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我们会稍微改动一下之前教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
例如
顶点着色器
顶点着色器的输入是从顶点数据中直接输入
但是他的输出,会被片段着色器接收。
因此,我们要指定一个输出到片段着色器的颜色输出
在main函数中,我们可以对这些变量进行操作,不过要给输出颜色赋值
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片段着色器
这里要指定一个输入变量,用于接收从顶点着色器输入的数据
并且指定一个输出的颜色。
在main函数中,我们可以这些变量进行操作,不过最后要记得给输出的颜色赋值
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
着色器类型封装
我们已经知道,渲染图形需要着色器,但是创建着色器程序的方式其实是一个重复的过程,不同的仅仅只是着色器源代码,而且创建着色器程序的的代码十分冗长,因此为了方便使用,我们将着色器的创建封装在一个头文件里,之后我们就只需要引入头文件,调用接口,传入着色器源代码文本文件的地址就能自动创建,十分方便。没错,我们不再使用C风格字符串去存储着色器源代码了,我们使用文本文件去存储,然后通过C++文件流去读取即可。因此我们只需要输入文件路径即可,是不是十分方便?
首先让我创建着色器类,并封装其函数
成员属性只有一个那就是着色器程序ID
class Shader
{
public:
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const char* vertexPath, const char* fragmentPath);
// 使用/激活程序
void use();
// uniform工具函数
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
};
这里提供了4个外部接口,分别是:激活着色器程序,工具函数用于给
uniform
变量赋值
对于
Uniform
变量的介绍在后文。
接下来我们一点点实现他们,首先是构造函数
在构造函数里我们要完成绝大部分工作
首先从外部读取着色器源代码文本文件的路径,通过文件流打开文件,并将源代码读取到字符串里。
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
}
然后,就是和之前基本相同的着色器与着色器程序创建过程了
链接到着色器程序后,记得删除着色器对象
const char* vShaderCode = vertexCode.c_str();
const char * fShaderCode = fragmentCode.c_str();
// 2. compile shaders
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
}
这样构造函数就完成了。
使用/激活程序
void use()
{
glUseProgram(ID);
}
这个很简单,就是一个函数
然后就是Uniform工具类的实现,其实就是调用了两个函数对数据进行操作,也比较简单
// utility uniform functions
// ------------------------------------------------------------------------
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);
}
这样我们就完成了着色器类的封装
那么该如何使用呢,很简单,首先引入头文件
#include "MyShader/myShader.h"
,名称自己设置哦
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
创建着色器对象即可,并将着色器源代码文本文件路径输入,即可完成之前那么多着色器代码的工作了,这里仅仅只需要一行代码即可。
在渲染指令中激活也很简单
ourShader.use();
设置每个Uniform变量也仅仅只需一行代码,有了封装后着色器类,代码会变得十分简洁
ourShader.setFloat("xOffset",moveValue);
ourShader.setFloat("colorOffset",colorValue);
完整代码
#ifndef SHADER_H
#define SHADER_H
#include "glad/glad.h"
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
class Shader
{
public:
unsigned int ID;
// constructor generates the shader on the fly
// ------------------------------------------------------------------------
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char * fShaderCode = fragmentCode.c_str();
// 2. compile shaders
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// activate the shader
// ------------------------------------------------------------------------
void use()
{
glUseProgram(ID);
}
// utility uniform functions
// ------------------------------------------------------------------------
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);
}
private:
// utility function for checking shader compilation/linking errors.
// ------------------------------------------------------------------------
void checkCompileErrors(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;
}
}
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;
}
}
}
};
#endif
Uniform
这是一个很重要的全局变量,它可以被着色器程序的任意着色器在任意阶段访问,我们可以在一个着色器中添加
uniform
关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:
我们可以在着色器中去定义这个变量,例如
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
并且以下面这种方式在渲染指令中去给他赋值
在C++程序里,我们首先需要找到这个变量
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
通过下面这行代码,第一个参数是着色器程序,第二个参数是这个变量名称的字符串。通过这个方式我们可以找到这个变量属性的索引/位置值,在通过下面这个函数,我们可以通过这个变量的索引/位置值,给这个变量赋值
赋值方式,根据定义的数据类型而定,这里我定义的是vec4因此使用
glUniform4
,赋予四个值。
如果你定义的是
vec1
那么就使用
glUniform1
,赋予一个值即可。
glUniform4f(vertexColorLocation, 0.0f, 0.0f, 0.0f, 1.0f);
那么我要这个变量有什么用呢?学到这里其实我们已经明白,正常情况下,我们是无法直接通过C++程序去编辑着色器程序的。这就导致,我们只能硬编码着色器程序,因此我们只能绘制静态的图形。但是当我有了
Uniform
变量,我们就可以从C++程序里动态的对着色器内的输出颜色或者顶点数据进行更改。
我们可以完成例如:颜色渐变,位置移动等操作
颜色渐变
顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
uniform float colorOffset;
void main()
{
gl_Position = vec4(aPos,1.0);
ourColor = vec3(aColor.x*colorOffset,aColor.y*colorOffset,aColor.z*colorOffset);
}
片段着色器
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0f);
}
在顶点着色器中,我们定义了一个
float
变量,并且将它与输出到片段着色器的颜色值的各个参数想乘。
然后,我们在C++程序的渲染指令这这样去控制这个变量
float timeValue = glfwGetTime();
float colorValue = (sin(timeValue) / 2.0f) + 0.5f;;
int vertexColorLocation = glGetUniformLocation(ourShader.ID, "colorOffset");
glUseProgram(ourShader.ID);
glUniform1f(vertexColorLocation,colorValue);
首先,我们通过
glfwGetTime
函数获得当前的运行的秒数,秒数是随时间不断变化的,这就是动态的本质来源。
然后,我们将
timeValue
通过
sin
函数的相关表达式将这个值控制在0-1之间,因为颜色值的四个值都是0-1.
然后通过
glGetUniformLocation
函数拿到着色器程序中的我们之前定义的这个
float
变量的索引/位置值。
再激活着色器程序,最后把变化的
colorValue
通过
glUniform1f
函数赋给这个
float
变量,这样一来,我们就能看到颜色的不断变化了!
颜色渐变与位置移动结合
其实就是定义了两个
uniform
变量
这里是着色器源代码
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
uniform float xOffset;
uniform float colorOffset;
void main()
{
gl_Position = vec4(aPos.x + xOffset/2, aPos.y, aPos.z, 1.0);
ourColor = vec3(aColor.x*colorOffset,aColor.y*colorOffset,aColor.z*colorOffset);
}
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0f);
}
原理基本一样,直接给C++完整代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cmath>
#include "MyShader/myShader.h"
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// ------------------------------------
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs"); // you can name your shader files however you like
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
// positions // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// render the triangle
ourShader.use();
float timeValue = glfwGetTime();
float moveValue = sin(timeValue);
float colorValue = sin(timeValue)/2+0.5;
// int vertexMoveLocation = glGetUniformLocation(ourShader.ID, "xOffset");
// int vertexColorLocation = glGetUniformLocation(ourShader.ID, "colorOffset");
ourShader.use();
ourShader.setFloat("xOffset",moveValue);
ourShader.setFloat("colorOffset",colorValue);
// glUniform1f(vertexMoveLocation,moveValue);
// glUniform1f(vertexColorLocation,colorValue);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}