原文地址:Docs » Shading » Screen-reading shaders
我们经常会渴望能够在shader中读取当前正在绘制的屏幕信息。然而诸如OpenGL DirectX 等3D API由于内部硬件的限制实现这个功能都非常困难。GPU是极端并行性的,所以读写屏幕会引发各种各样的缓存以及同一性问题。因此即使现在最先进的硬件设备也没有很好地支持这个机制。
替代方法是制作一个全屏幕或者部分屏幕的拷贝,存储到后台缓冲区(back-buffer
),渲染的时候从中读取。Godot提供了几个工具可以使这个处理变得简单。
Godot的着色器语言有一个特殊的纹理,“SCREEN_TEXTURE
”(以及“DEPTH_TEXTURE
”存储深度,在3D情况下)。
我们结合屏幕的UV
可以获得vec3
类型的屏幕RGB颜色。一个内置的varying
型属性SCREEN_UV
可以为当前的片元函数获取UV
。代码如下所示;
void fragment() {}
COLOR = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
}
这样会得到一个不可见的对象,因为它显示的底层的内容。
之所以要使用textureLod
函数,是因为Godot会复制一个屏幕块,并且会对它的mipmap
产生一个高效的高斯模糊。
译者注:textureLod函数的注释
vec4_type textureLod ( sampler2D_type s, vec2 uv, float lod ) //基于自定义深度读取并生成一个2D纹理
vec4_type textureLod ( samplerCube s, vec3 uv, float lod ) //基于自定义深度读取并生成一个立方体
这种机制不仅可以读取屏幕还可以在毫无额外计算的情况下基于不同的模糊度来读取。
SCREEN_TEXTURE
可以使用在很多场景。Demo中有一个屏幕空间着色器的实例,你可以下载并学习。其中一个例子是用shader调节亮度(brightness
),对比度(contrast
)和饱和度(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;
}
译者注:mix函数的注释:线性插值函数
虽然看起来很神奇,其实很简单。当第一次在一个即将被绘制的节点发现(其shader中使用)SCREEN_TEXTURE
的时候,引擎会做一个全屏拷贝,并缓存在后台缓冲区(back-buffer
)中。接下来即便再发现有在shader中使用SCREEN_TEXTURE
的节点也不再做全屏拷贝了,因为这样做是没有意义的。
那么,如果想在shader中用SCREEN_TEXTURE
做重叠效果,第二个节点将不会使用第一个节点的结果,于是就会产生一些非预期的效果。
上面这个图,第二个圆(即右上那个)使用的是和第一个圆相同的源,即都是第一个圆下面的内容,所以第一个圆(被重叠)的部分就消失了,或者说不可见了。
在3D中这是不可避免的,因为拷贝过程发生在非透明物体渲染完成后。
在2D中,这个现象可以通过后台缓冲区备份节点(BackBufferCopy
)被纠正。这种节点可以在两个圆中间被实例化。它可以针对某一块指定的屏幕区域或者全屏幕来工作。
这样两个圆就可以正确地融合了。
下面我们来彻底解释一下,Godot中后台缓冲区到底是怎样拷贝的:
如果一个节点使用了SCREEN_TEXTURE
,那么整个屏幕将会在这个节点被绘制之前被拷贝到后台缓冲区。这(个拷贝事件)只发生在第一次(有节点使用SCREEN_TEXTURE
的时候),即使后续再有节点使用SCREEN_TEXTURE
,拷贝事件也再不会被触发。
自动拷贝整个屏幕的仅仅发生在如下条件:SCREEN_TEXTURE
初次在一个节点被使用,并且在节点树上,此节点之前没有发现BackBufferCopy
节点。
后台缓冲备份的对象既可以是整个屏幕也可以是一个区域。如果用BackBufferCopy
节点拷贝一个区域(即不是全屏),然后又将SCREEN_TEXTURE
用在另一个区域上,这种行为是可能的,但是请避免这种行为!
在3D着色器中,我们可以访问屏幕深度缓冲区(screen depth buffer
)。这时将会用到DEPTH_TEXTURE
属性。这个属性获取的纹理不是线性的;它必须通过逆投影矩阵(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;
}