屏幕阅读着色器

简介

Very often, it is desired to make a shader that reads from the same screen to which it’s writing. 3D APIs, such as OpenGL or DirectX, make this very difficult because of internal hardware limitations. GPUs are extremely parallel, so reading and writing causes all sorts of cache and coherency problems. As a result, not even the most modern hardware supports this properly.

The workaround is to make a copy of the screen, or a part of the screen, to a back-buffer and then read from it while drawing. Godot provides a few tools that make this process easy!

SCREEN_TEXTURE built-in texture

Godot 着色语言 has a special texture, “SCREEN_TEXTURE” (and “DEPTH_TEXTURE” for depth, in the case of 3D). It takes as argument the UV of the screen and returns a vec3 RGB with the color. A special built-in varying: SCREEN_UV can be used to obtain the UV for the current fragment. As a result, this simple 2D fragment shader:

void fragment() {
    COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
}

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

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

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

SCREEN_TEXTURE示例

SCREEN_TEXTURE can be used for many things. There is a special demo for Screen Space Shaders, that you can download to see and learn. One example is a simple shader to adjust brightness, contrast and saturation:

shader_type canvas_item;

uniform float brightness = 1.0;
uniform float contrast = 1.0;
uniform float saturation = 1.0;

void fragment() {
    vec3 c = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0).rgb;

    c.rgb = mix(vec3(0.0), c.rgb, brightness);
    c.rgb = mix(vec3(0.5), c.rgb, contrast);
    c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);

    COLOR.rgb = c;
}

在幕后

虽然这看起来很神奇,但事实并非如此。 内置的SCREEN_TEXTURE,当首次在即将绘制的节点中找到时,会将全屏复制到后台缓冲区。 在着色器中使用它的后续节点将不会为它们复制屏幕,因为这最终效率低下。

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

../../_images/texscreen_demo1.png

在上图中,第二个球体(右上角)使用与下面第一个相同的SCREEN_TEXTURE源,因此第一个``消失``或不可见。

在3D中,这是不可避免的,因为在不透明渲染完成时会发生复制。

In 2D, this can be corrected via the BackBufferCopy node, which can be instantiated between both spheres. BackBufferCopy can work by either specifying a screen region or the whole screen:

../../_images/texscreen_bbc.png

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

../../_images/texscreen_demo2.png

后缓冲逻辑

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

  • If a node uses the SCREEN_TEXTURE, the entire screen is copied to the back buffer before drawing that node. This only happens the first time; subsequent nodes do not trigger this.
  • If a BackBufferCopy node was processed before the situation in the point above (even if SCREEN_TEXTURE was not used), the behavior described in the point above does not happen. In other words, automatic copying of the entire screen only happens if SCREEN_TEXTURE is used in a node for the first time and no BackBufferCopy node (not disabled) was found before in tree-order.
  • BackBufferCopy可以复制整个屏幕或区域。 如果仅设置为区域(不是整个屏幕),并且着色器使用不在复制区域中的像素,则该读取的结果是未定义的(很可能是前一帧中的垃圾)。 换句话说,可以使用BackBufferCopy复制屏幕区域,然后在不同区域使用SCREEN_TEXTURE。 避免这种行为!

DEPTH_TEXTURE

For 3D Shaders, it’s also possible to access the screen depth buffer. For this, the DEPTH_TEXTURE built-in is used. This texture is not linear; it must be converted via the inverse projection matrix.

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

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