OpenGL学习笔记(九)-高级数据-高级GLSL

参考网址:LearnOpenGL 中文版

第四章 高级OpenGL

4.6 数据与缓冲

在OpenGL中使用缓冲来储存数据,本节将讨论不同的缓冲函数,以及如何使用纹理对象来储存大量的数据。缓冲是用来管理特定内存块的对象,将它绑定到一个缓冲目标后,才赋予了其意义。例如,将缓冲绑定到GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFER等缓冲目标上,OpenGL根据目标的不同,以不同的方式处理缓冲。

4.6.1 缓冲函数

1、之前一直调用glBufferData函数来填充缓冲对象所管理的内存,这个函数会分配一块内存,并将数据添加到这块内存中。如果data参数设置为NULL,将只会分配内存,但不进行填充。

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

2、可使用glBufferSubData函数填充缓冲的特定区域。参数为:缓冲目标、偏移量、数据的大小和数据本身。偏移量指定从何处开始填充这个缓冲,因此可以插入或者更新缓冲内存的某一部分,但缓冲需要有足够的已分配内存,所以对一个缓冲调用glBufferSubData之前必须要先调用glBufferData

glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)]

3、将数据导入缓冲的另外一种方法是:通过调用glMapBuffer函数,返回当前绑定缓冲的内存指针,直接将数据复制到缓冲当中。该函数用于直接映射数据到缓冲,而不事先将其存储到临时内存中,比如从文件中读取数据,并直接将它们复制到缓冲内存中。

float data[] = {
  0.5f, 1.0f, -0.35f
  ...
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 获取指针
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 复制数据到内存
memcpy(ptr, data, sizeof(data));
//不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);
4.6.2 分批顶点属性

1、之前,通过glVertexAttribPointer函数指定顶点数组缓冲内容的属性布局。在顶点数组缓冲中,对属性进行了交错处理,将每一个顶点的位置、法线和纹理坐标紧密放置在一起。

2、另一种方式是:将每一种属性类型的向量数据打包为一个大的区块。与交错布局123123123123不同,采用分批的方式111122223333

3、从文件中加载顶点数据时,通常获取到的是位置数组、法线数组和纹理坐标数组。使用glBufferSubData函数进行缓冲填充:

float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);

更新顶点属性指针,stride参数等于顶点属性的大小

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);  
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));  
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));
4.6.3 复制缓冲

1、glCopyBufferSubData能够从一个缓冲中复制数据到另一个缓冲中,readtargetwritetarget参数需为复制源和复制目标的缓冲目标。如将VERTEX_ARRAY_BUFFER缓冲复制到VERTEX_ELEMENT_ARRAY_BUFFER缓冲,分别将这些缓冲目标设置为读和写的目标。

void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
                         GLintptr writeoffset, GLsizeiptr size);

2、如果想读写数据的两个不同缓冲都为顶点数组缓冲,就不能同时将两个缓冲绑定到同一个缓冲目标上。因此,使用另外两个缓冲目标,GL_COPY_READ_BUFFERGL_COPY_WRITE_BUFFER,可以将需要的缓冲绑定到这两个缓冲目标上,作为readtargetwritetarget参数。glCopyBufferSubData会从readtarget中读取size大小的数据,并将其写入writetarget缓冲的writeoffset偏移量处。

float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

4.7 高级GLSL

4.7.1 顶点着色器变量

1、GLSL定义了以gl_为前缀的内建变量,如:顶点着色器的输出向量gl_Position,它是顶点着色器的裁剪空间输出位置向量。

2、将图元设置为GL_POINTS时,每一个顶点都会被渲染为一个点,可以通过glPointSize函数来设置渲染出来的点的大小。也可以通过gl_PointSize设置点的宽高(像素),能对每个顶点设置不同的值。

  • 启用顶点着色器中修改点大小的功能:
glEnable(GL_PROGRAM_POINT_SIZE);
  • 将点的大小设置为裁剪空间位置的z值,也就是顶点距观察者的距离。点的大小会随着观察者距顶点距离变远而增大。
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    gl_PointSize = gl_Position.z;    
}

在这里插入图片描述
3、gl_VertexID为只读的输入变量,储存了正在绘制顶点的当前ID。当进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。

4.7.2 片段着色器变量

1、gl_FragCoord的x和y分量是片段的窗口空间坐标,其原点为窗口的左下角,z分量等于对应片段的深度值。可以根据片段的窗口坐标,计算出不同的颜色。gl_FragCoord的常用于对比不同片段计算的视觉输出效果,如将屏幕分成两部分,在窗口的左侧渲染一种输出,在窗口的右侧渲染另一种输出。

void main()
{             
    if(gl_FragCoord.x < 400)
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    else
        FragColor = vec4(0.0, 1.0, 0.0, 1.0);        
}

