OpenGL Buffers

一、VBO
VBO原理

使用现代opengl绘制一个三角形,有两个东西是必须提供给GPU的,一个是vertexBuffer(VBO), 这是一块存储空间,用于存储顶点数据,但是该空间是位于GPU中的,即显存;第二个是着色器程序,即告诉GPU处理顶点的程序。通过C++代码由CPU向GPU发起绘制指令的过程称为一次drawCall,告诉gpu从vbo中获取顶点数据,调用shader程序进行绘制。

这里关于buffer需要阐述下。我们先来搞清楚VAO,VBO缓存到底做的是什么工作?

首先是VBO(vertex buffer object),为什么我们要用VBO?
如果不使用VBO,我们每次绘制(glDrawArrays)图形时都是从本地内存处获取顶点数据然后传输给OpenGL来绘制,这样就会频繁的操作CPU->GPU增大开销,从而降低效率。
使用VBO,我们就能把顶点数据缓存到GPU开辟的一段内存中,然后使用时不必再从本地获取,而是直接从显存中获取,这样就能提升绘制的效率。
以之前传统opengl绘制的代码为例子,我们在传递顶点位置数据的时候,在OpenGL旧版本里,是通过glVertex逐个从CPU传递到GPU的,代码示例如下:

glBegin(GL_TRIANGLES);
    glVertex(0.0f, 0.0f);
    glVertex(1.0f, 0.0f);
    glVertex(0.0f, 1.0f);
glEnd();

这样每进行一次glVertex调用就会向GPU传递一次顶点数据,gpu在显存并没有集中进行存储,因此gpu读取顶点数据绘制时也是一个一个的读取,由于传输是同步的,所以效率很低。

为了解决这个问题,传统opengl引入了 Display List(显示列表)来解决这个问题。
显示列表是预先存储(编译)好的等待执行的一组opengl指令。一旦显示列表被创建,顶点数据就会被拷贝到opengl server端的显示列表显存中,这个过程只会执行一次。显示列表被编译后,你可以每帧渲染时重复使用而无需重新传递数据。显示列表是绘制static data的非常快的方法,因为其中的顶点数据和opengl指令都被缓存在显存中了,其减少了cpu到gpu的传输过程 。
由于显示列表被存储在server端,因此其还可以被多个client复用 。为了最大化性能,可以将矩阵变化,光照和材质计算放在显示列表中,这样opengl只会在显示列表创建的时候执行这些处理这些计算一次。

GLuint listName = glGenLists (1);
glNewList (listName, GL_COMPILE);
    glBegin (GL_TRIANGLES);
        glVertex2f (0.0, 0.0);
        glVertex2f (1.0, 0.0);
        glVertex2f (0.0, 1.0);
    glEnd ();
glEndList ();
...
// 绘制(不传输数据)
glCallList(listName);

显示列表的缺点是编译后就不可修改,如果你需要频繁修改顶点数据, 那么就需要CPU重新生成新的顶点再发送到GPU进行处理。

为了解决这个问题,opengl引入了 Vertex Array( glDrawArrays(), glDrawElements()等函数的原理)。Vertex Array,顶点数据要区别于我们开头提到的VAO,它跟缓存是没有关系的,它也是一种传输方案。VA也是通过收集顶点的方式来减少传输次数,但与显示列表不同的是,CPU端将会负责收集所有顶点,收集完成后一次性传输到GPU再进行绘制。因此,cpu到gpu是以一个整体进行顶点数据的传入的,opengl绘制时,从显存也是读取的一块整体。 这样处理后,每次进行绘制时,都会进行一次传输,虽然解决了数据频繁变化的问题,但是每帧必要的一次传输过程也影响了性能。

现代opengl使用VBO兼顾 Vertex Array和 显示列表的特点。VBO是显存开辟的存储顶点数据的一块区域,如果开辟的显存是用来存储像素数据,则称为PBO。
VBO一方面在开辟了高效的显存区域存储顶点数据,同时提供了映射该区域到client端内存区域以更新顶点数据的函数。VBO也是在server端的,因此可以被很多client共享。

可以这样创建VBO:

unsigned int vbo;
glGenBuffers(1, &vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo); //绑定,记住opengl是一个状态机
顶点属性和VBO布局

