OpenGL(三) 分离并使用着色器

OpenGL(三) 分离并使用着色器

注:本文仅供自己以及OPENGL初学者共同学习进步,并无实际教学意义,不过会对自己学习中遇到的关键点加上个人解释。应该会对初学者有所帮助,如有错误请指正!


继续沿用上章的代码,我们发现着色器代码写在主代码文件中看上去非常冗余,并且难以分析,所以我们可以建立一个专属处理Shader的类,封装处理Shader的操作,这样我们在使用Shader的时候就只需要短短一两行代码就可以实现。

依旧是在VS2017下完成以下操作。

先不管我们的main.cpp(主执行代码),右键我们的项目名称->添加->类,把类名设置成Shader,其他设置一律用默认,点击确定即可,VS2017就会帮我们建立好一个Shader.h 以及 Shader.cpp,来分析一下我们需要的东西,我们的目的是用从txt文档中取得着色器代码,并且能够编译使用它,那么Shader的构建函数就应该传入txt文档的路径,由于我们只使用了顶点着色器与片段着色器,所以只需要传入两个文件的路径即可,用const char * 接收(注意:这里必须是const类型):

1
Shader(const char * vertexPath, const char * fragmentPath);

然后看向Shader.cpp,既然是从文件读取内容,当然就要用到C++的文件流了:

1
2
3
4
5
6
#include "Shader.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>

我们要从txt文档中读取内容要经过以下几个步骤:

shader文件从硬盘被读取到内存里,放在文件缓冲区,然后转换到string缓冲,再由string字符接收内容,然后由于CPU只认得char,所以最后再将string转为char *,内容接收就完成了。


所以我们是不是需要文件缓冲,字符缓冲与string还有char *,看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Shader.h
#pragma once

#include <string>

class Shader
{
public:
Shader(const char * vertexPath, const char * fragmentPath);
~Shader();
private:
std::string vertexString;
std::string fragmentString;
const char * vertexSource;
const char * fragmentSource;

unsigned int ID; // 程序ID
void checkCompileErrors(unsigned int ID, std::string type);
public:
void use(); //激活程序
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//shader.cpp
#include "Shader.h"
#include <iostream>
#include <ostream>
#include <fstream>
#include <sstream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
using namespace std;

Shader::Shader(const char * vertexPath,const char * fragmentPath)
{
ifstream vertexFile;//顶点着色器 信息
ifstream fragmentFile;//片段着色器 信息
stringstream vertexSStream;
stringstream fragmentSStream;

vertexFile.open(vertexPath);//读取内容到文件缓冲
fragmentFile.open(fragmentPath);//同上

vertexFile.exceptions(ifstream::failbit || ifstream::badbit);//检测是否读取成功
fragmentFile.exceptions(ifstream::failbit || ifstream::badbit);//同上

try
{
if (!vertexFile.is_open()|| !fragmentFile.is_open())
{
throw exception("open Shader file error");
}

vertexSStream << vertexFile.rdbuf();
fragmentSStream << fragmentFile.rdbuf(); //file buffer -> string buffer

this->vertexString = vertexSStream.str();
this->fragmentString = fragmentSStream.str(); //string buffer -> string

vertexSource = vertexString.c_str();
fragmentSource = fragmentString.c_str(); // string -> char

//下面这几段就是我们之前在主执行代码里创建着色器程序的过程
unsigned int vertex, fragment;
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vertexSource, NULL);
glCompileShader(vertex);

fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fragmentSource, NULL);
glCompileShader(fragment);

this->ID = glCreateProgram();//ID就是我们使用的着色器程序名字
glAttachShader(this->ID, vertex);
glAttachShader(this->ID, fragment);
glLinkProgram(this->ID);

glDeleteShader(vertex);
glDeleteShader(fragment);

}
catch (const std::exception& ex)
{
printf(ex.what());
}
}

void Shader::use()
{
glUseProgram(this->ID); //使用着色器
}