在这里插入图片描述
2、gl_FrontFacing是一个bool变量,如果当前片段是正向面就返回true,否则就是false。如果不使用面剔除,能够对正反面使用不同的颜色(纹理)。

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D frontTexture;
uniform sampler2D backTexture;

void main()
{             
    if(gl_FrontFacing)
        FragColor = texture(frontTexture, TexCoords);
    else
        FragColor = texture(backTexture, TexCoords);
}

在这里插入图片描述
3、gl_FragCoord能获取片段的深度值,但不能修改。而输出变量gl_FragDepth可用来设置片段的深度值,直接写入一个0.0到1.0之间的float值。如果着色器没有写入值到gl_FragDepth,它会自动取用gl_FragCoord.z的值。

  • 在片段着色器中对gl_FragDepth进行写入,OpenGL就会禁用所有的提前深度测试,因为OpenGL无法在片段着色器运行之前确定片段的深度值。

  • 可以在片段着色器的顶部使用深度条件重新声明gl_FragDepth变量,可以对两者进行一定的调和。如将深度条件设置为greater,假设只会写入比当前片段深度值更大的值,当深度值比片段的深度值要小的时候,仍是能够进行提前深度测试的。

    layout (depth_<condition>) out float gl_FragDepth;
    
condition描述
any提前深度测试是禁用的,你会损失很多性能
greater只能让深度值比gl_FragCoord.z更大
less只能让深度值比gl_FragCoord.z更小
unchanged只能写入gl_FragCoord.z的值
#version 420 core // 注意GLSL的版本!
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

void main()
{             
    FragColor = vec4(1.0);
    gl_FragDepth = gl_FragCoord.z + 0.1;
}  
4.7.3 接口块

1、当需要从顶点着色器向片段着色器发送数组或结构体时,可以通过GLSL里的接口块组合这些变量。接口块的声明和结构体的声明类似,使用in或out关键字来定义输入输出。

2、声明了一个块名为VS_OUT,实例名为vs_out的接口块,将着色器的输入打包为数组。

#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec2 aTexCooords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out VS_OUT
{
	vec2 TexCoords;
}vs_out;

void main()
{
	gl_postion=projection*view*model*vec4(aPos,1.0);
	vs_out.TexCoords=aTexCoords;
}

3、在片段着色器中定义一个输入接口块,块名和顶点着色器中一样(VS_OUT),实例名是随意的。

#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
}
4.7.4 Uniform缓冲对象

1、Uniform缓冲对象是可以在多个着色器中使用的全局Uniform变量,使用glGenBuffers来创建,绑定到GL_UNIFORM_BUFFER缓冲目标,并将所有相关的Uniform数据存入缓冲。Uniform缓冲对象只需创建一次,但每个着色器中不同的uniform需要手动设置。

2、在每个渲染迭代中,对每个着色器设置projectionview 矩阵,现在只需要存储这些矩阵一次。首先,声明了一个叫做MatricesUniform块,将projectionview矩阵存储到Uniform块中,块中的变量可以直接访问。然后,在OpenGL中将这些矩阵值存入缓冲中,每个声明了这个Uniform块的着色器都能够访问这些矩阵。

#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};

uniform mat4 model;

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

4、layout (std140)设置了Uniform块布局,对它的内容使用一个特定的内存布局。Uniform块的内容是储存在一个缓冲对象中的,缓冲对象只是一块预留内存,并不会保存数据的类型,因此需要确定内存的哪一部分对应着着色器中的哪一个uniform变量。

layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};

5、需要确定每个变量的大小和偏移量,才能够按顺序将它们放进缓冲中。变量大小对应C++数据类型,其中向量和矩阵都是大的float数组。变量偏移量可以通过std140布局确定,std140布局声明了偏移量的计算规则。

  • 每个变量的基准对齐量等于变量在Uniform块中所占据的空间(包括填充量),可以使用std140布局的规则计算;
  • 每个变量的对齐偏移量等于变量从块起始位置的字节偏移量。对齐字节偏移量必须等于基准对齐量的倍数。
  • GLSL中的每个变量,比如说int、float和bool,都被定义为4字节量。每4个字节将会用一个N来表示。
类型布局规则
标量,比如int和bool每个标量的基准对齐量为N。
向量2N或者4N这意味着vec3的基准对齐量为4N。
标量或向量的数组每个元素的基准对齐量与vec4的相同。
矩阵储存为列向量的数组,每个向量的基准对齐量与vec4的相同。
结构体等于所有元素根据规则计算后的大小,但会填充到vec4大小的倍数。
layout (std140) uniform ExampleBlock
{
                     // 基准对齐量       // 对齐偏移量
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (必须是16的倍数,所以 4->16)
    mat4 matrix;     // 16              // 32  (列 0)
                     // 16              // 48  (列 1)
                     // 16              // 64  (列 2)
                     // 16              // 80  (列 3)
    float values[3]; // 16              // 96  (values[0])
                     // 16              // 112 (values[1])
                     // 16              // 128 (values[2])
    bool boolean;    // 4               // 144
    int integer;     // 4               // 148
}; 

