做完了这一部分的练习了,总共花了三个晚上,遇到了如下两个问题
-
编译通过,图形不显示
核心模式下,必须使用到VAO,单纯使用VBO时不会绘制。 -
不能同时画两个图形
每个图形都需要一套完整的配置,包括数据、绑定缓冲、数据解读方式设置、绘制函数
先记住几个名词
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
一些概念上的点
- OpenGL里面,任何事物都在3D空间中,屏幕和窗口是2D像素数组, OpenGL的大部分工作就是把3D坐标变为适应屏幕的2D像素。
- 3D转2D的处理过程 由图形渲染管线管理。
管线:一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程
变化处理:3D坐标转2D坐标,2D坐标转变为有颜色的像素
坐标:精确表示一个点在2D空间的位置
像素:这个点的近似值(应该理解为大概位置吧,像素填充坐标)
图形渲染管线(管线)
- 功能:3D坐标 -> 有色2D像素
- 着色器: 快速处理数据()
- 阶段 :顶点数据->顶点着色器->形状(图元)装配->几何着色器->光栅化->片段着色器->测试与混合
顶点数据:顶点数据输入给顶点着色器
顶点着色器:3D坐标->另一种3D坐标
形状装配:顶点着色器的顶点->装配成指定图元形状
数据渲染成一系列的点,还是三角形,还是一条长长的线?GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
几何着色器:图元形式的一系列点->产生新顶点构造出新的图元
光栅化:图元映射为最终屏幕上的像素,生成供片段着色器使用的片段
片段着色器:计算最终颜色
也是所有OpenGL高级效果产生的地方。通常包含3D场景的数据(光照、阴影、光的颜色)这些数据将可以用来计算最终像素的颜色
测试与混合:对各个片段混合,透明度,先后覆盖顺序等等
初步使用过程
- 定义至少一个顶点着色器
- 可以使用默认几何着色器
- 定义至少一个片段着色器
代码化这一个过程:
- 给OpenGL输入一些顶点数据。(3D坐标)
关于坐标
float vertices[] = {
/* X轴, Y轴, Z轴(平面图形无需Z轴) */
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
- 顶点数据传给顶点着色器
通过顶点缓冲对象,一次性发送大量数据到显卡,数据发送到显卡上的内存中后,顶点着色器能够立即访问顶点。
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
unsigned int VBO;
glGenBuffers(1, &VBO);
//绑定缓冲对象类型 GL_ARRAY_BUFFER为顶点缓冲类型
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//赋值顶点数据到缓冲内存
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
/*
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数
参数1:目标缓冲的类型 GL_ARRAY_BUFFER为顶点缓冲类型
参数2:指定传输数据的大小(以字节为单位)
参数3:希望发送的实际数据
参数4:指定了我们希望显卡如何管理给定的数据
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
*/
- 编写顶点着色器(顶点着色器语言)
- 版本声明
- in关键字,在顶点着色器中声明所有的输入顶点属性,location变量的位置值
- 放在函数内
- 位置数据赋值给预定义的gl_Position变量
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
- 编译顶点着色器
- 暂时将上述代码硬编码在代码文件C风格字符串中
- 创建着色器对象,通过ID引用,因此类型为unsigned int
- 把需要创建的着色器类型提供给glCreateShader,顶点着色器GL_VERTEX_SHADER
- 把着色器源码附加到着色器对象上
- 编译
- 检查编译成功与否
//* 暂时将上述代码硬编码在代码文件**C风格字符串**中
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
//创建着色器对象,通过ID引用,因此类型为unsigned int
unsigned int vertexShader;
//把需要创建的着色器类型提供给glCreateShader,顶点着色器GL_VERTEX_SHADER
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把着色器源码附加到着色器对象上
//glShaderSource函数
//第一个参数:要编译的着色器对象作为。
//第二参数指定了传递的源码字符串数量,这里只有一个。
//第三个参数是顶点着色器真正的源码
//第四个参数我们先设置为NULL。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译
glCompileShader(vertexShader);
//检查编译是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
//不成功则打印相关信息
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
- 编写片段着色器,编译着色器
#version 330 core
out vec4 FragColor;
void main()
{
// R G B 透明度
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
unsigned int fragmentShader;
//GL_FRAGMENT_SHADER 片段着色器
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
- 最终链接成着色器程序对象
//创建程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
//链接着色器
//附加顶点着色器
glAttachShader(shaderProgram, vertexShader);
//附加片段着色器
glAttachShader(shaderProgram, fragmentShader);
//链接
glLinkProgram(shaderProgram);
//判断链接成功与否
int success;
char infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//激活程序对象
glUseProgram(shaderProgram);
//glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象
//程序调用完毕 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
- 链接顶点属性(渲染前指定OpenGL该如何解释顶点数据)
* 使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
/*
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
*/
//设置解析方式
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//启动顶点属性
glEnableVertexAttribArray(0);
- 综合代码化
我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
索引缓冲对象(EBO)
保持VAO和VBO不变得情况下,增加索引方式
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 // 第二个三角形
};
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
.