OpenGL学习笔记(二):创建窗口,渲染流水线,VAO,VBO,EBO,shader


初始化OpenGL

我把glad.c单独放到了一个文件夹(SDK)下,CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)                                                                          
                                                                                                             
project(demo)                                                                                                 
                                                                                                             
aux_source_directory(/home/ykh/SDK GLAD)                                                                      
set(SOURCE_FILES main.cpp ${GLAD})                                                                            
                                                                                                               
add_executable(demo ${SOURCE_FILES})                                                                          
target_link_libraries(demo glfw3 GL m Xrandr Xi X11 Xxf86vm pthread dl Xinerama Xcursor)   

新建一个main.cpp:

#include<glad/glad.h>
#include<GLFW/glfw3.h>

int main()
{
    glfwInit();//初始化
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);//次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);//采用核心模式

    return 0;
}

这里使用OpenGL版本为3.3,核心模式
OpenGL有两种模式,另一种是立即模式,它比核心模式简单,但效率较低,并且不够灵活

创建窗口

#include<glad/glad.h>
#include<GLFW/glfw3.h>
#include<iostream>

using namespace std;

//窗口大小变动的回调函数
void framebuffer_size_callback(GLFWwindow* window,int width,int height)
{
    glViewport(0,0,width,height);
}

//如果ESC键按下,则关闭窗口
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

int main()
{
    glfwInit();//初始化
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3); //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);//次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);//采用核心模式
    
    //参数:窗口宽,窗口高,窗口标题,暂时忽略,暂时忽略
    GLFWwindow* window = glfwCreateWindow(800,600,"这是窗口标题",nullptr,nullptr);
    if(!window)
    {
        cout<<"创建窗口失败!"<<endl;
        glfwTerminate();//释放内存
        return -1;
    }
    glfwMakeContextCurrent(window);

    //初始化GLAD,GLAD用于管理函数指针
    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
        cout<<"初始化GLAD失败"<<endl;
        return -1;
    }
    
    //设置视口,OpenGL幕后的坐标转窗口坐标将以此为参考
    //OpenGL的坐标最大最小值为1和-1
    //如下x轴-1~1对应窗口坐标0~800,y轴-1~1对应窗口坐标0~600
    //如OpenGL坐标(0,0)对应窗口坐标(400,300),(-0.5,0.5)对应(200,450)
    glViewport(0,0,800,600);
    
    //注册窗口大小变化回调函数
    glfwSetFramebufferSizeCallback(window,framebuffer_size_callback);

    //窗口循环 如果窗口被关闭,则退出循环
    while(!glfwWindowShouldClose(window))
    {
        //监视键盘输入
        processInput(window);
        

        //清除颜色缓冲
        glClearColor(0.2f,0.3f,0.3f,0.8f);
        //GL_COLOR_BUFFER_BIT 颜色缓冲
        //GL_DEPTH_BUFFER_BIT 深度缓冲
        //GL_STENCIL+BUFFER_BIT 模板缓冲
        glClear(GL_COLOR_BUFFER_BIT);

        //交换颜色缓冲,OpenGL绘图的时候,前缓冲用于显示,后缓冲就在渲染
        //当后缓冲渲染完毕,前缓冲和后缓冲互换,后缓冲变前缓冲,前缓冲变后缓冲
        glfwSwapBuffers(window);
        //消息分发函数 检测有没有触发窗口消息,然后调用对应回调函数
        glfwPollEvents();
    }

    //释放内存
    glfwTerminate();

    return 0;
}

在这里插入图片描述

图形渲染管线

参考:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/

下图是顶点数据最终加工成图像的过程
在这里插入图片描述
着色器(shader)是在GPU上运行的小程序,GPU有成千上万个处理核心,能进行非常多的并行计算,在些计算的逻辑就写在着色器里,上图中蓝色部分就是表示我们可以通过写shader来进行控制的部分

顶点着色器先把3D坐标转换成另一种3D坐标,也就是OpenGL世界里的坐标
图元着色器会把顶点着色器传进来的坐标连接成一个图形
几何着色器可以进一步构造新的图元或其他形状
光栅化就是把图元映射成屏幕上对应的像素
片段着色器用来给物体上色
测试与混合如果物体对应Alpha值所表示的颜色带有一定的透明,它后面有不完全透明的物体,则要跟后面物体的颜色进行混合,以此达到透明效果,如果被前面物体挡住了,就丢弃被挡住部分的像素

整个流程就像工厂里的流水线一样,把原料(顶点数据)从一头放入,经过一道道工序的处理,最终变成产品,所以这个渲染流程也叫渲染流水线

顶点坐标输入