6、根据std140布局的规则,就能使用glBufferSubData的函数将变量数据按照偏移量填充进缓冲中了。虽然std140布局不是最高效的布局,但它保证了内存布局在每个声明了这个Uniform块的程序中是一致的。

4.7.5 使用Uniform缓冲

1、首先,调用glGenBuffers创建一个Uniform缓冲对象,将它绑定到GL_UNIFORM_BUFFER目标,并调用glBufferData,分配足够的内存。

unsigned int uboExampleBlock;
glGenBuffers(1,&uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER,uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER,152,NULL,GL_STATIC_DRAW);//分配152字节的内存
glBindBuffer(GL_UNIFORM_BUFFER,0);

2、当需要对缓冲更新或者插入数据,都会绑定到uboExampleBlock,并使用glBufferSubData来更新它的内存。只需要更新这个Uniform缓冲一次,所有使用这个缓冲的着色器就都使用的是更新后的数据了。

3、在OpenGL上下文中,定义了一些绑定点,将一个Uniform缓冲链接至它,并将着色器中的Uniform块绑定到相同的绑定点,就可以将Uniform缓冲对象和Uniform块对应起来。
在这里插入图片描述
4、可以绑定多个Uniform缓冲到不同的绑定点上。因为着色器A和着色器B都有一个链接到绑定点0的Uniform块,它们的Uniform块将会共享相同的uniform数据。

  • 调用glUniformBlockBinding函数将Uniform块绑定到一个特定的绑定点中,参数是着色器ID、Uniform块索引和绑定点。Uniform块索引是着色器中Uniform块的位置值索引,通过调用glGetUniformBlockIndex来获取,参数是着色器ID和Uniform块的名称。

    unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
    glUniformBlockBinding(shaderA.ID, lights_index, 2);
    
  • 使用glBindBufferBaseglBindBufferRangeUniform缓冲对象绑定到相同的绑定点上。glBindbufferBase需要目标,绑定点索引和Uniform缓冲对象作为它的参数。glBindBufferRange函数需要附加偏移量和大小参数,可以绑定Uniform缓冲的特定一部分到绑定点中。使用glBindBufferRange函数,可以让多个不同的Uniform块绑定到同一个Uniform缓冲对象上。

    glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
    // 或
    glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
    

5、使用glBufferSubData函数,用一个字节数组添加所有的数据,或者更新缓冲的一部分。要想更新uniform变量boolean,可以用以下方式更新Uniform缓冲对象:

glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);
4.7.6 举例

1、在顶点着色器中使用了3个矩阵:投影、观察和模型矩阵,而只有模型矩阵会频繁变动。如果有多个着色器使用了这同一组矩阵,那么使用Uniform缓冲对象会更好,因此将投影和模型矩阵存储到一个叫做MatricesUniform块中。

#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};
uniform mat4 model;

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

2、在例子程序中显示4个立方体,每个立方体使用不同的着色器程序渲染的。将顶点着色器的Uniform块设置为绑定点0。

unsigned int uniformBlockIndexRed    = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen  = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue   = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");  

glUniformBlockBinding(shaderRed.ID,    uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID,  uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID,   uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);

3、创建Uniform缓冲对象本身,为缓冲分配了足够的内存,它等于glm::mat4大小的两倍。并将缓冲中的特定范围(在这里是整个缓冲)链接到绑定点0。

unsigned int uboMatrices
glGenBuffers(1, &uboMatrices);

glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));

4、填充缓冲。将投影矩阵的视野值保持不变(所以摄像机就没有缩放了),只需要将其在程序中定义一次,使用glBufferSubData在进入渲染循环之前存储投影矩阵:

glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);

5、在每次渲染迭代中绘制物体之前,将观察矩阵更新到缓冲的后半部分:

glm::mat4 view = camera.GetViewMatrix();           
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);

6、因为修改了模型矩阵,每个立方体都移动到了窗口的一边,并且由于使用了不同的片段着色器,它们的颜色也不同。

glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f));  // 移动到左上角
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);        
// ... 绘制绿色立方体
// ... 绘制蓝色立方体
// ... 绘制黄色立方体 

在这里插入图片描述

7、Uniform缓冲对象比起独立的uniform有很多好处。第一,一次设置很多uniform会比一个一个设置多个uniform要快很多。第二,比起在多个着色器中修改同样的uniform,在Uniform缓冲中修改一次会更容易一些。第三,如果使用Uniform缓冲对象的话,你可以在着色器中使用更多的uniform。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值