LearnOpenGL——高级数据、高级GLSL

高级数据

OpenGL中的缓冲只是一个管理特定内存块的对象,我们需要将其绑定到一个缓冲目标时,才赋予意义。绑定到GL_ARRAY_BUFFER时就是一个顶点数组缓冲。OpenGL内部会为每个目标储存一个缓冲,并且会根据目标的不同,以不同的方式处理缓冲。
目前为止一直是调用glBufferData函数来填充缓冲对象对应的内存,若将data参数设置为null,那么这个函数只会分配内存不会填充数据。
还可以使用glBufferSubData来填充数据,这个函数允许我们引入一个偏移量,来实现指定位置填充数据。在这个函数使用时需要注意必须有已分配的内存,即要先调用glBufferData函数

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

还有另外一个将数据导入缓冲的方法,就是使用glMapBuffer请求缓冲指针,直接将数据复制到缓冲中。我们还使用了glUnmapBuffer函数,告诉OpenGL我们已经使用完指针,解除指针映射。如果要直接映射数据到缓冲,而不事先将其存储到临时内存中,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));
// 记得告诉OpenGL我们不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);

一、分批顶点属性

当从文件中加载数据时,得到的会是分类好的数据数组,比如会得到三个数组position[]、normal[]、texture[],而不是像我们之前使用的交错的数据数组123123123这样的。我们需要花费点力气将分类好的数组转化成交错的大数组。现在我们可以直接使用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);

我们还需要使用glVertexAttribPointer更新顶点属性指针来反映这些改变

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

二、复制缓冲

缓冲填充好数据后,我们想要实现从缓冲中复制数据到另外一个缓冲中,可以使用glCopyBufferSubData函数

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

readtarget填充的是读取数据目标,writetarget是写入数据目标
但如果我们想读写数据的两个不同缓冲都为顶点数组缓冲该怎么办呢?我们不能同时将两个缓冲绑定到一个缓冲目标上,所以OpenGL给我们提供了另外两种类型的缓冲目标 GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER,将这两个目标作为readtarget和writetarget参数

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

高级GLSL

一、GLSL的内建变量

1.顶点着色器变量

gl_Position是顶点着色器裁剪空间输出位置向量,下面我们还会介绍一些内建变量

gl_PointSize

我们能够选用的其中一个图元是GL_POINTS,每个顶点都是一个图元,都会被渲染为一个点,我们可以使用float类型的变量gl_PointSize来调整渲染出来的点的大小(可以在顶点着色器中修改这个值)。这个功能在OpenGL中默认是禁用的,我们需要先激活

glEnable(GL_PROGRAM_POINT_SIZE);

一个例子就是随着观察者距离点的远近来调整点的大小

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

gl_Position gl_PointSize都是输出变量,gl_VertexID是输入变量,这个变量可以给我们提供当前正在访问的顶点数据索引。比如使用glDrawElements进行绘制时,此时gl_VertexID变量就是正在绘制的顶点的索引;使用glDrawArrays绘制时,gl_VertexID代表的就是从渲染开始到目前已经处理的顶点数量

2.片元着色器

gl_FragCoord

gl_FragCoord的z分量代表的是对应片段的深度值,x和y分量代表的是在窗口空间的坐标。我们使用了glViewport设定了800*600的窗口大小,所以x范围在[0,600],y范围在[0,800]

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

我们设定窗口左半边是红色,右半边是是绿色

gl_FrontFacing

gl_FrontFacing是一个bool变量,返回判断当前面是否为正向面。(我们使用这个变量就不要开启面剔除了,不然就没用了)我们可以根据这个变量来为正向面反向面设置不同的效果

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

此处我为正向面和反向面设置了不同的纹理(如果开启了面剔除就没有意义了)

gl_FragDepth

gl_FragCoord.z深度值是只读的,如果我们要进行修改就要使用float类型的gl_FragDepth变量,但是如果使用了这个变量在某些情况就不能开启Early-Z,因为OpenGL不能在片元着色器渲染之前就得知确定的z值。

layout (depth_<condition>) out float gl_FragDepth;

condition的值可以为以下
在这里插入图片描述
使用greater和less时,我们将深度值写入更大或者更小的值,此时还是可以使用提前深度测试的

二、接口块

