之前提到过OGL中纹理缓存是作为输入缓存存在的,这使得输入缓存能够读取但是不能够改写。为了突破这一限制,在OGL中提出了Image的概念,这使得程序员能够有效的操作texture缓存——对背后的texture缓存进行读写操作。但是,这样的操作打破了原有的pipeline流水线,使得原本应该被OGL自身进行管理的缓存需要程序员自身来进行处理,因此提供了方便的同时也增加了程序员的编程负担。
为了对texture缓存进行写操作,需要将对应的纹理数据给绑定到相应的image uint中,需要注意的是image uint和texture uint是两个不同的区域,因此两者的ID号码计数是分离开的。由于image图像是和texture纹理绑定的,因此image的类型和texture的图像类型类似,其次每一个image里都有一个对应的存储类型,这个存储类型和image在内存中的分割以及图像像素的格式是相关的。
为了加载image中的数据,需要利用imageLoad内置函数,该函数需要两个参数,对应的输入图像以及相应的坐标信息,该函数会读取对应坐标处的图像的像素值。反过来,imageStore函数则会将相应的数据存入到对应的图像的对应的坐标位置处。下面是一个利用image进行存储和读取的实例。
首先创建两个纹理缓存,其中image_palette_buffer作为背后的缓存提供存储支持,该函数会分配相应的内存,但是并不进行数据的初始化,同时也不会将这个纹理缓存绑定到对应的texture uint中,真正被绑定到OGL管理的纹理缓存的数据是image_palette_texture。
glGenBuffers(1, &image_palette_buffer);//创建调色板缓存(提供内存)
glBindBuffer(GL_TEXTURE_BUFFER, image_palette_buffer);
glBufferData(GL_TEXTURE_BUFFER, 256 * 4 * sizeof(float), NULL, GL_STATIC_DRAW);
glGenTextures(1, &image_palette_texture);//创建调色板纹理,用于和片元着色器交互
glBindTexture(GL_TEXTURE_BUFFER, image_palette_texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, image_palette_buffer);
vmath::vec4 * data = (vmath::vec4 *)glMapBuffer(GL_TEXTURE_BUFFER, GL_WRITE_ONLY);
for (int i = 0; i < 256; i++)
{
data[i] = vmath::vec4((float)i);
}
glUnmapBuffer(GL_TEXTURE_BUFFER);
首先调用纹理激活函数切换纹理缓存,同时创建新的纹理对象output_texture,这个对象作为输出图像来接收输出的数据。同时设置对应的纹理参数,紧接着将其绑定到第0号图像单元上。注意这里的第0号图像单元和第0号纹理单元不是同一个对象。glBindImageTexture函数的第一个参数用于指定image单元的索引号,第二个参数用于指定纹理的ID号,第三个参数用于指定纹理的层次,倒数第二个参数用于指定图像的读写属性,最后一个则指定图像的存储格式,这里是每一个像素的颜色通道是32位的浮点数,每一个像素包括四个颜色通道RGB以及透明度。
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &output_texture);//2D_TEXTURE,用作输出缓存
glBindTexture(GL_TEXTURE_2D, output_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, MAX_FRAMEBUFFER_WIDTH, MAX_FRAMEBUFFER_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(0, output_texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_RGBA32F);
为了更方便快捷的清除texture缓存中的数据,引入一个新的概念pixel pack object,该对象管理一个像素相关的缓存区域,其解压的端口和纹理缓存或者frame缓存关联,反之当frame缓存和纹理缓存进行打包处理时,则会传递数据到pack object。下面的例子中创建一个PIXEL_UNPACK_BUFFER在稍后的处理中将这个缓存中的数据可以上传到纹理缓存中,达到清除纹理缓存中数据的效果。
glGenBuffers(1, &output_texture_clear_buffer);//解压出来的数据会提交给texture buffer
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, output_texture_clear_buffer);
glBufferData(GL_PIXEL_UNPACK_BUFFER, MAX_FRAMEBUFFER_WIDTH * MAX_FRAMEBUFFER_HEIGHT * sizeof(GLuint), NULL, GL_STATIC_DRAW);
data = (vmath::vec4 *)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
memset(data, 0x00, MAX_FRAMEBUFFER_WIDTH * MAX_FRAMEBUFFER_HEIGHT * sizeof(GLuint));
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
利用图像进行纹理写操作,首先将调色板纹理绑定点到纹理缓存中,作为输入图像颜色的初始数据。同时将输出缓存数据进行清空,主要是利用之前准备好的pack object实现。
//将调色板纹理和图像绑定作为输入颜色的初始数据
glBindImageTexture(0, image_palette_texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F);
//将输出缓存清零
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, output_texture_clear_buffer);
glBindTexture(GL_TEXTURE_2D, output_texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, current_width, current_height, GL_RGBA, GL_FLOAT, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
//绑定输出缓存作为输出,
glBindImageTexture(1, output_texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
在这里需要注意的是glBindImageTexture函数的第一个参数分别是0和1,和glsl语言中对应的图像对应。GLSL语言中利用binding指定绑定的点分别是0和1。在GLSL程序中,首先从给定的颜色纹理中读取颜色值,读取的索引根据给定的图元索引计算得到。第二步则将读取到的颜色值写到2D纹理中,这个纹理和output_texture绑定。GLSL程序中利用imageStore内置函数将颜色值写入到纹理缓存中。在稍后的处理中,只需要绑定output_texture就可以将其作为输入,同时利用output_texture进行片元。
#version 430 core
layout (binding = 0, rgba32f) uniform imageBuffer colors;//和texture buffer关联
layout (binding = 1, rgba32f) uniform image2D output_buffer;//2D_texture
out vec4 color;
void main(void)
{//从一个图像缓存中读取数据,数据的索引根据图元的ID号码决定
vec4 col = imageLoad(colors, gl_PrimitiveID & 255);
imageStore(output_buffer, ivec2(gl_FragCoord.xy) - ivec2(200, 0), col);
imageStore(output_buffer, ivec2(gl_FragCoord.xy) + ivec2(200, 0), col);
}
由于image的出现打破了pipeline的处理流程,因此需要程序员自己进行内存访问正确性的保证。OGL也提供相应的函数来保证对内存进行互斥访问,从而保证访问内存的正确性。主要是利用类似于OS层次的原子操作了外接的函数对这样的。比如利用imageAtomicAdd(output_buffer, ivec2(gl_FragCoord.xy), 1);函数进行原子加操作。
image最主要的一个应用层场景是可以创建OIT(顺序无关透明性),其主要实现原理是利用image缓存获取texture的中间输出,然后将其数据暂存起来,并在适当的时候进行排序,从而保证最终的渲染结果与原始的顺序无关。在红宝书上提供了一种顺序无关透明性的实现,其主要思想是利用链表暂存图像和当前像素相重叠的像素值,然后在需要渲染的时候将其进行排序处理从而保证每次渲染都与顺序无关。GLSL中第一步开启片元测试用于清除无效的片元数据,并且初始化原子计数器的数值为0。在GLSL中首先利用原子操作累加计数器的数值,并将其放入到链表的头指针中,链表是以数组的形式存在于图像缓存中。利用exchange操作返回原来保存的数据,从而可以得到上一个保存的索引数值。接下来利用光照位置以及顶点位置计算关照对像素颜色的影响。同时将计算得到的数据存储在图像list_buffer中,存放格式是x存放上下一个指针的索引,y则存放颜色信息,z则存放当前索引的深度数值,而w则存放一些
#version 420 core
layout (early_fragment_tests) in;
layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
layout (binding = 1, rgba32ui) uniform writeonly uimageBuffer list_buffer;
layout (binding = 0, offset = 0) uniform atomic_uint list_counter;
layout (location = 0) out vec4 color;
in vec3 frag_position;
in vec3 frag_normal;
in vec4 surface_color;
uniform vec3 light_position = vec3(40.0, 20.0, 100.0);
void main(void)
{
uint index;
uint old_head;
uvec4 item;
vec4 frag_color;
index = atomicCounterIncrement(list_counter);
old_head = imageAtomicExchange(head_pointer_image, ivec2(gl_FragCoord.xy), uint(index));
vec3 L = normalize(light_position - frag_position);
vec3 V = normalize(-frag_position);
vec3 N = normalize(frag_normal);
vec3 H = normalize(L + V);
float NdotL = dot(N, L);
float NdotH = dot(N, H);
vec4 modulator = vec4(surface_color.rgb * abs(NdotL), surface_color.a);
vec4 additive_component = mix(surface_color, vec4(1.0), 0.6) * vec4(pow(clamp(NdotH, 0.0, 1.0), 26.0)) * 0.7;
item.x = old_head;
item.y = packUnorm4x8(modulator);
item.z = floatBitsToUint(gl_FragCoord.z);
item.w = packUnorm4x8(additive_component);
imageStore(list_buffer, int(index), item);
frag_color = modulator;
color = frag_color;
}
在真正渲染的时候根据上面得到的链表利用存储的深度数值进行排序,从而有有效的引导颜色的组合,保证了最终的渲染结果与混合的结果无关。
#version 420 core
layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
layout (binding = 1, rgba32ui) uniform uimageBuffer list_buffer;
layout (location = 0) out vec4 color;
#define MAX_FRAGMENTS 40
uvec4 fragment_list[MAX_FRAGMENTS];
void main(void)
{
uint current_index;
uint fragment_count = 0;
current_index = imageLoad(head_pointer_image, ivec2(gl_FragCoord).xy).x;
while (current_index != 0 && fragment_count < MAX_FRAGMENTS)
{
uvec4 fragment = imageLoad(list_buffer, int(current_index));
fragment_list[fragment_count] = fragment;
current_index = fragment.x;
fragment_count++;
}
uint i, j;
if (fragment_count > 1)
{
for (i = 0; i < fragment_count - 1; i++)
{
for (j = i + 1; j < fragment_count; j++)
{
uvec4 fragment1 = fragment_list[i];
uvec4 fragment2 = fragment_list[j];
float depth1 = uintBitsToFloat(fragment1.z);
float depth2 = uintBitsToFloat(fragment2.z);
if (depth1 < depth2)
{//根据深度从大到小排序片元
fragment_list[i] = fragment2;
fragment_list[j] = fragment1;
}
}
}
}
vec4 final_color = vec4(0.0);
for (i = 0; i < fragment_count; i++)
{
vec4 modulator = unpackUnorm4x8(fragment_list[i].y);
vec4 additive_component = unpackUnorm4x8(fragment_list[i].w);
final_color = mix(final_color, modulator, modulator.a) + additive_component;
}
color = final_color;
}