OpenGL学习(三) 着色器
参考官方文档: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)
。
推荐阅读
-
【android学习笔记】第三篇——基础控件1
-
关于 OpenGL 固定存储着色器的理解
-
【WebService学习过程记录(三)】XFire开发Web Service---HelloWord 博客分类: javaWeb知识总结 webservicexfire
-
【WebService学习过程记录(三)】XFire开发Web Service---HelloWord 博客分类: javaWeb知识总结 webservicexfire
-
linux用while-until-for三种循环结构分别计算1+2+3...+100的值并输出----shell脚本初学习
-
算法LeetCode自主学习------存在连续三个奇数的数组
-
H2嵌入式数据学习三步曲 博客分类: h2db嵌入式数据库 h2db嵌入式数据库
-
python核心高级学习总结3-------python实现进程的三种方式及其区别
-
YII学习第三,SESSION和COOKIE的使用
-
opencv3学习笔记——第三章图像融合(带权重叠加)