屏幕阅读着色器

简介

很多时候,人们希望制作一个着色器,从它所写入的同一个屏幕上读取信息.由于内部硬件的限制,3D API,如OpenGL或DirectX,使之非常困难.GPU是极其并行的,所以读写会导致各种缓存和一致性问题.因此,即使是最现代的硬件也无法正确支持这一点.

解决办法是将屏幕或屏幕的一部分复制到一个后备缓冲区,然后在绘图时从那里读取.Godot提供了一些工具,使这一过程变得很容易!

SCREEN_TEXTURE内置纹理

Godot doc_shading_language`有一个特殊的纹理, ``SCREEN_TEXTURE` (在3D的情况下, DEPTH_TEXTURE 代表深度).它以屏幕的UV作为参数,并返回一个带有颜色的RGB vec3.一个特殊的内置变量.SCREEN_UV可以用来获取当前片段的UV.因此,这是个简单的canvas_item片段着色器:

  1. void fragment() {
  2. COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
  3. }

导致一个不可见的对象,因为它只是显示了背后的东西.

之所以必须使用textureLod是因为,当Godot复制一大块屏幕时,它还会对其mipmap执行有效的可分离高斯模糊.

这不仅允许从屏幕上读取,而且可以免费读取具有不同模糊量的屏幕.

注解

由于性能较差,且与旧设备不兼容,所以在GLES2中不会生成Mipmaps.

SCREEN_TEXTURE示例

SCREEN_TEXTURE 可以用来做很多事情.有一个专门的 Screen Space Shaders 的演示,你可以下载来看看和学习.其中一个例子是一个简单的着色器,用来调整亮度、对比度和饱和度:

  1. shader_type canvas_item;
  2. uniform float brightness = 1.0;
  3. uniform float contrast = 1.0;
  4. uniform float saturation = 1.0;
  5. void fragment() {
  6. vec3 c = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0).rgb;
  7. c.rgb = mix(vec3(0.0), c.rgb, brightness);
  8. c.rgb = mix(vec3(0.5), c.rgb, contrast);
  9. c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);
  10. COLOR.rgb = c;
  11. }

在幕后

虽然这看起来很神奇,但其实不然.在2D中, SCREEN_TEXTURE 内置,当第一次在一个即将被绘制的节点中找到时,会做一个全屏的拷贝到一个back-buffer.在着色器中使用它的后续节点将不会为它们复制屏幕,因为这终将是低效的.在3D中,屏幕是在不透明的几何通道之后,而在透明的几何通道之前被复制,所以透明的物体不会被捕捉到 SCREEN_TEXTURE .

因此,在2D中,如果使用 SCREEN_TEXTURE 的着色器重叠,第二个着色器将不会使用第一个着色器的结果,从而导致意外的视觉效果:

../../_images/texscreen_demo1.png

在上图中,第二个球体(右上方)使用的 SCREEN_TEXTURE 来源与下面第一个球体相同,所以第一个球体 “disappears” ,或者说不可见.

在2D中,这可以通过 BackBufferCopy 节点来纠正,它可以在两个球体之间实例化.BackBufferCopy可以通过指定一个屏幕区域或整个屏幕来工作:

../../_images/texscreen_bbc.png

通过正确的后缓冲区复制,两个球体正确混合:

../../_images/texscreen_demo2.png

在3D中,由于 SCREEN_TEXTURE 只捕获一次,所以解决这个特殊问题的灵活性较小.在3D中使用 SCREEN_TEXTURE 时要小心,因为它不会捕获透明的物体,可能会捕获一些在物体前面的不透明物体.

在3D中,你可以这样重现后置缓冲(back-buffer)逻辑:创建 视图窗口(Viewport) 并在你的对象所在位置放置摄像机,然后使用 视图窗口的(Viewport’s) 纹理(而不是 `` SCREEN_TEXTURE`` ).

后缓冲逻辑

所以,为了更清楚,这里是backbuffer复制逻辑在Godot中的工作原理:

  • 如果一个节点使用了 SCREEN_TEXTURE ,在绘制该节点之前,整个屏幕会被复制到后面的缓冲区.这只发生在第一次;随后的节点不会触发这个.

  • 如果一个BackBufferCopy节点在上面一点的情况之前被处理(即使 SCREEN_TEXTURE 没有被使用),上面一点描述的行为就不会发生.换句话说,只有当 SCREEN_TEXTURE 第一次在节点中使用,并且在树形顺序中之前没有发现BackBufferCopy节点时(未禁用),自动复制整个屏幕才会发生.

  • BackBufferCopy可以复制整个屏幕或一个区域.如果只设置为一个区域(而不是整个屏幕),而你的着色器使用了不在复制区域内的像素,那么该读取的结果是未定义的(很可能是以前帧的垃圾).换句话说,有可能使用BackBufferCopy复制回屏幕的一个区域,然后在不同的区域使用 SCREEN_TEXTURE .避免这种行为!

DEPTH_TEXTURE

对于3D着色器,也可以访问屏幕深度缓冲区.为此,使用了 DEPTH_TEXTURE 内置.这个纹理不是线性的;它必须通过反投影矩阵进行转换.

以下代码检索正在绘制的像素下方的3D位置:

  1. void fragment() {
  2. float depth = textureLod(DEPTH_TEXTURE, SCREEN_UV, 0.0).r;
  3. vec4 upos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
  4. vec3 pixel_position = upos.xyz / upos.w;
  5. }