Linux OpenGL 实践篇-15-图像数据操作
OpenGL图像数据操作
源代码:https://github.com/xin-lover/opengl-learn/tree/master/chapter-15-memory
图像数据操作允许用户在指定的位置进行读写操作的机制,着色器可以因此在内存中建立数据结构,然后谨慎地更新同一块内存位置,完成彼此之间的通信过程。
简单来说,就是一种着色器间的一种数据通信的手段。之前我们常见的通信手段:
-
- 在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值;虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存区域,不过总体上来说写入的时机是固定的,也是可以预知的。
- 在固定的阶段通过transform feedback操作来获取顶点数据并传递到transform feedback缓存中,
- 光栅化阶段,将片元着色器产生的像素写入帧缓存中。
使用纹理存储通用数据
我们可以使用内存来表示一个缓存对象,或则一个单一层级的纹理对象,并且在着色器中进行通用目的的读写操作。
在OpenGL中提供一些特别的图像类型来支持这一需求,它们主要用来表达未编码的图像数据。图像类型和采样器(sampler)非常的类似,比如采样器类型sampler2D,对应的图像类型image2D或iimage2D,在着色器中也是使用uniform定义,但它们并不相同:
首先,图像类型表达的是单一层级的纹理,不是完整的mipmap;
其次,图像类型不支持滤波等采样操作。注意不支持的操作还包括深度比较(depth comparison),所以阴影类型的采样器并没有对应的图像类型。
下面我们列了一些常用的图像类型:
图像类型 | 意义 |
image1D | 1D浮点数类型 |
image2D | 2D浮点数类型 |
image3D | 3D浮点数类型 |
imageCube | 浮点数Cube map数组类型 |
imageBuffer |
浮点数缓存类型 |
iimage1D |
1D有符号整数类型 |
iimage2D | 2D有符号整数类型 |
iimage3D | 3D有符号整数类型 |
iimageBuffer | 有符号整数缓存类型 |
uimage1D | 1D无符号整数类型 |
uimage2D | 2D无符号整数类型 |
uimage3D | 3D无符号整数类型 |
uimageCube | 无符号整数Cube map数组类型 |
uimageBuffer | 无符号缓存类型 |
这些图像类型定义的是通用的数据类型,我们还需要一个format的限定符来设置数据在内存中的图像格式。下表中将列举常用的一些图像格式限定符。
图像类型 | OpenGL内部格式 |
rgba32f | GL_RGBA32F |
rgba16f | GL_RGBA16F |
rg32f | GL_RG32F |
rg16f | GL_RG16F |
r32f | GL_R32F |
r16f | GL_R16F |
rgba32i | GL_RGBA32I |
rgba16i | GL_RGBA16I |
rgba8i | GL_RGBA8I |
rgba32ui | GL_RGBA32UI |
rgba16ui | GL_RGBA16UI |
rgba8ui | GL_RGBA8UI |
图像的format限定符是作为图像变量声明的一部分提供的,并且必须在声明一个用来读取图像的变量的时候使用。如果图像变量只用来写入,那么我们也可以忽略这个限定符。
注意,我们在使用图像限定符的时候一定要和图像本身的基本数据类型相匹配。比如,image2D必须使用浮点类型的限定符,如r32f或者rgba16_unorm,而非浮点型的限定符rg8ui是不行的。
图像类型在着色器中的声明示例如下:
layout(binding=0,rgba32f) uniform image2D image1;
其中binding表示图像单元的索引,类似采样器的位置(如GL_TEXTURE0),也可以使用glUniform1i来设置,默认为0,如果只使用一个图像就不需要显示使用glUniform1i设置。rgba32f即图像的format。
下面的例子我们将实现在glsl中存取图像数据:
static const GLfloat cData[] = { 1.0,0.0,0.0,1.0, 0.0,1.0,0.0,1.0, 0.0,0.0,1.0,1.0, 1.0,1.0,0.0,1.0 }; glGenTextures(1,&tex); glBindTexture(GL_TEXTURE_2D,tex); glTexStorage2D(GL_TEXTURE_2D,1,GL_RGBA32F,2,2); glTexSubImage2D(GL_TEXTURE_2D, 0, 0,0, 2,2, GL_RGBA,GL_FLOAT, cData); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); //输出图像 glGenTextures(1,&otex); glBindTexture(GL_TEXTURE_2D,otex); glTexStorage2D(GL_TEXTURE_2D,1,GL_RGBA32F,4,4); glBindTexture(GL_TEXTURE_2D,0); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glBindTexture(GL_TEXTURE_2D,0);
首先生成两张图片,一张用于输入,一张用于输出。生成的过程于普通的纹理生成过程一样,需要注意的是纹理的格式,即format,在这里,我们使用的是rgba32f,这个要于着色其中的格式相对应。
接下来是使用这两张纹理:
glClear(GL_COLOR_BUFFER_BIT); dShader->Use(); //着色器 glBindImageTexture(0,tex,0,GL_FALSE,0,GL_READ_ONLY,GL_RGBA32F); glBindImageTexture(1,otex,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA32F); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES,0,6); //glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); tShader->Use(); //着色器 glBindTexture(GL_TEXTURE_2D,otex); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES,0,6);
重点是glBindImageTexture这个函数,函数原型:
void glBindImageTexture(GLuint unit,GLuint texture,GLint level, GLboolear layered,GLint layer, GLenum access, GLenum format);
函数意义是将level层的纹理绑定到unit的图像单元。
- 如果texture是1D或者2D的纹理数组,且layered为GL_TRUE,则绑定整个数组,而layered为GL_FALSE则只绑定layer层。
- access可以有三种选择:GL_READ_ONLY,GL_WRITE_ONLY,GL_READ_WRITE,表示的是对图像的访问方式,
- 最后一个format即图像数据在内存中的格式,于texture声明的格式要兼容。
所以上述代码的表示将tex与0图像单元绑定,用于读取数据,otex与1图像单元绑定,用于写入数据,最后再用otex渲染。
下面为顶点和片元着色器代码:
#version 430 core layout(location=0) in vec3 iPos; layout(location=1) in vec2 iTexcoord; uniform mat4 model; uniform mat4 view; uniform mat4 proj; out vec2 texcoords; void main() { texcoords = iTexcoord; gl_Position = proj * view * model * vec4(iPos,1); }
#version 430 core layout(binding=0,rgba32f) uniform image2D colors; layout(binding=1,rgba32f) uniform image2D output_buffer; //uniform sampler2D image; in vec2 texcoords; out vec4 color; void main() { //ivec2 pos = ivec2(gl_GlobalInvocationID.xy); ivec2 size = imageSize(output_buffer); vec4 col = imageLoad(colors,ivec2(1,0));//ivec2(gl_FragCoord.xy)); imageStore(output_buffer,ivec2(size.x * texcoords.x, size.y * texcoords.y),vec4(texcoords,0,1)); //imageStore(output_buffer,ivec2(texcoords),vec4(1,1,0,1)); color = col;//vec4(1,0,0,1); //color = texture(image,texcoords); }
顶点着色器没什么变化,在片元着色器中需要对于图像单元的进行读取和写入。在着色器中对图像进行操作需要借助着色器的内置函数:imageLoad,imageStore,imageSize。这三个函数有很多重载,分别对应不同的图像,比如我们这次针对image2D使用的:
gvec4 imageLoad(readonly gimage2D image,ivec2 P); gvec4 imageStore(writeonly gimage2D image,ivec2 P, gvec4 data); ivec2 imageSize(gimage2D image);
imageLoad用于从图像中读取数据,imageStore则是写,imageSize则返回图像的大小。需要注意的是P的类型是ivec2,表示坐标位置,是整形的,于我们常用的vec2浮点型不一样,这个是像素的实际位置,没有进行归一化,即表示第P.x行,P.y列的数据。在本例中我们使用imageSize和顶点的纹理坐标(已归一化)来定位。
效果如图,背后的绿色物体使用从图像的(1,0)位置(整个它图像为2*2的,(1,0)位置为绿色)取的的数据,用于片元着色器输出,中间的矩形是使用着色器写入数据图像的图像渲染的,我们写入的数据是纹理坐标。