屏幕阅读着色器

前言

很多人想要让着色器在写屏幕的同时读取该屏幕的数据。因为内部硬件限制,OpenGL 和 DirectX 等 3D API 都很难实现这一功能。GPU 是极其并行的,所以同时进行读写会导致各种缓存和一致性问题。因此,即便是最新的硬件也对此进行无法正确的支持。 解决办法是将屏幕或屏幕的一部分复制到一个后备缓冲区,然后在绘图时从那里读取。Godot提供了一些工具,使这一过程变得很容易。

SCREEN_TEXTURE内置纹理

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

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

导致一个不可见的对象, 因为它只是显示了背后的东西. 之所以必须使用textureLod是因为, 当Godot复制一大块屏幕时, 它还会对其mipmap执行有效的可分离高斯模糊. 这不仅允许从屏幕上读取, 而且可以免费读取具有不同模糊量的屏幕. 注解 由于性能较差, 且与旧设备不兼容, 所以在GLES2中不会生成Mipmaps.

SCREEN_TEXTURE示例

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

  1. 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 内置, 当第一次在一个即将被绘制的节点中找到时, 会做一个全屏的拷贝到一个back-buffer. 在着色器中使用它的后续节点将不会为它们复制屏幕, 因为这终将是低效的. 在3D中, 屏幕是在不透明的几何通道之后, 而在透明的几何通道之前被复制, 所以透明的物体不会被捕捉到 SCREEN_TEXTURE . SCREEN_TEXTURE 的着色器重叠, 第二个着色器将不会使用第一个着色器的结果, 从而导致意外的视觉效果: SCREEN_TEXTURE 来源与下面第一个球体相同, 所以第一个球体 “disappears” , 或者说不可见. BackBufferCopy 节点来纠正, 它可以在两个球体之间实例化.BackBufferCopy可以通过指定一个屏幕区域或整个屏幕来工作: 通过正确的后缓冲区复制, 两个球体正确混合: SCREEN_TEXTURE 只捕获一次, 所以解决这个特殊问题的灵活性较小. 在3D中使用 SCREEN_TEXTURE 时要小心, 因为它不会捕获透明的物体, 可能会捕获一些在物体前面的不透明物体. 视图窗口(Viewport) 并在你的对象所在位置放置摄像机, 然后使用 视图窗口的 (Viewport’s) 纹理(而不是 SCREEN_TEXTURE ).

后缓冲逻辑

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

  • SCREEN_TEXTURE , 在绘制该节点之前, 整个屏幕会被复制到后面的缓冲区. 这只发生在第一次;随后的节点不会触发这个.
  • SCREEN_TEXTURE 没有被使用), 上面一点描述的行为就不会发生. 换句话说, 只有当 SCREEN_TEXTURE 第一次在节点中使用, 并且在树形顺序中之前没有发现BackBufferCopy节点时(未禁用), 自动复制整个屏幕才会发生.
  • SCREEN_TEXTURE . 避免这种行为!

    DEPTH_TEXTURE

    DEPTH_TEXTURE 即可。该纹理不是线性的;必须通过逆投影矩阵进行转换。 以下代码检索正在绘制的像素下方的3D位置:
    1. 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;}