#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <glad/glad.h>
#include <glfw3.h>
//使用VAO
#define USE_VAO 1
/*
三角形绘制基础代码
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
*/
unsigned int VBO = 0;
unsigned int VBO2 = 0;
unsigned int VAO = 0;
unsigned int VAO2 = 0;
unsigned int shaderProgram = 0;
void rend()
{
#if USE_VAO
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(VAO2);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
#else
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
#endif
}
void initModel()
{
//完成VAO VBO
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
/*!
* VAO (vertex array object)
*/
#if USE_VAO
glGenVertexArrays(1, &VAO); ///> VAO 中包含了 VBO
glBindVertexArray(VAO);
#endif
/*! VBO (vertex buffer object) 在GPU上开辟一块空间存储数据(vertices)
*
* openGL创建VBO的骤:
* 1、获取VBO的index
* 2、绑定VBO的index
* 3、给VBO分配显存空间 传输数据
* 4、告诉shader数据解析方式
* 5、激活锚点
*/
//1
glGenBuffers(1, &VBO);
//2
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//3
/*
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。
如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,
这样就能确保显卡把数据放在能够高速写入的内存部分。
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//4
/*
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用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);
//5
glEnableVertexAttribArray(0);
//解除绑定
glBindBuffer(GL_ARRAY_BUFFER, 0);
#if USE_VAO
glBindVertexArray(0);
#endif
#if 1
//-----------------------------------
float vertices2[] = {
-0.0f, -0.1f, 0.0f,
0.8f, -0.5f, 0.0f,
0.0f, 0.8f, 0.0f
};
#if USE_VAO
glGenVertexArrays(1, &VAO2); ///> VAO 中包含了 VBO
glBindVertexArray(VAO2);
#endif
glGenBuffers(1, &VBO2);
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
#if USE_VAO
glBindVertexArray(0);
#endif
#endif
}
void initShader(const char* _vertexPath, const char* _fragPath)
{
std::string _vertexCode("");
std::string _fragCode("");
std::ifstream _vShaderFile;
std::ifstream _fShaderFile;
_vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
_fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
_vShaderFile.open(_vertexPath);
_fShaderFile.open(_fragPath);
std::stringstream _vShaderStream;
std::stringstream _fShaderStream;
_vShaderStream << _vShaderFile.rdbuf();
_fShaderStream << _fShaderFile.rdbuf();
_vertexCode = _vShaderStream.str();
_fragCode = _fShaderStream.str();
}
catch (std::ifstream::failure e)
{
std::string errStr = "read shader fail";
std::cout << errStr << std::endl;
}
const char* _vShaderStr = _vertexCode.c_str();
const char* _fShaderStr = _fragCode.c_str();
//shader的编译链接
unsigned int _vertexID = 0;
unsigned int _fragID = 0;
char _infoLog[512];
int _successFlag = 0;
/*
* [01] 编译
*/
//vertexShader
_vertexID = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(_vertexID, 1, &_vShaderStr, nullptr);
glCompileShader(_vertexID);
glGetShaderiv(_vertexID, GL_COMPILE_STATUS, &_successFlag);
if (!_successFlag)
{
glGetShaderInfoLog(_vertexID, 512, nullptr, _infoLog);
std::cout << _infoLog;
}
//fragmentShader
_fragID = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(_fragID, 1, &_fShaderStr, nullptr);
glCompileShader(_fragID);
glGetShaderiv(_fragID, GL_COMPILE_STATUS, &_successFlag);
if (!_successFlag)
{
glGetShaderInfoLog(_fragID, 512, nullptr, _infoLog);
std::cout << _infoLog;
}
/*
* [02] 链接
*/
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, _vertexID);
glAttachShader(shaderProgram, _fragID);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &_successFlag);
if (!_successFlag)
{
glGetProgramInfoLog(shaderProgram, 512, nullptr, _infoLog);
std::cout << _infoLog;
}
glDeleteShader(_vertexID);
glDeleteShader(_fragID);
}
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
int main()
{
//初始化openGL的所有上下文环境
glfwInit();
//选择版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//选择OpenGL的核心模式
#if USE_VAO
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#else
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
#endif
//创建窗体
auto window = glfwCreateWindow(800, 600, "My First OpenGL Window", nullptr, nullptr);
if (window == nullptr)
{
std::cout << "Failed to create GLFW window" << std::endl;
//释放资源
glfwTerminate();
return -1;
}
//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
/*
* GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD
* 我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAddress,
* 它根据我们编译的系统定义了正确的函数。
*/
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
/*
* 在我们开始渲染之前还有一件重要的事情要做,
* 我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。
* 我们可以通过调用glViewport函数来设置窗口的维度
*
* 函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)
*
* 我们实际上也可以将视口的维度设置为比GLFW的维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,
* 这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。
*
* OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。
* 例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。
* 注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。
*
*/
glViewport(0, 0, 800, 600);
/*
* 然而,当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。
* 当窗口被第一次显示的时候framebuffer_size_callback也会被调用。对于视网膜(Retina)显示屏,width和height都会明显比原输入值更高一点。
*
* 我们还可以将我们的函数注册到其它很多的回调函数中。
* 比如说,我们可以创建一个回调函数来处理手柄输入变化,处理错误消息等。我们会在创建窗口之后,渲染循环初始化之前注册这些回调函数。
*/
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
initModel();
initShader("vertexShader.glsl", "fragmentShader.glsl");
while (!glfwWindowShouldClose(window))
{
processInput(window);
/*
* 在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)。
* 我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,
* 可能的缓冲位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲。
*/
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//----------------------- 这个中间可以做一些渲染 ---------------------------------------------||||||
rend();
//---------------------------------------------------------------------------------------------------
/*
* 双缓冲(Double Buffer)
* 应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,
* 而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,
* 这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,
* 它会在屏幕上显示;
* 而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,
* 这样图像就立即呈显出来,之前提到的不真实感就消除了。
*/
glfwSwapBuffers(window);
//处理一下相关的事件(鼠标键盘等)
glfwPollEvents();
}
/*
* 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。
* 我们可以在main函数的最后调用glfwTerminate函数来完成
*/
glfwTerminate();
return 0;
}