接上篇,我们进一步得解释和分离我们的代码
首先我们回顾一下,绘制三角的步骤
1、写出组成三角的顶点数据,三组顶点每组均由3个float分别代表x,y,z坐标
2、创建顶点缓冲对象
3、创建顶点数组对象
3、绑定缓冲对象与数组对象
4、告知opengl如何解释顶点,解释的是第几组顶点,并激活这组顶点
5、编写实际的顶点着色器与片段着色器,着色器处理的是一个顶点/一个像素(着色器代码存在其他的文件中)
6.、创建着色器对象
7、创建文件流读取着色器代码,并把着色器代码附加到着色器对象上
8、附加着色器对象
9、创建着色器程序对象
10、链接着色器对象并删除着色器对象
11、渲染循环中绑定顶点数组对象,启用着色器,调用绘制函数
那么这里有哪些代码可以分离呢?
首先,所有的创建步骤其实都可以分离。啊是的首先你想到了,把VAO和VBO的创建分离,很遗憾,这是一部分你无法分出的代码,当然你可以尝试,但是VAO,VBO要呆在main函数中创建,总之我们忽略他们,把他们的代码丑陋得留着。
那么我们还能做啥?是的,我们可以分离shader的创建。我们可以创建一个自己的着色器类,来减少每次创建着色器的重复代码。
好的,怎么做呢?提示一下,我们先决定要提供什么。
1、代码文件地址
2、没了
没错,除了代码地址,咱们啥都不需要,因为着色器总共只有三个已知类型,目前我们只用了两个,那么着色器数量的问题也可以通过重载或默认参数来解决。
那么接口呢?我们目前来看,只需要一个use(),即激活着色器。好的那么公有部分就确定了,一个构造函数,一个use接口。
接着是私有函数,首先是文件读取readFile,我们仅需要文件地址。
然后是着色器对象的创建,我们需要着色器的类型、源码来创建一个着色器,别的都一样,也就是需要一个createShader。
然后是着色器程序对象,我们需要链接函数linkShader,还需要一个可以保持的着色器程序对象,也就是一个数据成员。
啊,好的,回过头去看一眼,我们的着色器对象是一个一个创建,故创建着色器对象的函数调用readFile来获取自己的代码也很合理喽。而着色器程序对象是要创建了全部的着色器对象之后才能运作,所以它是独立的;这儿出现一个问题了“着色器程序对象到底有几个着色器对象需要链接?”,出于这个问题,我们会需要std::initializer_list<GLuint>来作为参数,用循环来链接/删除着色器对象。
好的,看一下基于这几点写出的代码。
mShader.h或者shader/h反正随你喜欢
#pragma once
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
class shader {
public:
shader(const std::string &vSource, const std::string &fSource,
const std::string &gSource = std::string())//几何着色器赋予默认值,因为几何着色器为可选着色器
{
GLuint vertex, fragment;
createShader(GL_VERTEX_SHADER, vertex, vSource);
createShader(GL_FRAGMENT_SHADER, fragment, fSource);
if (!gSource.empty()) {
GLuint geometry;
createShader(GL_GEOMETRY_SHADER, geometry, gSource);
linkShader({ &vertex,&fragment,&geometry });
}//检测几何着色器需不需要创建
else linkShader({ &vertex,&fragment });
}
void use() { glUseProgram(program); }
private:
GLuint program;
std::string readFile(const std::string &source) const;//接受地址读取文件代码
//需要文件地址来调用readFile
void createShader(GLenum type, GLuint &mshader,const std::string &source) const;
void linkShader(std::initializer_list<GLuint*> shaders);//读入数目不一定的着色器
};
std::string shader::readFile(const std::string &source) const
{
std::ifstream file;
//保证ifstream对象可以抛出异常:
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
file.open(source);
std::stringstream code;
code << file.rdbuf();
return code.str();
//返回字符串,注意不要用引用,因为不能返回局部对象的引用(code内的string随着code析构释放)
}
catch (std::iostream::failure &e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ\n" << e.what() << std::endl;
}
catch (std::exception &e)
{
std::cout << "ERROR::SHADER::ERROR_APPEARED_WHEN_IO\n" << e.what() << std::endl;
}
}
void shader::createShader(GLenum type, GLuint &mshader,const std::string &source) const
{
std::string code_string = readFile(source);
const char *code = code_string.c_str();//读取源码
mshader = glCreateShader(type);
glShaderSource(mshader, 1, &code, nullptr);
glCompileShader(mshader);
int success;
char infoLog[1024];
glGetShaderiv(mshader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(mshader, 1024, nullptr, infoLog);
std::cout << " SHADER FAILED TO COMPILE\n" << infoLog << std::endl;
}
}
void shader::linkShader(std::initializer_list<GLuint*> shaders)
{
{
program = glCreateProgram();
int num = sizeof(shaders);
for (GLuint *s : shaders)
{
glAttachShader(program, *s);
}
glLinkProgram(program);//先链接再删除
for (GLuint *s : shaders)
{
glDeleteShader(*s);
}
int success;
char infoLog[1024];
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(program, 1024, nullptr, infoLog);
std::cout << "ERROR::FAILED TO LINK PROGRAM!\n" << infoLog << std::endl;
}
}
}
并没有什么新的东西,所以接下来提一下新的东西。
新的顶点相关对象:索引缓冲对象 EBO
啊哈,我们的xxO家族中又多了一员,EBO(element buffer object)。还记得我们的顶点数据吧,3个一组,一共三组,绘制一个三角,如果是两个三角呢?我们需要glDrawArrays(GL_TRAINGLES, 0, 6);对不对,因为两个三角有6个顶点。那如果两个三角只有四个顶点呢?我们用顶点1/2/3组成一个三角,再用2/3/4组成第二个,非常合理,符合我们绘图的方法。然而VAO只会要求你把2/3两个顶点重复写入到顶点数组里,那么EBO就是让我们不需要增加顶点数据的东西,它标记你使用的顶点,也就是顶点的索引。
当然,它并不能让你只写四个顶点就完成两个三角的绘制,但是它可以让你使用4个顶点加上两组索引完成两个三角的绘制。
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
就像这样,第二组indices就是顶点的索引数据,目前看起来并没有什么明显的节省,但是如果你要画几百(万)个三角呢?可以想象我们省去的顶点数据。那么接下来我们创建一个EBO。
EBO使用
好的,我们使出我们的三板斧,glGenBuffers、glBufferData、glBindBuffer!没错,完全和VBO一样的函数调用,不同的只是参数(还有顺序)。
GLuint VBO, VAO, EBO;
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &EBO);//创建,第一斧
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//第二斧,绑定,注意一定在VAO,VBO均绑定后
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//第三斧,传递索引数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(0));
glEnableVertexAttribArray(0);
然后修改我们的绘制函数
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
//告知opengl绘制图元的方式(绘制背面及 正面,启用线框模式)
mShader.use();
glBindVertexArray(VAO);
//glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//图形,顶点数(索引数),索引类型,偏移
启用线框模式来让绘制轨迹更加清楚(请忽略这个函数的细节),这里注意这个6,是你要绘制的顶点个数,或者说你索引的数量、索引数组的大小。
新东西之二,拓展你的着色器
1、首先,让顶点着色器向片段着色器发送数据
回想之前的着色器代码
#version 330 core
//使用3.3版本核心模式
layout (location = 0) in vec3 aPos;
//接收位置在0 这个顶点属性是接收进来的(in) vec3类型的变量 之后使用名字为aPos
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
//处理这个顶点的数据 = vec4(x坐标,y坐标,z坐标, 透视变形值);
}
//-------------------------------------------------
#version 330 core
//同顶点着色器
out vec4 FragColor;
//声明一个要输出(out)的变量 类型为vec4 名字为FragColor
void main()
{
FragColor = vec4(1.0f, 0.8f, 0.2f, 1.0f);
//输出的颜色 = vec4类型 red1.0f green0.8f blue0.2f alpha1.0f(最高1.0即不透明)混合的颜色
}
可以看到我们使用in与out关键字来接收/输出数据,那么顶点着色器是在片段着色器之前的着色器,很简单得能想到,我们在顶点着色器中使用out关键字,然后就可以在片段着色器中使用in读取到这些数据。
代码:
#version 330 core
layout(location = 0)in vec3 aPos;
out vec4 beginColor;//发出这个vec4
void main()
{
//gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);
gl_Position = vec4(aPos, 1.0f); //注意这里,也就是说vec4( vec3,float )的构造是存在的
beginColor = vec4(0.2f, 0.0f, 0.3f, 1.0f);
}
//-----------------------------以下为片段着色器
#version 330 core
out vec4 FragColor;
in vec4 beginColor;//接收同名的vec4
void main()
{
FragColor = beginColor;//改变输出的颜色为从顶点着色器得到的变量
}
很简单的使用,顶点着色器中 out 关键字创建一个vec4变量,片段着色器中使用 in 关键字创建同名的变量即可读取这个其他着色器发来的vec4,然后再把读来的vec4赋给自己要输出的FragColor,结果我们的三角就变成了紫色。这实现的是gpu到gpu的通信,因为着色器是在gpu上运行的,当然,我们也可以让cpu以顶点属性之外的方式发送数据给gpu。
2、然后,让cpu给gpu发送数据
由cpu发送到gpu也就意味着从你的cpp发送到你的着色器,那么我们需要借助的是uniform变量,还有一个,可能你猜得到,也就是我们的着色器程序对象。
首先我们要修改我们的顶点数据,我们需要一组新的属性来传送每个顶点颜色数据
//顶点数据
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.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
啊,你可能会奇怪,顶点怎么有颜色呢?一个点如何有颜色?是的,对一个点上色你也很难看出来,然而opengl采用的是片段插值来决定片段的颜色。简单得说,这个顶点上的片段的颜色是你传递的颜色,如果一个片段在顶点1和顶点2直接,它与1的距离为总距离30%,与2的距离就会是(1-30%) = 70%的总距离,那么它的颜色将会是70%顶点1的颜色与30%的顶点2的颜色的混合值。也就是这个片段离哪个顶点近,就受到哪个顶点的颜色影响较大。
那么我们修改了定的数组,自然也要修改顶点的解析方式,这里我们的顶点数据变成了每隔6个float才能读取到下一个数据,而且对于颜色数据来说,他得先忽略开始的三个顶点数据,也就是颜色数据的偏移量为3 * sizeof(float)。
代码:
//顶点缓冲对象
GLuint VBO, VAO, EBO;
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &EBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, 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);//给出第二组属性,修改步长和偏移值
实际上没有什么新的东西,只是加了两句代码,修改了原来的步长。
那么此时编译你会看到一个彩色的三角,算是对之前的回顾把,接下来我们让这个彩色的三角变色。
首先我们要提出新的东西
uniform变量
uniform变量是定义在着色器中的变量,但它的实际值由你在cpp中发送,我们需要三角变色,所以我们会需要一个时刻变换的数字,比如时间,用float表示的时间。
那么我们先读出我们的时间,并且把它限制在(0.0f,1.0f)的范围
float time = sin(glfwGetTime() / 2.0f) + 0.5f;//把时间限定到(0f, 1.0f)的范围
这里使用glfw提供的函数和数学函数,你当然也可以用c++标准函数,随便啦。
然后把这个数值发送到着色器,我们会需要着色器对象。哦豁,问题出现了,我们的着色器对象是私有的,所以我们需要一个接口,一个不能更改着色器对象的接口。
const GLuint& program() const{ return _program; }
private:
GLuint _program;
如上。
然后我们需要获取uniform变量的位置,再发送到着色器。
float time = sin(glfwGetTime() / 2.0f) + 0.5f;//把时间限定到(0f, 1.0f)的范围
int aColorLocation = glGetUniformLocation(mShader.program(), "time");
//寻找uniform变量位置(程序对象,变量名)
if (aColorLocation == -1)//找不到变量的时候返回-1
{
cout << "ERROR::CAN'T FIND UNIFORM" << endl;
}
mShader.use();//激活着色器后才能传递
glUniform1f(aColorLocation, time);//传递变量
接着在着色器接收即可
#version 330 core
out vec4 FragColor;
in vec4 beginColor;//接收同名的vec4
uniform float time;//接受cpu发来的实时变化时间
void main()
{
FragColor = beginColor * time;//改变输出的颜色为从顶点着色器得到的变量 * 时间变化
}
注意这里我们使用的是glUniform1f来传递一个float变量,1f代表一个float,还有其他不同格式的uniform传递函数。
后缀 | 含义 |
---|---|
f | 函数需要一个float作为它的值 |
i | 函数需要一个int作为它的值 |
ui | 函数需要一个unsigned int作为它的值 |
3f | 函数需要3个float作为它的值 |
fv | 函数需要一个float向量/数组作为它的值 |
这个时候我们又可以进一步拓展我们的着色器类,因为unifrom变量总是和着色器绑定在一起的,所以我们把所有的传递uniform变量的函数添加到着色器类中去。
完整代码如下:
shader.h
#pragma once
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
class shader {
public:
shader(const std::string &vSource, const std::string &fSource,
const std::string &gSource = std::string())//几何着色器赋予默认值,因为几何着色器为可选着色器
{
GLuint vertex, fragment;
createShader(GL_VERTEX_SHADER, vertex, vSource);
createShader(GL_FRAGMENT_SHADER, fragment, fSource);
if (!gSource.empty()) {
GLuint geometry;
createShader(GL_GEOMETRY_SHADER, geometry, gSource);
linkShader({ &vertex,&fragment,&geometry });
}//检测几何着色器需不需要创建
else linkShader({ &vertex,&fragment });
}
void use() { glUseProgram(_program); }
const GLuint& program() const{ return _program; }
//uniform 工具函数
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(_program, name.c_str()), (int)value);
}
// ------------------------------------------------------------------------
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(_program, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(_program, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setVec2(const std::string &name, const glm::vec2 &value) const
{
glUniform2fv(glGetUniformLocation(_program, name.c_str()), 1, &value[0]);
}
void setVec2(const std::string &name, float x, float y) const
{
glUniform2f(glGetUniformLocation(_program, name.c_str()), x, y);
}
// ------------------------------------------------------------------------
void setVec3(const std::string &name, const glm::vec3 &value) const
{
glUniform3fv(glGetUniformLocation(_program, name.c_str()), 1, &value[0]);
}
void setVec3(const std::string &name, float x, float y, float z) const
{
glUniform3f(glGetUniformLocation(_program, name.c_str()), x, y, z);
}
// ------------------------------------------------------------------------
void setVec4(const std::string &name, const glm::vec4 &value) const
{
glUniform4fv(glGetUniformLocation(_program, name.c_str()), 1, &value[0]);
}
void setVec4(const std::string &name, float x, float y, float z, float w)
{
glUniform4f(glGetUniformLocation(_program, name.c_str()), x, y, z, w);
}
// ------------------------------------------------------------------------
void setMat2(const std::string &name, const glm::mat2 &mat) const
{
glUniformMatrix2fv(glGetUniformLocation(_program, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat3(const std::string &name, const glm::mat3 &mat) const
{
glUniformMatrix3fv(glGetUniformLocation(_program, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(_program, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
private:
GLuint _program;
std::string readFile(const std::string &source) const;//接受地址读取文件代码
//需要文件地址来调用readFile
void createShader(GLenum type, GLuint &mshader,const std::string &source) const;
void linkShader(std::initializer_list<GLuint*> shaders);//读入数目不一定的着色器
};
std::string shader::readFile(const std::string &source) const
{
std::ifstream file;
//保证ifstream对象可以抛出异常:
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
file.open(source);
std::stringstream code;
code << file.rdbuf();
return code.str();
//返回字符串,注意不要用引用,因为不能返回局部对象的引用(code内的string随着code析构释放)
}
catch (std::iostream::failure &e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ\n" << e.what() << std::endl;
}
catch (std::exception &e)
{
std::cout << "ERROR::SHADER::ERROR_APPEARED_WHEN_IO\n" << e.what() << std::endl;
}
}
void shader::createShader(GLenum type, GLuint &mshader,const std::string &source) const
{
std::string code_string = readFile(source);
const char *code = code_string.c_str();//读取源码
mshader = glCreateShader(type);
glShaderSource(mshader, 1, &code, nullptr);
glCompileShader(mshader);
int success;
char infoLog[1024];
glGetShaderiv(mshader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(mshader, 1024, nullptr, infoLog);
std::cout << " SHADER FAILED TO COMPILE\n" << infoLog << std::endl;
}
}
void shader::linkShader(std::initializer_list<GLuint*> shaders)
{
{
_program = glCreateProgram();
int num = sizeof(shaders);
for (GLuint *s : shaders)
{
glAttachShader(_program, *s);
}
glLinkProgram(_program);//先链接再删除
for (GLuint *s : shaders)
{
glDeleteShader(*s);
}
int success;
char infoLog[1024];
glGetProgramiv(_program, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(_program, 1024, nullptr, infoLog);
std::cout << "ERROR::FAILED TO LINK PROGRAM!\n" << infoLog << std::endl;
}
}
}
main.cpp
#include"main.h"
int main()
{
using namespace initialization;
GLFWwindow *window = init(SCR_WIDTH,SCR_HEIGHT);
//顶点数据
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.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
//顶点缓冲对象
GLuint VBO, VAO, EBO;
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &EBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, 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);//给出第二组属性,修改步长和偏移值
//着色器对象,特别注意不要把顶点着色器与片段着色器文件读反,或者附加反,或者用了两个顶点/片段
shader mShader("vertexShader.vs", "fragmentShader.fs");
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);//清除指定BUFFER
// input
// -----
processInput(window);
float time = sin(glfwGetTime() / 2.0f) + 0.5f;//把时间限定到(0f, 1.0f)的范围
int aColorLocation = glGetUniformLocation(mShader.program(), "time");
if (aColorLocation == -1)
{
cout << "ERROR::CAN'T FIND UNIFORM" << endl;
}
mShader.use();
glUniform1f(aColorLocation, time);
glBindVertexArray(VAO);
//glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);//交换缓冲
glfwPollEvents();//处理事件响应
}
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
main.h
没有变化,除了增加了#include"shader.h"