学习例子:https://github.com/JackieLong/OpenGL/tree/main/project_shader_test
OpenGL 2.1开始,要求必须设置顶点着色器和片段着色器。
这里就展示一个简单的包含shader的OpenGL程序做示范。
一、顶点着色器:VertexShader
// ************************************************
// ********** VertexShader.vert,顶点着色器扩展名一般为vert
// ************************************************
#version 330 core // 开头必须声明版本,和OpenGL版本一样
layout (location = 0) in vec2 aPos; // 顶点数据中的顶点坐标,
// 简单起见,例子中我们直接传入的NDC坐标,无需矩阵变换
uniform vec3 appColor; // 从应用程序中动态设置一个颜色值
out vec3 outColor; // 向片段着色器输出一个颜色
void main()
{
outColor = appColor;
// gl_Position解释见https://www.yuque.com/tvvhealth/cs/qgs2z1#34NPT
// GLSL内置变量,是顶点进行投影矩阵变换之后输出的裁剪坐标,供下一个阶段进行高效裁剪(Clipping)
// 必须赋值,否则报错。
// 本来这里需要进行模型、视图、投影矩阵变换,简单起见,我们直接使用NDC坐标,也就是变换后的坐标。
gl_Position = vec4(aPos, 0.0, 1.0);
};
二、片段着色器:FragmentShader
// ************************************************
// ********** FragmentShader.frag,片段着色器扩展名一般为frag
// ************************************************
#version 330 core
in vec4 outColor; // 从顶点着色器传入
out vec4 FragColor; // 需要输出一个vec4类型数值,表示生成的片段的颜色值。
void main()
{
FragColor = outColor;
};
三、编译链接GLProgram
const char* vShaderSrc = "......";
const char* fShaderSrc = "......";
GLuint vShaderID = -1; // handle of vertex shader object
GLuint fShaderID = -1; // handle of fragment shader object
GLuint programID = -1; // handle of gl program object
// **********************************************************************
// 一、创建并编译一个Vertex/Fragment Shader Object
// **********************************************************************
vShaderID = glCreateShader(GL_VERTEX_SHADER); // 创建一个shader对象
glShaderSource(vShaderID, 1, &vShaderSrc, NULL); // 导入源码
glCompileShader(vShader); // 编译shader
GLint success; // 编译成功与否
const GLsizei lenLog = 512; // 日志长度
GLchar infoLog[lenLog]; // 编译输出日志
glGetShaderiv( shader, GL_COMPILE_STATUS, &success ); // 获取编译状态数据
if( !success ) { // 编译没有成功,输出日志
glGetShaderInfoLog( shader, lenLog, NULL, infoLog ); // 保存日志
cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << endl;
}
// **********************************************************************
// 二、创建GLProgram,链接vertex/fragment shader
// **********************************************************************
programID = glCreateProgram(); // 创建一个gl program object
glAttachShader(GL_VERTEX_SHADER, vShaderID); // 至少要绑定顶点/片段着色器
glAttachShader(GL_FRAGMENT_SHADER, vShaderID);
glLinkProgram(programID); // 链接,会做一些优化工作,去掉没被使用的uniform/attribute
GLint success; // 链接是否成功
const GLsizei lenLog = 512; // 链接日志长度
GLchar infoLog[lenLog]; // 链接输出日志
glGetProgramiv( id, GL_LINK_STATUS, &success ); // check for linking errors
if( !success ) { // link失败,输出日志
glGetProgramInfoLog( id, lenLog, NULL, infoLog ); // 获取日志
cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << endl;
}
// **********************************************************************
// 三、link finished,可以删除shader object了
// **********************************************************************
// 在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了,只需要programID即可了。
glDeleteShader( vShaderID );
glDeleteShader( fShaderID );
四、准备顶点数据
准备一批供着色器处理的顶点数据。
const GLfloat vertices[] =
{
// 顶点坐标(NDC坐标),屏幕左下角(-1.0f, -1.0f),右上角(1.0f, 1.0f)
-1.0f, -1.0f, // 左下角,index = 0
1.0f, -1.0f, // 右下角,index = 1
1.0f, 1.0f, // 右上角,index = 2
-1.0f,-1.0f, // 左下角,index = 3
1.0f, 1.0f, // 右上角,index = 4
-1.0f,1.0f, // 左上角,index = 5
};
// 按索引顺序,绘制的三角形如下,构成一个矩形:
// 5 -- 4 2
// | / / |
// | / / |
// | / / |
// 3 0 -- 1
GLuint VAO; // handle of VAO
GLuint VBO; // handle of VBO
glGenBuffers(1, &VBO); // 创建一个Buffer Object,在显存中
glBindBuffers(GL_ARRAY_BUFFER, VBO); // 绑定为VBO,只有在绑定时才最终决定这个是干什么用的buffer
// 开辟一块内存(显存),由VBO管理,并将CPU内存中的数据vertices,一次性大批传递到这块显存中
// 此时顶点数据就缓存在了GPU内存中。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glGenVertexArrays(1, &VAO); // 创建一个VAO对象,这个就不是Buffer缓冲类型的了。
glBindVertexArray(VAO); // 绑定为当前VAO,接下来的设置的相关状态都会记录到VAO中。
// 告诉OpenGL,顶点着色器中的attribute变量如何从当前的VBO管理的内存中读取数据。
// 下面glVertexAttribPointer这句是告诉OpenGL,attribute变量:layout (location = index) in vec2 aPos;
// 它的数据从当前VBO中内存中的第一个地址偏移pointer个字节开始每次执行读取size*type个字节的数据,
// 两次读取的首地址之间的间隔是stride个字节
GLuint index = 0;
GLint size = 2; // 分量数量
GLenum type = GL_FLOAT; // 分量类型 size和type就组成了数据类型,也就是vec2
GLboolean normalized = GL_FALSE; // 数据是否需要归一化。
GLsizei stride = 2 * sizeof(GLfloat);
const GLvoid* pointer = (GLvoid*)(0 * sizeof(GLfloat));
glVertexAttribPointer(index, size, type, normalized, stride, pointer);
glEnableVertexAttribArray(index); // 这个是开关,千万别忘记调用,不然就不会生效了。
五、渲染绘制
GLuint programID = -1; // handle of gl program object
GLuint VAO; // handle of VAO
GLuint VBO; // handle of VBO
......; // 执行前面的代码,获取上面这些数据 。
while(......){ // 渲染循环
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 准备好顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 使用VBO
glBindVertexArray(VAO); // 使用VAO
// 准备好shader program
glUseProgram(programID); // 使用GLProgram
// 设置好shader program中的uniform变量的值
// 获得GLProgram(programID)中名称叫appColor的uniform变量的location
GLint uniformLocation = glGetUniformLocation(programID, "appColor" );
glUniform3f(uniformLocation, 0.0f, 0.0f, 1.0f ); // appColor赋值
// 所有数据准备完备,可以开始绘制了。
// GLProgram的数据全部准备完毕,可以执行绘制(glDrawArrays/glDrawElements)来启动管线了
GLenum mode = GL_TRIANGLES;
GLint first = 0; // 第一个顶点数据,在VBO中的索引
GLsizei count = 6; // 一次绘制6个顶点,索引顺序偏移
// 绘制顶点构成的三角形,按顺序拼成。
// 0,1,2构成了第一个三角形。
// 3,4,5构成了第二个三角形。
glDrawArrays(GL_TRIANGLES, 0, 6);
}