最新大型开源项目-云游戏,云桌面系统,欢迎关注
本项目代码仓库
1. 现实中我们如何精确的绘制一个有颜色的三角形?
- 1.1 确定三个顶点的位置
- 1.2.连接三个顶点,成三角形状
- 1.3.涂上颜色
通过以上三步,我们可以轻松的绘制一个三角形。那么在OpenGL中如何做呢?
2. OpenGL的工作流程
首先看一张图:
这也可以称为简洁版的OpenGL的渲染管线(Graphics PipeLine),但也完全可以说明问题。
从图中可以看出,OpenGL的也是接受顶点,组成形状,染上颜色,最终输出到屏幕上。
从头开始看:
-
2.1 顶点数据 ,这部分由我们的代码(通常是在C++中)准备好,送入显存。
-
2.2 顶点着色器(Vertex Shader),由开发人员提供且必须提供,简单来说,就是告诉OpenGL,我的点,在什么位置。
-
2.3 图元装配,根据绘制的图形,将顶点构造成相应的图形,OpenGL的底层实现,无需操作。
-
2.4 几何着色器(Geometry Shader),OpenGL底层有实现,开发人员也可以根据需求自己实现。比如我这里实现过的,大场景草地渲染,由16万个顶点,经过几何变换,生成32万棵草(后面教程再详细展开,默认不用提供此着色器):
-
2.5 光栅化,几何意义上的三角形是连续的直线组成的图形,但是屏幕是有分辨率的,离散的,所以光栅化会将像素对应到三角形。OpenGL默认实现,无需操作。因为是离散的屏幕,所以也会有锯齿的出现,抗锯齿是另一个话题,一般情况下GLFW,Qt都可以很简单的设置抗锯齿。当然,离屏渲染的抗锯齿需要自己设置,后面再详细展开,上图的草地就是用了离屏渲染的抗锯齿。可以参看这张图,显示了锯齿的形成:
-
2.6 片段着色器(Fragment Shader),决定每个片段的颜色。通常情况下一个片段就是一个像素,但超高清屏幕因为视网膜无法分辨一个像素,还是多个像素,会有优化,那么会出现多个像素共享一个片段颜色,以加快渲染速度。因为大多数情况下一个片段就是一个像素,也叫像素着色器。这个着色器必须由开发人员提供。
-
2.7 后面的测试,混合,以及再后面的视口变换暂时还用不上,后面进入3D会大量使用,且都是OpenGL已经实现的,我们只需要跟据自己的需求,设置不同的参数就好了。
总结来说,途中蓝色的方框代表的操作,都是可以更改的。其中顶点着色,片段着色是必须的。很容易发现,顶点着色器的运行次数,要远小于片段着色器,简单来看,三角形的顶点着色执行三次,而组成一个三角形的像素有成百上千,甚至百万个,而每个像素都要执行片段着色。
3. 着色器长什么样?
- 3.1 首先看一个最简单的顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos, 1.0);
}
- 3.1 #version 330 core,这一句表示运行版本和模式。
- 3.2 layout (location = 0) in vec3 aPos; 这句是声明一个变量,接受C++传递的顶点数据。
- 3.3 gl_Position = vec4(aPos, 1.0); 将我们的顶点变成齐次坐标,发送给OpenGL。其中gl_Position是内置变量,用来接收一个齐次坐标。所谓齐次坐标(x,y,z,w),是方便平移,旋转,缩放用同一个矩阵来操作而发明的,w=1代表一个点,w=0代表一个向量。
那么可以理解一下这个着色器代表的意义:我给你的点在什么位置,就给我输出什么位置。
- 3.2 片段着色器
#version 330 core
void main()
{
gl_FragColor = vec4(1.0f, 0.5f, 0.8f, 1.0f);
}
gl_FragColor 是内置变量,保存的是RGBA,来决定这个片段/像素的颜色,那么这个着色器的意思就是:给我染成粉色
4 着色器怎么用?
像C/C++一样,编译,链接后,就可以使用了,如下代码:
ShaderPtr ShaderLoader::LoadShader(const std::string& v_gl, const std::string& g_gl, const std::string& f_gl) {
auto result = std::make_shared<ShaderProgram>();
GLint compile_result = GL_FALSE;
int log_len;
// Create the shaders
GLuint vertex_shader_id = GLFunc glCreateShader(GL_VERTEX_SHADER);
GLuint fragment_shader_id = GLFunc glCreateShader(GL_FRAGMENT_SHADER);
GLuint geometry_shader_id = GLFunc glCreateShader(GL_GEOMETRY_SHADER);
// Compile Vertex Shader
char const *vertex_source = v_gl.c_str();
GLFunc glShaderSource(vertex_shader_id, 1, &vertex_source, NULL);
GLFunc glCompileShader(vertex_shader_id);
// Check Vertex Shader
GLFunc glGetShaderiv(vertex_shader_id, GL_COMPILE_STATUS, &compile_result);
GLFunc glGetShaderiv(vertex_shader_id, GL_INFO_LOG_LENGTH, &log_len);
if (log_len > 0) {
std::vector<char> err_msg(log_len + 1);
GLFunc glGetShaderInfoLog(vertex_shader_id, log_len, NULL, &err_msg[0]);
printf("%s\n", &err_msg[0]);
}
if (!g_gl.empty()) {
// Compile Geometry Shader
char const *geometry_source = g_gl.c_str();
GLFunc glShaderSource(geometry_shader_id, 1, &geometry_source, NULL);
GLFunc glCompileShader(geometry_shader_id);
// Check Vertex Shader
GLFunc glGetShaderiv(geometry_shader_id, GL_COMPILE_STATUS, &compile_result);
GLFunc glGetShaderiv(geometry_shader_id, GL_INFO_LOG_LENGTH, &log_len);
if (log_len > 0) {
std::vector<char> err_msg(log_len + 1);
GLFunc glGetShaderInfoLog(geometry_shader_id, log_len, NULL, &err_msg[0]);
printf("%s\n", &err_msg[0]);
}
}
// Compile Fragment Shader
char const *fragment_source = f_gl.c_str();
GLFunc glShaderSource(fragment_shader_id, 1, &fragment_source, NULL);
GLFunc glCompileShader(fragment_shader_id);
// Check Fragment Shader
GLFunc glGetShaderiv(fragment_shader_id, GL_COMPILE_STATUS, &compile_result);
GLFunc glGetShaderiv(fragment_shader_id, GL_INFO_LOG_LENGTH, &log_len);
if (log_len > 0) {
std::vector<char> err_msg(log_len + 1);
GLFunc glGetShaderInfoLog(fragment_shader_id, log_len, NULL, &err_msg[0]);
printf("%s\n", &err_msg[0]);
}
// Link the program
GLuint program_id = GLFunc glCreateProgram();
GLFunc glAttachShader(program_id, vertex_shader_id);
if (!g_gl.empty()) {
GLFunc glAttachShader(program_id, geometry_shader_id);
}
GLFunc glAttachShader(program_id, fragment_shader_id);
GLFunc glLinkProgram(program_id);
// Check the program
GLFunc glGetProgramiv(program_id, GL_LINK_STATUS, &compile_result);
GLFunc glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &log_len);
if (log_len > 0) {
std::vector<char> err_msg(log_len + 1);
GLFunc glGetProgramInfoLog(program_id, log_len, NULL, &err_msg[0]);
printf("%s\n", &err_msg[0]);
}
GLFunc glDetachShader(program_id, vertex_shader_id);
GLFunc glDetachShader(program_id, fragment_shader_id);
GLFunc glDetachShader(program_id, geometry_shader_id);
GLFunc glDeleteShader(vertex_shader_id);
GLFunc glDeleteShader(fragment_shader_id);
GLFunc glDeleteShader(geometry_shader_id);
result->program_id = program_id;
return result;
}
这部分代码就是一个工具,固定的流程,实现一个方法就好。
5. 给我们的三角形设定坐标
- 5.1 OpenGL的坐标系:
从图中可以看出,中心为原点,范围从是 [-1, 1],那么我们的三角形也可以轻松的定义:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
6.如何将我们的数据给OpenGL?
- 6.1 因为运行在Core模式下,我们必须要有一个 顶点数组对象:Vertex Array Object,VAO ,然后再进行我们其他的GL操作。每个VAO都可以包含一组操作,参考下图:
GLuint VAO;
glGenVertexArrays(1, &VAO);
....其他OpenGL操作
glBindVertexArray(0);
在实践中,我们绘制多个图形,模型都必须使用VAO,例如我做的这个场景:
- 6.2 我们需要一个显存上的顶点缓冲对象,将数据发送给他,OpenGL就能使用了。
GLuint VAO;
GLuint VBO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
生成一个VBO
glGenBuffers(1, &VBO);
绑定这个VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
将数据传递到这个VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
这里的0,对应的是我们的 顶点着色器的 aPos 指定的location,启用它,并告诉OpenGL如何使用我们的数据。
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, (void*)0);
glBindVertexArray(0);
这个函数参数较多,逐一解释。
glVertexAttribPointer(0, // 对应 aPos 的location
3, // 表示这个属性有几个数据
GL_FLOAT, // 每个数据的类型
false, //是否要归一化,因为我们是在[-1,1]范围内定义的,所以不需要。
0, //跨度(STRIDE),指的是在内存中,到达下一个相同的属性,跨过的字节数。这里是0,代表整个数据都是相同的。后面的教程会修改这个值。
(void*)0);//(OFFSET)在每个跨度中的起始位置
参看这个图:
7. 绑定VAO,执行绘制操作
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);