我们通过vbo实现了顶点数据buffer从cpu到gpu的传递,并在gpu显存中进行了缓存,当gpu开始绘制时,会从该buffer中读取数据。
我们传递的顶点数据实际上不单单包括位置信息,还有颜色,法线等信息,而buffer其实就是一块字节数组,我们怎么告诉gpu怎么解析这块字节呢?这是本节要讲的内容。
数据的本质就是字节流,通过对字节流的解析才能获取原来的内容。比如,cpp定义了一个int*的指针,因此该指针指向一个4字节的内存区域,也指明了会将这4个字节解析成int,如果我们把这个指针强转成一个4字节的对象指针,那就是告诉cpu按照这个对象进行字节流的解析,可能就会有完全不同的解析结果。因此,字节流具体是什么内容,完全是根据解析规则。
vbo中的数据就是一整块字节,如上节我们传递了6个浮点数,并且每2个为一组表示位置,这就是解析规则,我们需要告诉gpu如何解析。
Opengl提供了以下函数让我们解析vbo:

glEnableVertexAttribArray(0); // 激活0号插槽位
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
  • 0表示位于shader中的0号插槽位,分配给位置属性。shader中的属性插槽位是有个数限制的,通常情况下为16,
    通过glVertexAttribPointer可以让gpu知道,0号位是位置属性,且定义了从vbo中的解析规则;也可以通过glGetAttribLocation(program,
    “position”)获取位置的插槽位替代直接指定0;
  • 2表示2个一组进行解析成位置
  • GL_FLOAT参数表示vbo中的数据类型
  • GL_FALSE表示是否归一化
  • 2 * sizeof(GLfloat)表示一个顶点数据占的所有字节,这里只有位置,如果包含了颜色等信息,要累加起来
  • (GLvoid*)0表示一个顶点数据中位置信息距离起始字节的偏移量

通过这个函数,opengl就知道如何解析vbo了,其中的第一个参数和shader有关系。

参考文件:
http://www.songho.ca/opengl/index.html
https://docs.gl/ (查看所有opengl函数说明)

二、Uniform 变量

两个问题:

1、如果在着色器中要传递多个uniform变量时,总是使用多个uniform,然后在CPU程序中设置这些变量的值;如果uniform变量数量太多,怎样组织更合理?
2、如果要在多个shader之间共享Uniform变量,例如投影矩阵projection和视变换矩阵view的话,仍然需要为不同shader分别设置这些uniform变量,怎样实现shader之间共享?

Interface Block

Interfac block是一组GLSL着色器里面的输入、输出、uniform等变量的集合,有一些类似于C语言中的struct。通过使用interface block,我们可以将着色器中的变量以组的形式来管理。
interface block的声明形式为:

storage_qualifier block_name
{
  <define members here>
} instance_name;

其中storage_qualifier指明这个block的存储限定符,限定符可以使用in​, out​, uniform​, 或者buffer​(GLSL4.3支持)等,block_name则给定名称,而instance_name给定实例名称。

例如,在实现点光源的过程中,顶点着色器和片元着色器之间需要传递法向量、纹理坐标等变量,可以将他们封装到一个block中。
顶点着色器中输出变量定义形式如下:

// 定义输出interface block
out VS_OUT
{
    vec3 FragPos;
    vec2 TextCoord;
    vec3 FragNormal;
}vs_out;

而在片元着色器中,要以相同的block_name接受,实例名称则可以不同,形式可以定义为:

// 定义输入interface block
in VS_OUT
{
    vec3 FragPos;
    vec2 TextCoord;
    vec3 FragNormal;
}fs_in;

如果指定了instance_name,则在片元着色器中引用这些变量时需要加上instance_name前缀,例如:

// 环境光成分
vec3  ambient = light.ambient * vec3(texture(material.diffuseMap, fs_in.TextCoord));

反之如果没有指定instance_name,则这个block中的变量将和uniform一样是全局的,可以直接使用。如果没有给定instance_name,则需要注意,interface block中给定的变量名不要和uniform给定的重复,否则造成重定义错误,例如下面的定义将造成重定义错误:

uniform MatrixBlock
{
  mat4 projection;
  mat4 modelview;
};

uniform vec3 modelview;  // 重定义错误 和MatrixBlock中冲突

相比于之前以分散形式书写这些变量,interface block能够让你更合理的组织变量为一组,逻辑更清晰。

UBO

uniform buffer的实现思路为: 在多个着色器中定义相同的uniform block(就是上面的interface block,使用uniform限定符定义),然后将这些uniform block绑定到对应的uniform buffer object,而uniform buffer object中实际存储这些需要共享的变量。着色器中的uniform block和主程序中的uniform buffer object,是通过OpenGL的绑定点(binding points)连接起来的:
在这里插入图片描述
使用时,每个shader中定义的uniform block有一个索引,通过这个索引连接到OpenGL的绑定点x;而主程序中创建uniform buffer object,传递数据后,也将这个UBO绑定到对应的x,此后shader中的uniform block就和OpenGL中的UBO联系起来,我们在程序中操作UBO的数据,就能够在不同着色器之间共享了。例如上图中,着色器A和B定义的Matrices的索引都指向绑定点0,他们共享openGL的uboMatrices这个UBO的数据。同时着色器A的Lights和着色器B的Data,分别指向不同的UBO。

