2.画一个三角形
配置VS2019,加载常用的库,参考1.1:
链接: link.
2.1预先需要知道的知识
1、屏幕和窗口是2D像素数组,因此当我们描绘3D图形的时候,我们需要工具把3D坐标转变为适应屏幕的2D像素,这就是OpenGL需要完成的工作。
2、这部分工作主要有图形渲染管线来完成(Graphics Pipeline)
第一部分把3D坐标转换为2D坐标
第二部分把2D坐标转换为实际的有颜色的像素
3、2D坐标和像素的不同:2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。
4、着色器:在渲染管线当中各自运行的一些小程序,在图形渲染管线中处理我们的数据
5、我们在接下来的代码中以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形
这个数组叫做顶点数据(Vertex Data)。顶点数据是一系列顶点的集合,一个顶点(Vertex)是一个3D坐标的数据的集合。
6、顶点着色器:输入:一个单独的顶点 作用:把3D坐标转为另一种3D坐标,同时可以对顶点数据进行基本处理
7、图元装配(primitive Assembly):将顶点着色器输出的所有顶点作为输入,将所有的顶点装配成指定图元的形状
8、几何着色器(Geoetry Shader):接受图元装配的输出作为输入,通过产生新顶点构造出新的图元来生成其他形状
9、光栅化阶段(Rasterization Stage),把图元映射为最终屏幕上相应的像素。生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
10、片段着色器的主要目的是计算一个像素的最终颜色,通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
2.2 顶点输入
OpenGL是3D图形库,因此我们输入的所有坐标都是3D坐标(x,y,z)的形式。
OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。
所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
创建数组,指定三角形的三个顶点:
GLfloat vertices[]={
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f,
0.0f,0.5f,0.0f
};
//创建了一个GLfloat数组,因为这里渲染的是一个2D三角形,所以三个顶点的z坐标都为0
定义了顶点数据后,顶点着色器接收数据在GPU上创建内存用于储存顶点数据,并解释内存发送给显卡。
通过VBO管理内存
VBO(Vertex Buffer Objects, VBO)顶点缓冲对象,可以一次性地发送一大批数据到显卡上,而不是每个顶点发送一次。
通过glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint VBO;//声明一个GLuint变量,GLuint相当于GL里面类似C++的unsigned int
glGenBuffers(1, &VBO);
//glGenBuffers 官方解释:generate buffer object names
//第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组
//该函数会在buffers里返回n个缓冲对象的名称。
//在本程序中,我们只需要他返回一个数组
声明一个GLuint变量,然后使用glGenBuffers后,它就会把缓冲对象保存在vbo里
glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量,我们可以分配内存对象并且用它的名称来引用这个内存对象。
使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标
顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//glBindBuffer 官方解释:bind a named buffer object
//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称,也就是我们在上一个函数里生成的名称VBO
绑定对象的过程就像设置铁路的道岔开关,每一个缓冲类型中的各个对象就像不同的轨道一样,我们将开关设置为其中一个状态,那么之后的列车都会驶入这条轨道。
从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof
计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
- GL_STATIC_DRAW :数据不会或几乎不会改变。(基本都是用这个)
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
2.3顶点着色器
如果我们打算做渲染的话,我们需要至少设置一个顶点和一个片段着色器
#version 330 core
//声明OpenGL版本,必不可少 每个着色器都起始于一个版本声明。
layout (location = 0) in vec3 position;
//创建一个vec3输入变量position
//位置变量的属性位置值为0
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
//设置顶点着色器的输出,把位置数据赋值非预定义的gl_Position变量,
//由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。
//我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f
//(我们会在后面解释为什么)来完成这一任务
}
2.4 编译着色器
2.4.1创建着色器对象
GLuint vertexShader
vertexShader=glcreateShader(GL_VERTEX_SHADER);
2.4.2 将着色器源码附加到着色器对象上并编译
glshaderSource(vertexShader,1,&vertexShaderSource,NULL);
//第一个参数是要编译的着色器对象:VertexShader
//第二个参数是指定传递的源码字符串数量
//第三个参数是顶点着色器真正的源码
glCompileShader(vertexShader);
2.5片段着色器
片段着色器全是关于计算像素最后的颜色输出
2.5.1在OpenGL中定义颜色
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分度,缩写为RGBA。
颜色的每个分量的强度设置在0.0到1.0之间
#version 330 core
out vec4 color //声明是一个4分量向量
void main()
{
color=vec4(1.0f,0.5f,0.2f,1.0f);//橘黄色
//alpha值为1.0代表完全不透明
}
2.5.2 编译片段着色器
使用GL_FRAGMENT_SHADER常量作为着色器类型
GLuint fragmentShader;//定义一个fragmentShader整形变量
fragmentShader = glCreateShader(GL_Fragment_SHADER);
//glCreateShader创建一个着色器对象,括号内的参数指定要创建的着色器类型
//只支持两种类型的着色器:GL_VERTEX_SHADER和GL_FRAGMENT_SHADER。
glShaderSource(fragmentShader,1,&fragmentShaderSource,null);
glCompileShader(fragmentShader);
GL_VERTEX_SHADER类型的着色器是一个用于在可编程顶点处理器上运行的着色器。 GL_FRAGMENT_SHADER类型的着色器是一个着色器,旨在在可编程片段处理器上运行。
2.6着色器程序
根据前面的步骤,顶点着色器和片段着色器都已经编译了。
着色器程序对象是多个着色器合并之后并最终链接完成的版本
创建一个程序对象
GLuint shaderProgram;
shaderProgram = glCreateProgram();
//glCreateProgram函数创建一个程序,并返回新创建程序对象ID引用
把之前编译的着色器附加到程序对象上,然后用函数链接它们
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
得到一个完整的着色器程序后,我们调用glUserProgram函数用刚创建的程序对象作为它的参数,以激活这个程序对象
glUseProgram(shaderProgram);
现在可以删除着色器对象:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
2.7 链接顶点属性
我们需要在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据将被如下图解释:
-
位置数据被储存为32-bit(4字节)浮点值
-
每个位置包含3个这样的值
-
在这3个值之间没有间隙(或其他值)。这几个值在数组中紧密排列
-
数据中第一个值在缓冲开始的位置
2.7.1 glVertexAttribPointer函数
使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据
glvertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*siezeof(GLfloat),(GLvoid*)0);
glVertexAttribPointer函数的参数:
第一个参数:指定我们要配置的顶点属性。
在前面的顶点着色器中,我们使用了layout(location=0)定义了position顶点属性的位置值location,也就是把顶点属性的位置值设置了为0
第二个参数:指定顶点属性的大小
这里的顶点属性是vec3,由3个值组成,所以大小是3
第三个参数:指定数据类型
vec*都是由浮点数值GL_FLOAT组成
第四个参数:决定数据是否被标准化。
TRUE则所有数据映射到0
第五个参数:输入在连续的顶点属性组之间的间隔。也就是一个顶点的内存大小。
每个顶点的数据占据了3*sizeof(GLfloat)的大小
第六个参数:表示位置数据在缓冲中起始位置的偏移量(Offset)。(不太清楚实际意思。。。)
2.7.1 glEnableVertexAttribArray函数
glEnableVertexAttribArray(0);//以顶点属性位置值作为参数,启用顶点属性,顶点属性默认是禁用的
顶点数组对象(VAO)
VAO存储所有的顶点属性调用,当配置顶点属性指针时,只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
只需要绑定不同的VAO就可以在不同的顶点数据和属性配置之间切换。
VAO中储存的内容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过
glVertexAttribPointer
调用进行的顶点缓冲对象与顶点属性链接。
VAO和VBO的关系图解
创建和使用VAO
Gluint VAO;
glGenVertexArrays(1,&VAO);//创建了一个VAO对象,与创建一个VBO对象类似
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。
使用glDrawArrays函数激活着色器绘制图形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引,我们这里填0
。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
完整源代码(在vs2019上可运行)
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GL/glut.h>
// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;
// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
// The MAIN function, from here we start the application and run the game loop
int main()
{
// Init GLFW
glfwInit();
// Set all the required options for GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// Create a GLFWwindow object that we can use for GLFW's functions
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
glfwMakeContextCurrent(window);
// Set the required callback functions
glfwSetKeyCallback(window, key_callback);
// Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
glewExperimental = GL_TRUE;
// Initialize GLEW to setup the OpenGL Function pointers
glewInit();
// Define the viewport dimensions
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
// Build and compile our shader program
// Vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// Check for compile time errors
GLint success;
GLchar 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;
}
// Fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// Check for compile time errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// Link shaders
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// Check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// Set up vertex data (and buffer(s)) and attribute pointers
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0); // Note that this is allowed, the call to glVertexAttribPointer registered VBO as the currently bound vertex buffer object so afterwards we can safely unbind
glBindVertexArray(0); // Unbind VAO (it's always a good thing to unbind any buffer/array to prevent strange bugs)
// Game loop
while (!glfwWindowShouldClose(window))
{
// Check if any events have been activiated (key pressed, mouse moved etc.) and call corresponding response functions
glfwPollEvents();
// Render
// Clear the colorbuffer
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
// Swap the screen buffers
glfwSwapBuffers(window);
}
// Properly de-allocate all resources once they've outlived their purpose
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// Terminate GLFW, clearing any resources allocated by GLFW.
glfwTerminate();
return 0;
}
// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}