Shader::~Shader()
{
}

然后接下来我们就可以把主代码那些关于着色器的代码全部去掉了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#define GLEW_STATIC
#include<GL/glew.h>
#include<GLFW/glfw3.h>
#include<iostream>
#include"Shader.h"
using namespace std;


float vertices[] = { //三角形是逆时针构造 所以逆时针为正面,顺时针为反面,原因参考 法向量
//pos //rgb
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,//0
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,//1
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f //2

};

unsigned int indices[] = {
0,1,2
};

void processInput(GLFWwindow * window);

int main() {

glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//major version
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//minor version
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

//OpenGL GLFW Window
GLFWwindow * window = glfwCreateWindow(800, 600, "demo", NULL, NULL);
if (window == NULL) {
cout << "Fail to create" << endl;
glfwTerminate();
return -1;
}

glfwMakeContextCurrent(window);

//Initg GLFW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
cout << "Init GLEW failed" << endl;
glfwTerminate();
return -1;
}

glViewport(0, 0, 800, 600);

Shader * testShader = new Shader("vertexSource.txt", "fragmentSource.txt");

unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO); //绑定

unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

//以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

//接下来就可以正式使用两个shader了
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);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

while (!glfwWindowShouldClose(window))
{
processInput(window);//窗口模板载入

glClearColor(0.2f, 0.3, 0.3, 1.0f);//背景色
glClear(GL_COLOR_BUFFER_BIT);//清除缓冲

testShader->use();

glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//glDrawArrays(GL_TRIANGLES, 0,6);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();

}
glfwTerminate();
//system("pause");
return 0;
}

void processInput(GLFWwindow * window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}

既然着色器代码已经删除了,那么我们就需要两个txt文档来写着色器代码.
可以直接到VS2017右键项目->添加->新建项->实用工具->文档,创建两个文档,一个写vertexShader,一个写fragmentShader,然后你会发现你的资源文件多了两个txt文档,把之前的着色器代码复制粘贴进去,去掉双引号和"\n"(不去干净就会出错),就OK了。


然后我们到主执行代码程序里面去,创建Shader对象,初始化路径,使用use方法就行啦。
还有一点要注意,创建Shader对象时,必须是在初始化glad或glew之后才能创建。

附上ShaderSource代码:


vertexSource.txt

1
2
3
4
5
6
7
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 vertexColor;
void main(){
vertexColor = aColor;
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}

fragmentSource.txt

1
2
3
4
5
#version 330 core
out vec4 FragColor;
in vec3 vertexColor;
void main() {
FragColor = vec4(vertexColor,1.0);}

最后再补充一下代码,可以看到我们的顶点数组多了些内容,也就是每个顶点的RGB值,还记得我们如何告诉OpenGL处理这些数据吗

1
2
3
4
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);

我们将RGB值放在了我们绑定的VAO的1号位置,然后由于现在每一个顶点数据变成了6个,所以第四个参数改成了6*sizeof(float);

可以看到我们的vertexShader里面也多了个aColor属性,然后将它传入了fragmentShader.
输出效果如下:

我们要记得一个地方,就是如果颜色不对,一般就是fragmentShader出问题了,如果连图形都没有,一般就是vertexShader出问题了,然后检查两个着色器的代码。

那我们希望知道着色器代码编译出错的点该怎么办,我们只需要使用OpenGL的函数来获取Shader的编译信息就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Shader::checkCompileErrors(unsigned int ID, std::string type)
{
int success;
char infoLog[512];

if (type != "PROGRAM") //着色器编译错误
{
glGetShaderiv(ID, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(ID,512,NULL,infoLog);
std::cout << "shader compile error: " << infoLog << std::endl;
}
}
else //着色器程序链接错误
{
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "program compile error: " << infoLog << std::endl;
}
}
}

只要我们着色器编译出错,它就会在窗口打印出错误信息。这样我们就能知道错误的详细情况了。