UBO的使用
UBO的实现依赖于着色器中uniform block的定义,uniform block的内存布局四种形式:shared​, packed​, std140​, and std430​(GLSL4.3以上支持),默认是shared内存布局。我们重点关注shared和std140这两种内存布局形式。

  • shared 默认的内存布局
    采用依赖于具体实现的优化方案,但是保证在不同程序中具有相同定义的block拥有相同的布局,因此可以在不同程序之间共享。要使block能够共享必须注意block具有相同定义,同时所有成员显式指定数组的大小。同时shared保证所有成员都是激活状态,没有变量被优化掉。
  • std140
    这种方式明确的指定alignment的大小,会在block中添加额外的字节来保证字节对齐,因而可以提前就计算出布局中每个变量的位移偏量,并且能够在shader之间共享;不足在于添加了额外的padding字节。

下面通过两个简单例子,来熟悉std140和默认的shared内存布局。这个例子将会在屏幕上通过4个着色器绘制4个不同颜色的立方体,在着色器之间共享的是投影矩阵和视变换矩阵。

layout std140

字节对齐的概念
字节对齐的一个经典案例就是C语言中的结构体变量,例如下面的结构体:

struct StructExample {
    char c;  
    int i;  
    short s; 
}; 

在Windows平台测试,当int占用4个字节,short占用2个字节是,实际占用大小为12个字节,这12个字节是怎么算出来的呢? 就是用到了字节补齐的概念。实际上上述结构体的内存布局为:

struct StructExample {
  char c;  // 0 bytes offset, 3 bytes padding
  int i;   // 4 bytes offset
  short s; // 8 bytes offset, 2 bytes padding
}; // End of 12 bytes

字节对齐的一个重要原因是为了使机器访问更迅速。例如在32字长的地址的机器中,每次读取4个字节数据,所以将字节对齐到上述地址 0x0000,0x0004和0x0008, 0x000C将使读取更加迅速。否则例如上面结构体中的int i将跨越两个字长(0x0000和0x0004),需要两次读取操作,影响效率。
关于字节对齐,我们需要知道的几个要点就是:

  1. 一个内存地址,当它是n字节的倍数时,称之为n字节对齐,这里n字节是2的整数幂。
  2. 每种数据类型都有它自己的字节对齐要求(alignment),例如char是1字节,int一般为4字节,float为4字节对齐,8字节的long则是8字节对齐。
  3. 当变量的字节没有对齐时,将额外填充字节(padding)来使之对齐。

上面的结构体中,int变量i需要4字节对齐,因此在char后面填充了3个字节,同时结构体变量整体大小需要满足最长alignment成员的字节对齐,因此在short后面补充了2个字节,总计达到12字节。

std140的字节对齐
std140内存布局同样存在字节对齐的概念,常用标量int,float,bool要求4字节对齐,4字节也被作为一个基础值N,这里列举几个常用的结构的字节对齐要求:
在这里插入图片描述
例如一个复杂的uniform block定义为:

layout (std140) uniform ExampleBlock
{
    //               // base alignment  // aligned offset
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (must be multiple of 16 so 4->16)
    mat4 matrix;     // 16              // 32  (column 0) (each line 16)
                     // 16              // 48  (column 1)
                     // 16              // 64  (column 2)
                     // 16              // 80  (column 3)
    float values[3]; // 16              // 96  (values[0]) (each element 16)
                     // 16              // 112 (values[1])
                     // 16              // 128 (values[2])
    bool boolean;    // 4               // 144
    int integer;     // 4               // 148
}; 

上面的注释给出了它的字节对齐,其中填充了不少字节,可以根据上面表中给定的对齐基数提前计算出来,在主程序中可以设置这个UBO的变量:

   GLuint UBOId;
   glGenBuffers(1, &UBOId);
   glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
   glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_DYNAMIC_DRAW); // 预分配空间 大小可以提前根据alignment计算
   glBindBuffer(GL_UNIFORM_BUFFER, 0);
   glBindBufferBase(GL_UNIFORM_BUFFER, 1, UBOId); // 设置绑定点为1
   // step4 只更新一部分值
   glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
   GLint b = true; // 布尔变量在GLSL中用4字节表示 因此这里用int存储
   glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); // offset可以根据UBO中alignment提前计算
   glBindBuffer(GL_UNIFORM_BUFFER, 0);

说明: 上面最终计算出的大小为152,UBO整体不必满足vec4的字节对齐要求。152 /4 = 38,满足N的对齐要求即可。
从上面可以看到,当成员变量较多时,这种手动计算offset的方法比较笨拙,可以事先编写一个自动计算的函数库,以减轻工作负担。

