OpenGL中的image变量

写在前面

  OpenGL 在 4.2 版本中增加了新的 glsl 变量 —— image 变量,详情请见 image variable。image 变量和 texture 变量非常相似,不同点在于 image 变量同时具有读和写的功能,而 texture 变量是只读的。由于 OpenGL 的历史遗留原因,glsl 中没有 texture 变量,只能使用 sampler,但是 texture 和 sampler 其实各司其职,OpenGL 中的 texture 变量和 glsl 中的 sampler 变量搭配运行,望知悉。

Image 变量

  image 变量只能被定义在 glsl 中,通过将 OpenGL 中的 texture 变量绑定到 glsl image 变量来使用。所以和 sampler 变量一样,image 变量也必须使用 uniform 存储符来修饰。
  可读可写的 image 变量的用处非常大,比如顺序无关的透明,配合 compute shader 甚至可以实现 CUDA 的通用计算效果。
  但是天下没有白吃的午餐,image 变量的强大伴随着效率的降低。由于 image 变量是同时可写可读的,必定会面临并发操作的问题 —— image 变量的操作是并不是原子化操作。所以程序员就有必要管理好 image 变量的读写问题。

内存修饰符

  对于所有可能产生读写冲突的 OpenGL&glsl 操作一样,image 变量也需要定义多种内存声明。注意,下列修饰符都使用在 glsl 程序中


coherent

  控制图像的缓存机制,用于运行不同片元的同一个着色器共享信息,如果学习过 CUDA 的话就会了解到它声明开辟 shared memory。正是开辟并行运行不同片元的同一个着色器会访问到同一个 coherent 修饰的 image 变量,所以在读取时尽可能保证前面的写操作已经完成。所以经常需要配合 glsl 中的 memoryBarrier() 函数来配合使用。

volatile

  volatile 修饰符和 C 语言中的意义一致。比如出现 const 修饰的字面值常量编译器可能会直接带入到代码中而不为它分配内存,还有出现的永远不会运行到的代码,会被剔除掉,代码声明自然也就删除掉了。glsl 编译器会自动删除只有定义而没有用到的变量。使用 volatile 修饰就能够保证编译器一定会注意到它并且为它分配内存,即使后面用不上。
  volatile 可以修饰 glsl 中所有的变量:全局声明, uniform 变量,函数参数,局部变量。

restrict

  restrict 修饰符和 C 语言中的意义一直。负责指示编译器某个图像中引用的数据不是其他的别名(意义与 C++ 中的引用类似)。也就是说,同一个 shader 中修饰了 restrict 的 image 变量不能和其他 image 变量绑定同一个 texture。
  这个关键字非常有用,这种情况下,将数据写入 texture 不会影响其它 texture 的内容。glsl 编译器默认外部缓存可能存在别名的问题,所以会保证安全,限制运行次序。如果使用了 restrict 修饰符,编译器就可以有更大的自由优化代码。
  restrict 在这里应该翻译为“严格区分”的意思。同样,restrict 也可以修饰 glsl 中所有的变量。另外,标准推荐在使用 image 变量时尽可能加上 restrict 修饰。

readonly(writeonly)

  这两个修饰符非常好理解。表示限制 image 变量只能读或者写。唯一要注意的是,当限制为 writeonly 时,不能使用原子操作。原子操作内部执行的顺序是读取-修改-写入的循环,所以无法对声明了 readonly 或者 writeonly 的 image 使用原子操作。


绑定 texture 到 image

  和 glsl 中的 sampler 的意义一样。需要将 image 变量和 texture 变量绑定到同一个 image unit,和 texture unit 不通用。glBindImageTexture 可以在外部增加关键字 GL_READ_ONLY 来设定 image 变量的修饰符,和 glsl 中的 readonly 等一致。如果使用 glBindImageTexture,会自动覆盖掉 glsl 中的设定。所以,要使用 glsl设定的模式需要使用 glBindImageTextures。

读取和写入

  image 变量的读取和写入通过内置函数实现。从一个 image 变量中读取纹素的时候直接调用 imageLoad() 函数,第一个参数为 image 变量,第二个参数为纹素的整数位置,比如取左下角开始 20×20 位置的值应该输出 (19,19);写入纹素的时候调用 imageStore() 函数,第一个参数为 image 变量,第二个参数为纹素的整数位置,第三个参数为写入变量。
  imageSize() 函数可以返回图像的大小,二维则返回ivec2,三维 image 变量返回 ivec3。


  image 变量读取和写入原子操作不再赘述,见下方代码:

#version 420 core
// 设定 image 的格式为 r32ui
layout(r32ui) uniform uimage2D overdraw_count;
void main() {
    // 原子操作加法
    imageAtomicAdd(overdraw_count, ivec2(gl_FragCoord.xy), 1);
}

通用计算

  image 变量的一个非常棒的特性是可以和计算着色器结合进行通用计算。
  计算着色器是可以单独运行的着色器。计算着色器不使用 glDraw… 函数来调用,而使用 glDispatch() 来运行,参数和 CUDA 核函数调用类似,用于指定维度。理论上,glsl 可以用所有 CUDA 可以实现的通用计算。

示例:计算纹理平均亮度

  有时候,我们需要获取纹理平均亮度来实现 HDR 动态调整曝光的目的。纹理数据存储在 GPU 中,如果选择复制到 CPU 中开销太大,如果选择渲染一个正方形,会浪费很多不必要的计算。这时候计算着色器就派上用场了。如下所示:

layout(local_size_x = 1, local_size_y = 1) in;
layout(rgba16f) readonly restrict uniform image2D inputImage;
layout(rgba16f) writeonly restrict uniform image2D outputImage;

const vec3 LUMINANCE_VECTOR = vec3(0.2125, 0.7154, 0.0721);
void main()
{
  ivec2 size = imageSize(inputImage);
  float logLumSum = 0;
  int x, y;
  for(y = 0; y < size.y; ++y)
  {
    for(x = 0; x < size.x; ++x)
    {
        logLumSum += (dot(imageLoad(inputImage, ivec2(x,y)).rgb, LUMINANCE_VECTOR) + 0.00001);
    }
  }
  logLumSum /= 256.0;
  float val = (logLumSum + 0.00001);
  imageStore(outputImage, ivec2(0, 0), vec4(val, val, val, val));
}

  上面的代码是一个简单的实例。这个计算着色器总共只有一个线程,它计算出来纹理的整体亮度,然后保存在 outputImage 的 (0,0) 位置上。正是由于 glsl 中的 shader storage block 和 image 变量的出现,配合 OpenGL 中的 SSBO 和 texture 变量,很多 CUDA 上的程序都可以修改后应用到 glsl 上。
  最后,由于 glBindTexture() 函数的存在,在 OpenGL 端和 glsl 端都可以控制 image 变量的参数。更推荐写 glsl 时就确定参数,然后使用 glBindTextures() 默认使用 glsl 写入的参数。

转载请带上本文永久固定链接:http://www.gleam.graphics/image-variable.html

About the Author

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注