源代码在下面。文档查询 > docs.gl
结果展示:使用自己的shader和打印错误描述
该篇主要在上一部分代码的基础上添加了自己写的shader,即着色器。最常用的两个着色器 vertex shader 和 fragment shader,即顶点着色器和片段着色器。
大概了解一下:
- shader只是一段程序
- 假如你要画一个三角形,有三个顶点,那么vertex shader就被调用了三次
- 你要对三角形填色,也就是光栅化,那么fragment shader就被调用了很多次,成千上万次,一个像素点调用一次
- openGL是一个状态机,就跟有很多开关一样,不是说你一改变代码就立马执行并影响到后面的代码了
添加的shader主要通过函数 CreateShader 来实现,本次主要通过查文档来提高一下分析能力。
/*方便起见,写成一个函数*/
static unsigned int CompileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str(); /*或者写 &source[0]*/
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE) {
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
// char message[length]; /*这里会发现因为长度不定,无法栈分配,但你仍要这么做*/
char* message = (char*)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Failed to compile " <<
(type == GL_VERTEX_SHADER ? "vertex":"fragment" )<< "shader!请定位到此行" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}
/*使用static是因为不想它泄露到其他翻译单元?
使用string不是最好的选择,但是相对安全, int类型-该着色器唯一标识符,一个ID*/
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {
/*使用unsigned是因为它接受的参数就是这样,
或者可以使用 GLuint,但是作者不喜欢这样,因为它要使用多个图像api*/
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
开始分析部分代码:
1.先看 CreateShader 和 CompileShader,都是 static
这里对static关键字的使用做以下解释:
-
C++函数默认是非static的,也就是可以从外部函数/文件调用。
-
如果定义为static,则这个函数只在所在的编译单元(源文件内)可见并用,外部无法访问。
-
在OpenGL中,通常一个文件会包含多个shader程序,我们需要一个函数返回一个特定程序的ID。
-
如果不加static,这个函数就会被所有程序公用,可能会产生冲突。
-
加static后,这个函数就只对本文件内的对象可见,不会对其他文件产生影响。
-
换句话说,不加static可能会导致函数定义在其他翻译单元(文件)中重复,产生命名冲突。
-
加static可以避免该函数暴露给其他文件,仅在本文件内起到程序创建的目的,不与其他程序产生影响。
所以总的来说,static用于将函数封装在本文件内,避免跨文件调用产生潜在 bugs 的意思。
您提出了一个好问题。我刚才解释中说:
“在OpenGL中,通常一个文件会包含多个shader程序,我们需要一个函数返回一个特定程序的ID。”
这里需要进一步解释下为什么:
-
在OpenGL中,一个着色器程序可以包含多个shader(顶点着色器+片元着色器等)。
-
如果一个文件定义了多个这样的程序,就需要区分返回每个程序的唯一ID。
-
如果函数定义为非static,当多个程序同时调用此函数时:
-
由于函数名重复定义,编译会报错。
-
或者后定义的程序会覆盖前面程序保存的ID,导致ID错乱。
-
-
使用static后,每个程序调用的函数实际上是不同函数的副本。
-
这样每个程序得到的ID就是对应着色器程序本身的唯一标识符。
所以非static定义在这里可能会导致:
- 编译错误
- ID管理错误
- 程序无法正确识别各自的着色器
而static可以避免此问题,保证函数本地性质的定义。
2.根据文档分析函数,如 glShaderSource
查看文档:https://docs.gl/gl4/glShaderSource
四个参数,分别是
函数描述:
glShaderSource sets the source code in shader to the source code in the array of strings specified by string. Any source code previously stored in the shader object is completely replaced. The number of strings in the array is specified by count. If length is NULL, each string is assumed to be null terminated. If length is a value other than NULL, it points to an array containing a string length for each of the corresponding elements of string. Each element in the length array may contain the length of the corresponding string (the null character is not counted as part of the string length) or a value less than 0 to indicate that the string is null terminated. The source code strings are not scanned or parsed at this time; they are simply copied into the specified shader object.
这个看完就比较清楚了。
第一个参数,GLuint类型,确定是哪个shader,
您提出了一个重要的问题。
OpenGL中的shader id(如glCreateShader返回的id),如果不加以管理,确实可能不是唯一的。
例如:
-
程序运行期间创建多个shader对象,IDs可能重叠冲突。
-
不同程序间shader ID也可能相同。
所以为了保证shader id的唯一性,需要做一些附加处理:
-
使用静态计数器维护shader id分配,每个id加1分配。
-
将id与shader对象绑定,使用对象指针作为唯一标识。
-
提取id生成代码到单独函数,控制唯一性。
-
为程序设计名称空间,每个程序独立定义id。
-
使用面向对象的shader类,id作为对象属性管理。
-
等等其它方法。
即使OpenGL ids本身不唯一,我们也可以通过程序设计的方式来管理ids,保证在程序运行过程中是唯一的。
这就需要考虑额外的id管理机制,而不是简单依赖OpenGL原生id。
第二个参数,算个数,shader的个数,shader是以string的形式存在的,vertexShader的内容就是vertexShader的源代码。
第三个参数传进来的指针就是
std::string vertexShader =
"#version 330 core\n"
"\n"
"layout(location = 0) in vec4 position;"
"\n"
"void main()"
"{\n"
" gl_Position = position;\n"
"}\n";
std::string fragmentShader =
"#version 330 core\n"
"\n"
"layout(location = 0) out vec4 color;"
"\n"
"void main()"
"{\n"
" color = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
第四个参数,给一个数组计算每个shader的长度
3.错误打印 直接看代码即可
glGetShaderInfoLog(id, length, &length, message);
openGL提供了接口。
源代码:
#inclu
de <iostream>
#include <string>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
/*方便起见,写成一个函数*/
static unsigned int CompileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str(); /*或者写 &source[0]*/
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE) {
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
// char message[length]; /*这里会发现因为长度不定,无法栈分配,但你仍要这么做*/
char* message = (char*)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Failed to compile " <<
(type == GL_VERTEX_SHADER ? "vertex":"fragment" )<< "shader!请定位到此行" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}
/*使用static是因为不想它泄露到其他翻译单元?
使用string不是最好的选择,但是相对安全, int类型-该着色器唯一标识符,一个ID*/
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {
/*使用unsigned是因为它接受的参数就是这样,
或者可以使用 GLuint,但是作者不喜欢这样,因为它要使用多个图像api*/
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
int main(void)
{
GLFWwindow* window;
/* Initialize the library */
if (!glfwInit())
return -1;
//if (glewInit() != GLEW_OK)/*glew文档,这里会报错,因为需要上下文,而上下文在后面*/
// std::cout << "ERROR!-1" << std::endl;
/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK)/*这里就不会报错了*/
std::cout << "ERROR!-2" << std::endl;
std::cout << glGetString(GL_VERSION) << std::endl;
float positions[6] = {
-0.5f, 0.5f,
0.0f, 0.0f,
0.5f, 0.5f
};
/*
这段代码是创建和初始化顶点缓冲对象(Vertex Buffer Object,简称VBO)。
VBO是OpenGL中一个很重要的概念,用于高效渲染顶点数据。
它这段代码的作用是:
glGenBuffers生成一个新的VBO,ID保存到buffer变量中。
glBindBuffer将这个VBO绑定到GL_ARRAY_BUFFER目标上。
glBufferData向被绑定的这个VBO中填充实际的顶点数据。
通过这三步:
我们得到了一个可以存储顶点数据的VBO对象
后续绘制调用只需要指定这个VBO就可以加载顶点数据
教程强调VBO是因为:
相对直接送入顶点更高效
绘制调用不再需要每帧重复发送相同顶点
提高渲染性能
所以总结下VBO可以高效绘制复杂顶点数据至显卡,是OpenGL重要概念
glGenBuffers(1, &buffer);
glGenBuffers作用是生成VBO对象的ID编号。
第一个参数1表示要生成的VBO数量,这里只生成1个。
第二个参数&buffer是用于返回生成的VBO ID编号。
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBindBuffer用于将VBO对象绑定到指定的目标上。
第一个参数GL_ARRAY_BUFFER表示要绑定的目标是顶点属性数组缓冲。
GL_ARRAY_BUFFER指定将要保存顶点属性数据如位置、颜色等。
第二个参数buffer就是前面glGenBuffers生成的VBO ID。
所以总结下:
glGenBuffers生成1个VBO对象并获取ID编号
glBindBuffer将这个VBO绑定到属性缓冲目标上,作为后续顶点数据的存储对象。
glBufferData的作用是向之前绑定的VBO对象中填充实际的顶点数据。
参数说明:
GL_ARRAY_BUFFER:指定操作目标为顶点属性缓冲(与glBindBuffer一致)
6 * sizeof(float):数据大小,这里 positions 数组有6个float数
positions:数组指针,提供实际的数据源
GL_STATIC_DRAW:数据使用模式
GL_STATIC_DRAW:数据不会或很少改变
GL_DYNAMIC_DRAW:数据可能会被修改
GL_STREAM_DRAW:数据每次绘制都会改变
它的功能是:
分配指定大小内存给当前绑定的VBO对象
将positions数组内容拷贝到VBO对象内存中
以GL_STATIC_DRAW模式,显卡知道如何优化分配内存
这样一来,positions数组中的顶点数据就上传到GPU中VBO对象里了。
OpenGL随后通过该VBO对象来读取顶点数据进行绘制。
*/
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
/*index-只有一个属性,填0
size-两个数表示一个点,填2
stripe-顶点之间的字节数
pointer-偏移量
好的,我们来用一个例子来解释glVertexAttribPointer的参数含义:
假设我们有一个VBO,里面存放3个三维顶点数据,每个顶点由(x,y,z)组成,每个元素类型为float。
那么数据在VBO中排列如下:
VBO地址 | 数据
0 | x1
4 | y1\
8 | z1
12 | x2
16 | y2
20 | z2
24 | x3
28 | y3
32 | z3
现在我们要告诉OpenGL如何解析这些数据:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 12, 0);
- 0:属性为位置数据
- 3:每个位置由3个float组成,(x,y,z)
- GL_FLOAT:数据类型是float
- 12:当前属性到下一个属性的间隔,即一个顶点需要12个字节
- 0:这个属性起始位置就是VBO的开头
这样OpenGL就知道:
- 从VBO开始地址读取3个float作为第一个顶点的位置
- 下一个顶点偏移12字节再读取3个float
最后一个参数0就是告诉OpenGL属性的起始读取偏移是多少。
好的,用一个例子来具体说明一下这种情况:
假设我们有一个VBO来存储顶点数据,每个顶点包含位置和颜色两个属性。
数据在VBO内部的排列方式为:
位置x | 位置y | 位置z | 颜色r | 颜色g | 颜色b
那么对于第一个顶点来说,它在VBO内的布局是:
VBO地址 | 数据
0 | 位置x\
4 | 位置y
8 | 位置z
12 | 颜色r
16 | 颜色g
20 | 颜色b
此时,我们设置位置属性和颜色属性的指针:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, 12);
可以看到:
- 位置属性从0字节处开始读取
- 颜色属性从12字节处开始读取(让出位置数据占用的空间)
这就是为什么位置属性的偏移不能写0,需要指定非0偏移量让出给颜色属性存储空间。
这样才能正确解析这两个分开但共处一个VBO的数据。*/
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);/* (const void)*8/
/*这里开始使用着色器*/
std::string vertexShader =
"#version 330 core\n"
"\n"
"layout(location = 0) in vec4 position;"
"\n"
"void main()"
"{\n"
" gl_Position = position;\n"
"}\n";
std::string fragmentShader =
"#version 330 core\n"
"\n"
"layout(location = 0) out vec4 color;"
"\n"
"void main()"
"{\n"
" color = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
unsigned int shader = CreateShader(vertexShader, fragmentShader);
glUseProgram(shader);
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
// glDrawElements(GL_TRIANGLES, )
/* glBegin(GL_TRIANGLES);
glVertex2f(-0.5f, 0.5f);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.5f, 0.5f);
glEnd();*/
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
glDeleteProgram(shader);
glfwTerminate();
return 0;
}