每个顶点坐标的xyz值为[-1,1],z代表深度值,及物体离屏幕多远,其坐标系大概是这样的
在这里插入图片描述
z值范围是[-1,1],但转换成深度值的时候就是[0,1],深度值越小,离屏幕越近

我们可以定义一组数据作为顶点数据放进流水线,这是下图三角形的顶点数据,z值都为0保证这些点都在同一个平面

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

在这里插入图片描述

VBO

当顶点数据进入流水线的时候,需要一个顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存
,它的数据类型是unsigned int类型,值是一个独一无二的ID,以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:

unsigned int VBO;
glGenBuffers(1, &VBO);

OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲(也就是说一个物体可以对应多个VBO),只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);  

从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

参数:

  1. 缓冲类型
  2. 大小
  3. 数据指针
  4. 参数4如下
  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变

这里画三角形的数据基本不会变,所以参数4是GL_STATIC_DRAW

VAO

VAO就是用来解释VBO里的数据是啥的,相当于一个模板,VBO本质就是一堆二进制数据,你可以把它解释成全是float,也可以把它前16个字节解释成unsigned int,后16个字节解释成float
创建一个VAO:

unsigned int VAO;
glGenVertexArrays(1, &VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
/*
参数1:指定要修改的顶点属性的索引值

参数2:指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a))

参数3:指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。

参数4:指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。

参数5:指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。

参数6:指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;

这里会把数据解释为:
struct P{
	float x;
	float y;
	float z;
}
struct VAO{
	P p1;
	P p2;
	P p3;
}
这样,
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};
就可以解释成3个坐标点,并存在0号位置,供shader读取
*/
glEnableVertexAttribArray(0);
/*
默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,
意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,
由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。
*/

当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute),也就是上文参数1。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

当要绘制的时候,还需要绑定VAO

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引,我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
参数1其他参数:
点:GL_POINTS,
线:GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES,
三角形:GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES,
四边形:GL_QUAD_STRIP, GL_QUADS, and GL_POLYGON.

EBO

举个例子,当我们要绘制一个矩形的时候,可以通过绘制两个三角形来形成一个矩形,那么它的顶点数据需要这样定义:

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,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

这样就导致了性能的浪费,有两个点都是重合的,明明只需要4个点,却要定义6个顶点数据
这时候就需要索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)了。
和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。
顶点数据和索引节点数据

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);

绘制的时候可以直接调用glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)来进行绘制
关于glDrawElements

着色器

着色器是使用一种叫GLSL的类C语言写成的。一个典型的着色器有下面的结构:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

数据类型

GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)
向量有5种,分别用来储存int(ivecn)、float(vecn)、double(dvecn)、uint(uvecn)和bool(bvecn),n代表1~4的数值,vec4就相当于:

struct vec4
{
	float r;
	float g;
	float b;
	float a;
}

向量可以重组,语法如下:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

初始化:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

输入输出

in out

in 表示从上一个流程输入进来的变量
out 表示输出变量到下一流程
例如在顶点着色器输出

out vec4 color;

在片段着色器想要得到:

in vec4 color;

变量名字和类型也要一样

顶点着色器的输入

顶点着色器是流水线第一个,不能直接通过in来进行输入顶点着色器的输入是靠

layout (location = 顶点属性的索引值) in 类型 变量名;

来获取的,例如:

layout (location = 0) in vec4 pos;

Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,它是全局的,可以被流水线上的任一阶段访问
在shader中声明:

uniform vec4 ourColor;

在CPU应用中访问:

int 顶点属性的索引值 = glGetUniformLocation(shader程序对象,"shader中变量名");
glUniform+后缀(顶点属性的索引值,值1,2...;

后缀有:
在这里插入图片描述
例如修改:uniform vec4 ourColor;

//vec4 有4个float值,分别为值1~4
glUniform4f(顶点属性的索引值,1,2,3,4);

编译着色器

1. shader对象ID = glCreateShader(着色器类型),着色器类型为 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER
2. glShaderSource(shader对象ID,源码字符串的数量,源码字符串,字符串数组的长度如果为空则代表整个字符串)
3. glCompileShader(shader对象ID)

例如:

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";
unsigned int vertexShader;

vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

编译检查是否有误:

int  success;
char infoLog[512];
glGetShaderiv(shader对象ID, GL_COMPILE_STATUS, &success);
if(!success)
{
    glGetShaderInfoLog(shader对象ID, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

链接着色器程序

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

//添加着色器对象
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//链接
glLinkProgram(shaderProgram);

//检查是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

//激活它
glUseProgram(shaderProgram);

//在把着色器对象链接到程序对象以后,记得删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吾无法无天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值