屏幕读取着色器¶
介绍¶
通常,需要使一个明暗器从它正在写入的同一屏幕上读取。由于内部硬件的限制,诸如OpenGL或DirectX等3D API使得这变得非常困难。GPU极为并行,因此读写会导致各种缓存和一致性问题。因此,即使是最现代的硬件也不能正确地支持这一点。
解决方法是将屏幕或屏幕的一部分复制到后台缓冲区,然后在绘图时从中读取。Godot提供了一些工具,使这个过程容易!
屏幕纹理内置纹理¶
Godot 底纹语言 有一个特殊的纹理,“屏幕纹理”(和“深度纹理”的深度,在三维情况下)。它将屏幕的UV作为论据,并返回带有颜色的vec3 rgb。一个特殊的内置变化:屏幕紫外线可以用来获得当前片段的紫外线。因此,这个简单的二维片段着色器:
void fragment() {}
COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
}
结果是一个看不见的物体,因为它只显示了背后的东西。
必须使用TextureLod的原因是,当Godot复制回一块屏幕时,它也会对其mipmap进行有效的分离高斯模糊。
这不仅允许从屏幕上阅读,而且允许以不同的模糊度免费阅读。
屏幕纹理示例¶
屏幕纹理可以用于许多事情。有一个特别的演示 屏幕空间明暗器 ,您可以下载以查看和学习。一个例子是一个简单的明暗器来调整亮度、对比度和饱和度:
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;
}
幕后¶
虽然这看起来很神奇,但事实并非如此。当第一次在将要绘制的节点中发现屏幕纹理时,内置的屏幕纹理会将全屏复制到后台缓冲区。在明暗器中使用它的后续节点将不会为它们复制屏幕,因为这会导致效率低下。
因此,如果使用“屏幕纹理”的明暗器重叠,则第二个明暗器将不会使用第一个明暗器的结果,从而导致意外的视觉效果:
在上面的图像中,第二个球体(右上角)与下面的第一个球体使用相同的屏幕纹理源,因此第一个球体“消失”或不可见。
在3D中,这是不可避免的,因为复制是在不透明渲染完成时进行的。
在2d中,可以通过 BackBufferCopy 节点,可以在两个球体之间实例化。backbuffercopy可以通过指定屏幕区域或整个屏幕来工作:
使用正确的后缓冲区复制,两个球体正确混合:
后缓冲逻辑¶
所以,为了更清楚地说明,下面是后台缓冲区复制逻辑在godot中的工作原理:
如果一个节点使用屏幕纹理,那么在绘制该节点之前,整个屏幕将被复制到后台缓冲区。这种情况只在第一次发生;后续节点不会触发这种情况。
如果在上述点中的情况之前处理了backbuffercopy节点(即使未使用屏幕纹理),则不会发生上述点中描述的行为。换句话说,只有在第一次在一个节点中使用屏幕纹理,并且之前没有发现任何树顺序的backbuffercopy节点(未禁用)时,才会自动复制整个屏幕。
backbuffercopy可以复制整个屏幕或区域。如果设置为仅区域(而不是整个屏幕),并且您的明暗器使用的像素不在复制的区域中,则该读取的结果是未定义的(很可能是来自以前帧的垃圾)。换句话说,可以使用backbuffercopy复制屏幕的一个区域,然后在另一个区域上使用屏幕纹理。避免这种行为!
DEPTH_TEXTURE¶
对于3D着色,也可以访问屏幕深度缓冲区。为此,使用内置的深度纹理。此纹理不是线性的;必须通过反向投影矩阵进行转换。
以下代码检索所绘制像素下方的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;
}