让我们一起用OpenGL绘制一个自己的三角形吧!
第一步 先把要画的三角形顶点数据输入进去,将它们储存在显卡内存中
//将三角形的三个顶点的3D坐标以标准化坐标形式定义为一个float数组
float vertices[]{
-0.5f, -0.5f, 0.0f, //left
0.5f, -0.5f, 0.0f, //right
0.0f, 0.5f, 0.0f //top
}
第二步 创建顶点着色器(Vertex Shader)和片段着色器(fragment shader)来处理数据,分为三小步
1.创建VBO
我们在确定了三角形的输入数据之后,要发送给图形渲染管线上的第一个处理阶段:顶点着色器,它会在GPU上创建内存用于储存我们的顶点数据。
之后用VBO(Vertex Buffer Object 顶点缓冲对象)来管理内存,在GPU内存中存储大量的顶点,可以一次性发送一大批数据到显卡上,之后顶点着色器几乎能立即访问顶点,比起一个一个顶点的发可谓是很快了
unsigned int VBO;
glGenBuffers(1, &VBO); //创建顶点缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); //VBO的缓冲类型是GL_ARRAY_BUFFER,把顶点数组复制到缓冲中供OpenGL使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
函数解释:
/*
glBindBuffer():
用把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
之后使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)
调用glBindBuffer函数,会把之前定义的顶点数据复制到缓冲的内存中
glBufferData(1,2,3,4):
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数
1.GL_ARRAY_BUFFER:是目标缓冲(VBO)的类型
2.sizeof(vertices):指定传输数据的大小
3.vertices:我们希望发送的实际数据4.GL_STATIC_DRAW:指定我们希望显卡如何管理给定的数据
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
*/
2.创建顶点着色器(Vertex Shader)
//vertex shader
unsigned int vertexShader; //创建着色器对象
vertexShader = glCreateShader(GL_VERTEX_SHADER); //把需要创建的着色器类型以参数形式提供给glCreateShader
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //把着色器源码附加到着色器对象上
glCompileShader(vertexShader); //编译顶点着色器对象
//检查调用glCompileShader后编译是否成功
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;
}
函数说明:
/*
glShaderSource(1,2,3,4)函数格式:
1.编译的着色器对象
2.指定了传递的源码字符串数量
3.顶点着色器真正源码
4.先设置为NULL
*/
顶点着色器的源码
//顶点着色器的源代码硬编码
const char* vertexShaderSource = "#version 330 core\n" //用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器
"layout (location = 0) in vec3 aPos;\n" //通过layout(location = 0)设定了输入变量的位置值(Location)
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" //由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的
"}\0";
3.创建片段着色器(fragment shader)
// fragment shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//check for shader compile 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;
}
片段着色器源码
//片段着色器的源代码硬编码
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"
"}\n\0";
第三步 链接着色器
//link shaders
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建一个程序,并返回新创建程序对象的ID引用
glAttachShader(shaderProgram, vertexShader); //把之前编译的着色器附加到程序对象上
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); //链接它们
第四步 告诉OpenGL怎么解释内存中的顶点数据以及处理顶点数据连接到顶点着色器属性上 下面的内容是不使用VAO情况下的代码流程
/*里面的内容是不使用VAO情况下的代码流程*/
//复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//链接顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //允许顶点着色器读取GPU(服务器端)数据
/*
glVertexAttribPointer(1,2,3,4,5,6);
1.指定要配置的顶点属性 layout (location = 0)
2.顶点属性的大小 vec3
3.数据的类型
4.是否希望数据被标准化(Normalize) GL_TRUE[所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间]
5.步长(Stride) 连续的顶点属性组之间的间隔
6.参数的类型是void* 表示位置数据在缓冲中起始位置的偏移量(Offset)*/
glUseProgram(shaderProgram); // 当我们渲染一个物体时要使用着色器程序
someOpenGLFunctionThatDrawsOurTriangle(); // 绘制物体
*/
每当我们绘制一个物体的时候都必须重复这一过程,如果顶点属性很多,物体也很多,绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事,因此我们引入VAO(顶点数组对象(Vertex Array Object),将所有状态配置储存在一个对象,通过绑定这个对象来恢复状态
unsigned int VAO; //创建顶点数组对象(Vertex Array Object, VAO)
glGenVertexArrays(1, &VAO); //生成顶点数组对象名称
glBindVertexArray(VAO); //绑定VAO
下面是绘制三角形的完整代码:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include<iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
//顶点着色器的源代码硬编码
const char* vertexShaderSource = "#version 330 core\n" //用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器
"layout (location = 0) in vec3 aPos;\n" //通过layout(location = 0)设定了输入变量的位置值(Location)
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" //由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的
"}\0"; //同时把w分量设置为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"
"}\n\0";
int main()
{
//glfw:初始化和配置
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
//glfw窗口创建
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
/*第一步 把顶点数据存储在显卡内存中*/
//将三角形的三个顶点的3D坐标以标准化设备坐标的形式定义为一个float数组
float vertices[]{
-0.5f, -0.5f, 0.0f, //left
0.5f, -0.5f, 0.0f, //right
0.0f, 0.5f, 0.0f //top
};
/*第二步 着色器程序:创建顶点着色器(Vertex Shader)和片段着色器(fragment shader)来处理数据*/
//1.vertex shader
unsigned int vertexShader; //创建着色器对象
vertexShader = glCreateShader(GL_VERTEX_SHADER); //把需要创建的着色器类型以参数形式提供给glCreateShader
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //把着色器源码附加到着色器对象上
glCompileShader(vertexShader); //编译顶点着色器对象
//检查调用glCompileShader后编译是否成功
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;
}
// 2.fragment shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//check for shader compile 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;
}
//3.link shaders
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建一个程序,并返回新创建程序对象的ID引用
glAttachShader(shaderProgram, vertexShader); //把之前编译的着色器附加到程序对象上
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); //链接它们
//check for linking error
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
//glUseProgram(shaderProgram);//调用该函数激活程序对象
glDeleteShader(vertexShader);//删除着色器对象
glDeleteShader(fragmentShader);
/*第三步 告诉OpenGL怎么解释内存中的顶点数据以及处理顶点数据连接到顶点着色器属性上
创建VBO和VAO*/
unsigned int VBO;
unsigned int VAO; //创建顶点数组对象(Vertex Array Object, VAO)
glGenBuffers(1, &VBO); //创建顶点缓冲对象
glGenVertexArrays(1, &VAO); //生成顶点数组对象名称
glBindVertexArray(VAO); //绑定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(float), (void*)0);//设置顶点属性指针
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0); //解绑
glBindVertexArray(0); //解绑
//glDrawArrays(GL_TRIANGLES, 0, 3);
/*
glDrawArrays(1,2,3)
1.绘制的OpenGL图元的类型 GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
2.顶点数组的起始索引
3.打算绘制多少个顶点
*/
//取消注释此调用以绘制线框面 (无填充)
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); //函数选择多边形光栅化模式 GL_FRONT_AND_BACK正面多边形和后向多边形 GL_LINE多边形的边界边缘绘制为线段
// render loop 渲染循环
// 明确地关闭它之前不断绘制图像并能够接受用户输入
// 函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后游戏循环便结束了,之后为我们就可以关闭应用程序了。
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindVertexArray(0); // no need to unbind it every time
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// WAP 缓冲区和循环 IO 事件(按下/释放键、移动鼠标等)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// 可选:在资源超出用途后取消分配所有资源:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// glfw: terminate, clearing all previously allocated GLFW resources.
// GLFW:终止,清除所有以前分配的 GLFW 资源。
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// 处理所有输入:查询GLFW是否按下/释放此帧的相关键并做出相应的反应
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// glfw:每当窗口大小发生变化(通过操作系统或用户调整大小)时,都会执行此回调函数
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
//确保视口与新的窗口尺寸匹配;请注意,宽度和高度将明显大于 Retina 显示屏上指定的宽度和高度。
glViewport(0, 0, width, height);
//前两个参数控制窗口左下角的位置,第三个和第四个参数控制渲染窗口的宽度和高度(像素)
}
只是学习笔记:
文章参考来源
小白一个,有错误欢迎在评论区指出和讨论