学习自LearnOpenGL
知识点
-
1.图形渲染管线:简要说明就是先将3D事物的坐标转换为2D的屏幕坐标,再把2D坐标转换为实际的有颜色的像素
-
2.着色器:图形渲染关键可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。在GPU上为每一个阶段运行各自的着色器,从而快速处理你的数据。
-
3.顶点缓冲对象(Vertex Buffer Objects,VBO):管理显存中顶点数据。好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
-
4.顶点数组对象(Vertex Array Object,VAO):对顶点属性的调用都会存储在VAO中。好处是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变的非常简单,只需要绑定不同的VAO就行了。
开始
- 1.顶点输入:OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴上都为-1.0到1.0的范围内时才处理它。
由于我们需要渲染一个三角形,我们要指定三个顶点,每个顶点都有一个3D位置。
//定义的顶点坐标数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
因为我们渲染的是有一个2D三角形,我们将它顶点的z坐标设置为0.0。这样子三角形每一点离我们的距离都是一样的,从而使它看上去像是2D的。
接着我们通过绑定顶点数组对象
//顶点数组对象
unsigned int VAO;
glGenVertexArrays(1, &VAO); //1代表一个id
glBindVertexArray(VAO);//绑定VAO
/*
unsigned int VAO[10];
glGenVertexArrays(10,VAO);//如果有多个VAO 传入数组名当作首地址
*/
一个顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用进行的顶点缓冲对象与顶点属性链接。
之后会用到
然后通过顶点缓冲对象来管理这个内存,我们使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:
//顶点缓冲对象
unsigned int VBO;
glGenBuffers(1, &VBO);//同理 1代表一个id 生成一个VBO对象
//把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO);//顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER,
//我们使用的任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲(VBO)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//将之前定义的顶点数据复制到缓冲的内存中
VAO与VBO之间的关系如图所示:
- 2.顶点着色器
先不用理解什么意思,之后会细讲。因为我们这里需要渲染三角形,要用到。
//顶点着色器设置源码
const char* vertexShaderSource =
"#version 330 core \n"
"layout (location = 0) in vec3 aPos; \n"
"void main(){ \n"
" gl_Position=vec4(aPos.x , aPos.y , aPos.z, 1.0);} \n";
- 3.片元着色器
//片段着色器设置源码
const char* fragmentShaderSource =
"#version 330 core \n"
"out vec4 FragColor; \n"
"void main() { \n"
" FragColor = vec4 (1.0f, 0.5f, 0.2f, 1.0f);} \n";
- 4.编译着色器
我们之前已经写好了着色器的源码,但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的代码
unsigned int vertexShader;
vertexShader= glCreateShader(GL_VERTEX_SHADER);//创建顶点着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。
//第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
glCompileShader(vertexShader);//编译顶点着色器
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);//创建片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);//编译片段着色器
我们已经编译完了顶点着色器和片段着色器,接着我们要使用着色器程序对象来将它们链接起来,然后在渲染对象的时候激活这个着色器程序。
//如果要使用刚才编译的着色器我们必须把它们链接称为一个着色器程序对象,
//然后在渲染对象的时候激活这个着色器程序。
//创建一个程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
//需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);//attach 英文翻译 依附
glLinkProgram(shaderProgram);
- 5.链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
- 位置数据被储存为32-bit(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
- 6.绘制物体
glUseProgram(shaderProgram);//调用shaderProgram
glBindVertexArray(VAO); //绑定VAO调用
glDrawArrays(GL_TRIANGLES, 0, 3);//画三角形 从下标为0的顶点开始 画3个顶点
大功告成,结果如下图
最终所有代码:
#define GLEW_STATIC //使用静态链接库
#include<GL/glew.h>
#include<GLFW/glfw3.h>
#include<iostream>
//定义的顶点坐标数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//顶点着色器设置源码
const char* vertexShaderSource =
"#version 330 core \n"
"layout (location = 0) in vec3 aPos; \n"
"void main(){ \n"
" gl_Position=vec4(aPos.x , aPos.y , aPos.z, 1.0);} \n";
//片段着色器设置源码
const char* fragmentShaderSource =
"#version 330 core \n"
"out vec4 FragColor; \n"
"void main() { \n"
" FragColor = vec4 (1.0f, 0.5f, 0.2f, 1.0f);} \n";
//回调函数
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {//按住Escape键
glfwSetWindowShouldClose(window, true);//退出窗口
}
}
int main() {
//固定格式
glfwInit();//初始化GLFW库
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//定义最高版本为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//定义最低版本为3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//Open GLFW Window
GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);//创建一个窗口800×600 第三个参数为标题名
if (window == NULL)
{
printf("Open window failed.");
glfwTerminate();//关闭中止窗口
return -1;
}
glfwMakeContextCurrent(window); //选择当前的window作为主线程
//Init GLEW
glewExperimental = true; //能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed");
glfwTerminate();
return -1;
}
glViewport(0, 0, 800, 600);//前两个参数是窗口的左下角的位置,后两个参数设置宽高
//顶点数组对象
unsigned int VAO;
glGenVertexArrays(1, &VAO); //1代表一个id
glBindVertexArray(VAO);//绑定VAO
/*
unsigned int VAO[10];
glGenVertexArrays(10,VAO);//如果有多个VAO 传入数组名当作首地址
*/
//顶点缓冲对象
unsigned int VBO;
glGenBuffers(1, &VBO);//同理 1代表一个id 生成一个VBO对象
//把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO);//顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER.
//我们使用的任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲(VBO)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//将之前定义的顶点数据复制到缓冲的内存中
/*
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。
它有三种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。
如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,
这样就能确保显卡把数据放在能够高速写入的内存部分。
*/
unsigned int vertexShader;
vertexShader= glCreateShader(GL_VERTEX_SHADER);//创建顶点着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。
//第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
glCompileShader(vertexShader);//编译顶点着色器
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);//创建片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);//编译片段着色器
//如果要使用刚才编译的着色器我们必须把它们链接称为一个着色器程序对象,
//然后在渲染对象的时候激活这个着色器程序。
//创建一个程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
//需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);//attach 英文翻译 依附
glLinkProgram(shaderProgram);
// 用来判断是否连接成功
GLint success;
GLchar infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::Program::LINK_FAILED\n" << infoLog << std::endl;
}
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//为了不让我们的程序花了一个简单的图像就立即退出或者关闭窗口,我们需要创建一个渲染循环(render loop)
while (!glfwWindowShouldClose(window))
{
//输入
processInput(window);
glClearColor(0.2f, 0.3f, 0.3, 1.0f);//当调用glClear()清除颜色缓冲之后,整个颜色缓冲会被填充为设置里的颜色
glClear(GL_COLOR_BUFFER_BIT);//清空颜色缓冲
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);//画三角形 从下标为0的顶点开始 画3个顶点
glfwSwapBuffers(window);//分为front buffer和back buffer,front buffer解决最终输出在屏幕上的图片,
//在渲染的时候都是在back buffer中进行,一旦渲染指令完成了,就交换back buffer和front buffer完成目标
glfwPollEvents();//接受用户的交互操作指令
}
glfwTerminate();//释放GLFW分配的内存
return 0;
}