std140的简单例子
下面通过一个简单例子来熟悉UBO的使用。

Step1: 首先我们在顶点着色器中定义uniform block如下:

#version 330 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;


uniform mat4 model; // 因为模型变换矩阵一般不能共享 所以单独列出来

// 定义UBO
layout (std140) uniform Matrices
{
   mat4 projection;
   mat4 view;
};  // 这里没有定义instance name,则在使用时不需要指定instance name


void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
}

Step2 在主程序中设置着色器的uniform block索引指向到绑定点0:

   // step1 获取shader中 uniform buffer 的索引
    GLuint redShaderIndex = glGetUniformBlockIndex(redShader.programId, "Matrices");
    GLuint greeShaderIndex = glGetUniformBlockIndex(greenShader.programId, "Matrices");
    ...
    // step2 设置shader中 uniform buffer 的索引到指定绑定点
    glUniformBlockBinding(redShader.programId, redShaderIndex, 0); // 绑定点为0
    glUniformBlockBinding(greenShader.programId, greeShaderIndex, 0);
    ...

Step3: 创建UBO,并绑定到绑定点0
我们需要传入2个mat4矩阵,由于mat4中每列的vec4对齐,因此两个mat4中没有额外的padding,大小即为2*sizeof(mat4):

   GLuint UBOId;
   glGenBuffers(1, &UBOId);
   glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
   glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_DYNAMIC_DRAW); // 预分配空间
   glBindBuffer(GL_UNIFORM_BUFFER, 0);
   glBindBufferRange(GL_UNIFORM_BUFFER, 0, UBOId, 0, 2 * sizeof(glm::mat4)); // 绑定点为0

Step4: 更新UBO中的数据
这里使用前面介绍的glBufferSubData更新UBO中数据,例如更新视变换矩阵如下:

glm::mat4 view = camera.getViewMatrix(); // 视变换矩阵
glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);

通过上面的步骤,我们完成了着色器中unifrom block和UBO的连接,实现了投影矩阵和视变换矩阵在4个着色器之间的共享。

layout shared

同std140内存布局方式不一样,shared方式的内存布局依赖于具体实现,因此我们无法提前根据某种字节对齐规范计算出UBO中变量的位移偏量和整体大小,因此在使用shared方式时,我们需要多次利用OpenGL的函数来查询UBO的信息。

Step1: 在着色器中定义一个用于混合颜色的uniform block:

#version 330 core
// 使用默认shared​方式的UBO
uniform mixColorSettings {
    vec4  anotherColor;
    float mixValue;
};
out vec4 color;
void main()
{
    color = mix(vec4(0.0, 0.0, 1.0, 1.0), anotherColor, mixValue);
}

Step2: 在出程序中首先查询UBO整体大小,预分配空间:

GLuint UBOId;
glGenBuffers(1, &UBOId);
glBindBuffer(GL_UNIFORM_BUFFER, UBOId);
// 获取UBO大小 因为定义相同 只需要在一个shader中获取大小即可
GLint blockSize;
glGetActiveUniformBlockiv(redShader.programId, redShaderIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
glBufferData(GL_UNIFORM_BUFFER, blockSize, NULL, GL_DYNAMIC_DRAW); // 预分配空间
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferBase(GL_UNIFORM_BUFFER, 1, UBOId); // 绑定点为1

Step3: 通过查询UBO中成员变量的索引和位移偏量来设置变量值:

// 通过查询获取uniform buffer中各个变量的索引和位移偏量
const GLchar* names[] = {
    "anotherColor", "mixValue"
};
GLuint indices[2];
glGetUniformIndices(redShader.programId, 2, names, indices);
GLint offset[2];
glGetActiveUniformsiv(redShader.programId, 2, indices, GL_UNIFORM_OFFSET, offset);
// 使用获取的位移偏量更新数据
glm::vec4 anotherColor = glm::vec4(0.0f, 1.0f, 1.0f, 1.0f);
GLfloat mixValue = 0.5f;
glBindBuffer(GL_UNIFORM_BUFFER, colorUBOId);
glBufferSubData(GL_UNIFORM_BUFFER, offset[0], sizeof(glm::vec4), glm::value_ptr(anotherColor));
glBufferSubData(GL_UNIFORM_BUFFER, offset[1], sizeof(glm::vec4), &mixValue);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

使用shared布局时,当变量较多时,这种查询成员变量索引和位移偏量的工作显得比较麻烦。

三、FBO
四、PBO

OpenGL Pixel Buffer Object (PBO)
OpenGL 使用 PBO 高速复制屏幕图像到内存或者纹理中

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值