屏幕阅读着色器

简介

It is often 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内置纹理

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

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

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

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

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

注解

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

SCREEN_TEXTURE示例

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

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;
}

在幕后

虽然这看起来很神奇, 但其实不然. 在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

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;
}