我们目前是只声明in和out变量在顶点着色器和片元着色器中传输,但对于数组和结构体我们就需要接口块,这个类似于一个struct

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

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

out VS_OUT
{
    vec2 TexCoords;
} vs_out;

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

使用in或者out来定义是输入还是输出结构体。在传输时,VS_OUT应该是和着色器中一样的,但是实例名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);   
}

三、Uniform缓冲对象

OpenGL限制了它能够处理的uniform数量,这可以通过GL_MAX_VERTEX_UNIFORM_COMPONENTS来查询。当使用Uniform缓冲对象时,最大的数量会更高。所以,当你达到了uniform的最大数量时(比如再做骨骼动画(Skeletal Animation)的时候),可以选择使用Uniform缓冲对象。

目前对于一个个uniform变量,我们还需要一个个来设置,我们可以使用Uniform缓冲对象的工具。它允许我们定义一系列在多个着色器中相同的uniform变量,对于uniform缓冲对象,我们只需要设置相关的uniform一次。对于不同的uniform变量仍需手动配置。
uniform缓冲对象是一个缓冲,我们需要glGenBuffers创建,绑定到GL_UNIFORM_BUFFER缓冲目标,并所有相关的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);
}

在大多数例子中,我们都会在每个渲染迭代中设置每个着色器的projection和view Uniform矩阵,使用了uniform缓冲对象就只需要存储一次就好。
在这个代码中,我们定义了名为Matrices的Uniform块,Uniform块中的变量可以直接访问,不需要加块名做前缀。layout (std140)表示当前定义的Uniform块对它的内容使用一个特定的内存布局。这个语句设置了Uniform块布局。

1.Uniform块布局

Uniform块的内容是储存在一个缓冲对象中的,它实际上只是一块预留内存。我们需要告诉OpenGL内存的哪一部分对应着着色器中的哪一个uniform变量

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

我们需要知道每个变量的大小和偏移量,以此来按顺序防止变量,但是OpenGL没有声明间距的,所以硬件可能是随机放置。默认情况下,GLSL会使用一个叫做“共享”布局的Uniform内存布局,共享是因为一旦硬件定义了偏移量,它们在多个程序中是共享并一致的。
使用共享布局时,GLSL可以为了优化而对uniform变量的位置进行改变(只要保持变量的顺序不变)。因为我们不知道每个变量的偏移值,所以就不知道如何填充Uniform缓冲(虽然可以用glGetUniformIndices查询信息,但工作量会大)
所以我们不使用共享布局,而是std140布局,表明每个变量的偏移量可以由一系列规则制定。
每个变量都有一个基准对齐量,等于每个变量在uniform块中所占据的空间;接下来再计算对其偏移量,它是从块起始位置的字节偏移量,它必须等于基准对齐量的倍数。
在这里插入图片描述

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

根据std140布局的规则,我们就能使用像是glBufferSubData的函数将变量数据按照偏移量填充进缓冲中了。

2.使用Uniform缓冲

我们需要使用glGenBuffers函数创建一个uniform缓冲,并使用glBindBuffer函数绑定到GL_UNIFORM_BUFFER目标,以及使用glBufferData分配内存,glBufferSubData来更新内存

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

我们需要告诉OpenGL哪个缓冲对应哪个Uniform块,所以我们需要将每个着色器中相同的uniform块绑定到相同点上
在这里插入图片描述

unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

从OpenGL 4.2版本起,你可以添加一个布局标识符,显式地将Uniform块的绑定点储存在着色器中

layout(std140, binding = 2) uniform Lights { ... };

接下来我们还需要绑定Uniform缓冲对象到相同的绑定点上,可以使用glBindBufferBase或glBindBufferRange来完成。

  • glBindbufferBase需要一个目标,一个绑定点索引和一个Uniform缓冲对象作为它的参数
  • glBindBufferRange函数,它需要一个附加的偏移量和大小参数,这样子你可以绑定Uniform缓冲的特定一部分到绑定点中,可以实现让不同uniform块绑定到同一个uniform缓冲对象上
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

都配置完毕后,就可以使用glBufferSubData函数,用一个字节数组添加所有数据,或者更新缓冲中的一部分。想要更新uniform中的boolean变量,我们可以使用以下方法

glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值