简介
文章根据Learnopengl的节奏分章节分享学习心得,后续会持续更新。
本章介绍使用opengl绘制三角形。
学习网站:主页 - LearnOpenGL CN
如果环境搭建大家还有问题请移步环境搭建教程。
环境搭建链接如下:WSL2+XLaunch配置opengl开发环境-CSDN博客
前置知识
先了解下渲染相关关键词含义
顶点着色器(Vertex Shader)把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理
基本图元装配(Primitive Assembly)把所有输入的顶点数据作为输入,输出制定的基本图元。
几何着色器(Geometry Shader)把基本图元形式的顶点的集合作为输入,可以通过产生新顶点构造出新的(或是其他的)基本图元来生成其他形状。
光栅化(Rasterization)即像素化,着色器输出的基本图形映射为屏幕上网格的像素点,生成供片段着色器处理的片段(Fragment),光栅化包含一个剪裁操作,会舍弃超出定义的视窗之外的像素。
片段着色器(Fragment Shader)的主要作用是计算出每一个像素点最终的颜色,通常片段着色器会包含3D场景的一些额外的数据,如光线,阴影等。
测试与混合是对每个像素点进行深度测试,Alpha测试等测试并进行颜色混合的操作,这些测试与混合操作决定了屏幕视窗上每个像素点最终的颜色以及透明度。
VAO (Vertex Array Object)顶点数组对象 :向GPU解释顶点数据 对VBO进行解释 一个VAO可以解释多种VBO数组 (比如顶点 纹理 颜色等等)
VBO(Vertex Buffer Object)是一个内存缓冲区,用来保存顶点信息,颜色信息,法线信息,纹理坐标,可以认为是各种点信息的集合,VBO会自动把数据送到gpu
EBO(Element Buffer Object) 是一个缓冲区,像一个顶点缓冲区对象,存储 OpenGL 用来决定要绘制哪些顶点的索引 可以认为EBO是从VBO中索引点去绘制图形 可以减少VBO重复点的定义
图形渲染管线的流程
老生常谈的一张图,你可以在各种教程中看到。
这里包含了从原始数据经过一系列的处理变换并最终输出到屏幕上的整个过程
获取顶点->拼接所有顶点形成基础图元->根据几何着色器把大的图元分割成目标形状->然后这个形状需要映射到屏幕像素上,需要光栅化的过程 原本笔直的线对于屏幕的像素点呈现就发生像素扩展,这里跟边缘锯齿化问题同理->经过片段着色器上色->颜色混合 这里是根据透明度 判断是否对某个像素是要替换原本像素 还是有透明度 进行混合算法
顶点输入
首先需要给OpenGL输入一些顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
这里的坐标都在(-1,1)之间,所有在这个范围内的坐标叫做标准化设备坐标,此范围内的坐标最终显示在屏幕上。
上图内容均会呈现到屏幕上,这里是画了一个三角形,定义了3个顶点的坐标。
这些点会存储到VBO中,后续送到GPU显存
生成VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
每个缓冲都有一个ID标识,这里VBO对应的缓冲ID就是1 你可以绑定多个VBO到不同的ID上
使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
GL_ARRAY_BUFFER是指顶点属性数组缓冲区,用于存储顶点属性数据
绑定后针对GL_ARRAY_BUFFER的操作都会作用到你之前绑定的VBO中。
然后你就可以针对当前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 :数据每次绘制时都会改变。
这些你可以认为是针对显卡对数据处理的一些策略,不同的方式 决定GPU读写速度等。
顶点着色器
顶点着色器是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。
介绍一个基础的顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
layout(location=0):
设定了输入变量的位置值,可以认为是外部程序传给着色器程序的有关顶点的属性值,外部可以通过针对顶点属性进行设置
例如:glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);第一个参数0 即location 0
in:在顶点着色器中声明所有的输入顶点属性
向量(Vector)
在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过
vec.x
、vec.y
、vec.z
和vec.w
来获取。注意vec.w
分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。
绑定顶点着色器到程序中
先把着色器转成C风格 char*方便后续使用
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);
glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL
。设置为NULL,则 glShaderSource() 函数将假定每个字符串都是以 null 结尾的。
片段着色器
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色!
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
输出变量可以使用out
关键字
vec4(1.0f, 0.5f, 0.2f, 1.0f);这里是RGBA的4个通道,前三个RGB通道数据进行归一化,可以自行乘255,这里代表橘黄色。
添加片段着色器 类似顶点着色器一样
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
创建一个程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
添加顶点着色器和片段着色器并链接
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
激活程序对象
glUseProgram(shaderProgram);
链接顶点属性
到这里其实各种着色器都定义好了 但是没有设置顶点属性 这样在VBO中读取顶点数据的时候 会出现问题,这里需要设置VBO的顶点属性给到着色器使用
float vertex = {
//顶点 //颜色 //纹理
0.3f, 0.3f, 0.0f, 0.1f, 0.5f, 0.8f, 0.0f, 1.0f,
0.1f, -0.3f, 0.0f, 0.4f, 0.0f, 0.3f, 1.0f, 0.0f,
-0.3f, -0.6f, 0.0f, 0.1f, 0.0f, 0.5f, 0.0f, 3.0f,
-0.5f, 0.6f, 0.0f, 0.2f, 1.0f, 0.0f, 0.0f, 1.0f
};
你可能会遇到上述顶点定义,此时如果不添加顶点属性解释,GPU是无法读取到对应的顶点数据的,下面介绍如果使用顶点属性配置
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
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。
顶点数组对象(VAO)
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
属性绑定不需要重复进行,可以记录VAO中提高gpu渲染效率
VAO创建
unsigned int VAO;
glGenVertexArrays(1, &VAO);
这里初始化的动作均介绍完毕,在渲染的循环中添加下面的代码
用于最终出图
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays 是 OpenGL 中用于绘制几何图形的函数。它使用顶点数组来指定要绘制的顶点。
第一个参数是我们打算绘制的OpenGL图元的类型
第二个参数指定了顶点数组的起始索引
最后一个参数指定我们打算绘制多少个顶点
glBindVertexArray(VAO);这里是绑定VAO 但是因为顶点属性没有变化 这里可以不用频繁绑定
glUseProgram(shaderProgram);绑定着色器
到这里 就可以编译试试能不能绘制出三角形把
我们可以给三角形设置一个颜色 比如红色:
FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);
这里准备工作已经全部完成 去生成属于你自己的三角形吧
后续会逐步更新 一起学习。