写在前面
目前,咱们在着色器中要传递多个uniform变量时,老是使用多个uniform,而后在主程序中设置这些变量的值;同时若是要在多个shader之间共享变量,例如投影矩阵projection和视变换矩阵view的话,仍然须要为不一样shader分别设置这些uniform变量。本节将为你们介绍interface block,以及基于此的uniform buffer object(UBO),这些技术将简化着色器中变量的传递和共享问题。本节示例程序都可以从个人github下载。html
本节内容参考自:
1.www.learningopengl.com Advanced GLSL
2.GLSL Tutorial – Uniform Blocks
3.《OpenGL 4.0 Shading Language Cookbook》-Using Uniform Blocks and Uniform Buffer Objectsgit
interfac block是一组GLSL着色器里面的输入、输出、uniform等变量的集合,有一些相似于C语言中的struct,可是不像struct那样简单明了,还有一些其余的选项包含在里面。经过使用interface block,咱们能够将着色器中的变量以组的形式来管理,这样书写更整洁。github
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可以让你更合理的组织变量为一组,逻辑更清晰。ui
主程序部分未变,实现的点光源效果相同,这里仍是给出效果图以下:
从上面能够看出,interface block确实解决了咱们一直想要合理组织着色器中变量的问题。这是咱们提到的第一个问题。
本节开始提到的第二个问题是,如何在多个着色器之间简洁的共享变量。GLSL中能够经过uniform buffer来实现。uniform buffer的实现思路为: 在多个着色器中定义相同的uniform block(就是上面的interface block,使用uniform限定符定义),而后将这些uniform block绑定到对应的uniform buffer object,而uniform buffer object中实际存储这些须要共享的变量。着色器中的uniform block和主程序中的uniform buffer object,是经过OpenGL的绑定点(binding points)链接起来的,它们的关系以下图所示(来自www.learningopengl.com Advanced GLSL):
使用时,每一个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的实际使用。UBO的实现依赖于着色器中uniform block的定义,uniform block的内存布局四种形式:shared, packed, std140, and std430(GLSL4.3以上支持),默认是shared内存布局。本节咱们重点学习shared和std140这两种内存布局形式,其余的形式能够在须要时自行参考OpenGL规范。
shared 默认的内存布局 采用依赖于具体实现的优化方案,可是保证在不一样程序中具备相同定义的block拥有相同的布局,所以能够在不一样程序之间共享。要使block可以共享必须注意block具备相同定义,同时全部成员显式指定数组的大小。同时shared保证全部成员都是激活状态,没有变量被优化掉。
std140 这种方式明确的指定alignment的大小,会在block中添加额外的字节来保证字节对齐,于是能够提早就计算出布局中每一个变量的位移偏量,而且可以在shader之间共享;不足在于添加了额外的padding字节。稍后会介绍字节对齐和padding相关内容。
下面经过两个简单例子,来熟悉std140和默认的shared内存布局。这个例子将会在屏幕上经过4个着色器绘制4个不一样颜色的立方体,在着色器之间共享的是投影矩阵和视变换矩阵,以及为了演示shared layout而添加的混合颜色的示例。
字节对齐的一个经典案例就是C语言中的结构体变量,例以下面的结构体:
struct StructExample { char c; int i; short s; };
你估计它占用内存大小多少字节? 假设在int 占用4字节,short占用2个字节,那么总体大小等于 1+ 4+ 2 = 7字节吗?
答案是否认的。在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),须要两次读取操做,影响效率。固然关于为何使用字节对齐的更详细分析,感兴趣地能够参考SO Purpose of memory alignment。
关于字节对齐,咱们须要知道的几个要点就是(参考自wiki Data structure alignment):
一个内存地址,当它是n字节的倍数时,称之为n字节对齐,这里n字节是2的整数幂。
每种数据类型都有它本身的字节对齐要求(alignment),例如char是1字节,int通常为4字节,float为4字节对齐,8字节的long则是8字节对齐。
当变量的字节没有对齐时,将额外填充字节(padding)来使之对齐。
上面的结构体中,int变量i须要4字节对齐,所以在char后面填充了3个字节,同时结构体变量总体大小须要知足最长alignment成员的字节对齐,所以在short后面补充了2个字节,总计达到12字节。
关于字节对齐这个概念,介绍到这里,但愿了解更多地能够参考The Lost Art of C Structure Packing。
std140内存布局一样存在字节对齐的概念,你能够参考官方文档获取完整描述。经常使用标量int,float,bool要求4字节对齐,4字节也被做为一个基础值N,这里列举几个经常使用的结构的字节对齐要求:
类型 | 对齐基数(base alignment) |
---|---|
标量,例如 int bool | 每一个标量对齐基数为N |
vector | 2N 或者 4N, vec3的基数为4N. |
标量或者vector的数组 | 每一个元素的基数等于vec4的基数. |
矩阵 | 以列向量存储, 列向量基数等于vec4的基数. |
结构体 | 元素按以前规则,同时总体大小填充为vec4的对齐基数 |
例如一个复杂的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) // 16 // 48 (column 1) // 16 // 64 (column 2) // 16 // 80 (column 3) float values[3]; // 16 // 96 (values[0]) // 16 // 112 (values[1]) // 16 // 128 (values[2]) bool boolean; // 4 // 144 int integer; // 4 // 148 };
上面的注释给出了它的字节对齐,其中填充了很多字节,能够根据上面表中给定的对齐基数提早计算出来,在主程序中能够设置这个UBO的变量:
GLuint exampleUBOId;
glGenBuffers(1, &exampleUBOId); glBindBuffer(GL_UNIFORM_BUFFER, exampleUBOId); glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_DYNAMIC_DRAW); // 预分配空间 大小能够提早根据alignment计算 glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBufferBase(GL_UNIFORM_BUFFER, 1, exampleUBOId); // 绑定点为1 // step4 只更新一部分值 glBindBuffer(GL_UNIFORM_BUFFER, exampleUBOId); 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的方法比较笨拙,能够事先编写一个自动计算的函数库,以减轻工做负担。
下面经过一个简单例子来熟悉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); ...
这里为了演示代码中重复写出了4个着色器,实际中能够经过vector装入这4个着色器简化代码。
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个着色器之间的共享,绘制4个立方体以下图所示:
这里在着色器中添加一段代码测试下上面那个复杂的ExampleBlock的内容,咱们在主程序中设置boolean变量为true,在着色器中添加一个判断,若是boolean为true,则输出白色立方体:
if(boolean) { color = vec4(1.0, 1.0, 1.0, 1.0); }
最终显示得到了4个全是白色的立方体,效果以下:
这就验证了上述计算出那个复杂ExampleBlock的大小为152,boolean变量位移偏量为144是正确的。
同std140内存布局方式不同,shared方式的内存布局依赖于具体实现,所以咱们没法提早根据某种字节对齐规范计算出UBO中变量的位移偏量和总体大小,所以在使用shared方式时,咱们须要屡次利用OpenGL的函数来查询UBO的信息。
这里在着色器中定义一个用于混合颜色的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); }
在出程序中首先查询UBO总体大小,预分配空间:
GLuint colorUBOId;
glGenBuffers(1, &colorUBOId); glBindBuffer(GL_UNIFORM_BUFFER, colorUBOId); // 获取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, colorUBOId); // 绑定点为1
而后,经过查询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);
和上面std140定义的uniform block一块儿工做,产生的混合颜色效果以下图所示:
从上面能够看到,使用shared布局时,当变量较多时,这种查询成员变量索引和位移偏量的工做显得比较麻烦。
本节学习了interface block概念,以及UBO的两种内存布局方式。限于本节内容较多,部分函数的具体使用未在此展开介绍,须要的能够自行参考OpenGL文档。同时本文中关于那个复杂的std140布局的UBO的offset的计算方法,以及使用shared方式时经过查询获取UBO总体大小、索引和偏移量的方法,须要尽可能掌握。