スクリーン読み取りシェーダー

はじめに

多くの場合、書き込み先と同じ画面から読み取るシェーダーを作成することが望まれます。 OpenGLやDirectXなどの3D APIは、内部ハードウェアの制限により、これを非常に困難にします。 GPUは非常に並列であるため、読み取りと書き込みはあらゆる種類のキャッシュと一貫性の問題を引き起こします。その結果、最新のハードウェアでさえこれを適切にサポートしていません。

回避策は、画面または画面の一部をバックバッファにコピーし、描画中にそこから読み取ることです。 Godotには、このプロセスを簡単にするツールがいくつか用意されています。

スクリーンテクスチャ

Godot doc_shading_lang には、すでにレンダリングされた画面のコンテンツにアクセスするための特別なテクスチャがあります。これは sampler2D のUniformを宣言するときに hint_screen_texture ヒントを指定することで使用されます。またビルトイン変数 SCREEN_UV を使用して、現在のフラグメントの画面を基準としたUVを取得できます。次の2Dフラグメントシェーダーは、結果として背後にあるもののみを表示するため、実質見えないオブジェクトになります:

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

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

ここでは textureLod を使用して、一番下のミップマップからのみ読み取っています。代わりにぼやけたテクスチャから読み取りたい場合は、 textureLod の3番目の引数を増やし、ヒント filter_nearestfilter_nearest_mipmap (またはミップマップが有効になっている他のフィルタ) に変更します。ミップマップフィルターを使用する場合、Godotはぼやけたテクスチャを自動的に計算します。

警告

フィルタモードの名前に mipmap を含んでいない場合、 textureLod のLODパラメータに 0.0 より大きい値を指定しても 0.0 と同じ結果になります。

スクリーンテクスチャの使用例

スクリーンテクスチャはさまざまな用途に使用できます。 Screen Space Shaders には特別なデモがあり、ダウンロードして見て学ぶことができます。1つの例は、明るさ、コントラスト、彩度を調整するシンプルなシェーダーです:

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

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では描画しようとしているノード内で hint_screen_texture が最初に見つかると、Godot はバックバッファにフルスクリーンコピーを実行します。シェーダーでこれを使用する後続のノードでは、画面がコピーされません。これは非効率的になるためです。3Dでは画面は不透明なジオメトリパスの後、透明なジオメトリパスの前にコピーされるため、透明なオブジェクトはスクリーンテクスチャにキャプチャされません。

その結果、2Dで hint_screen_texture を使用するシェーダーが重なった場合、2番目のシェーダーは最初のシェーダーの結果を使用せず、予期しないビジュアルになります:

../../_images/texscreen_demo1.png

上の画像では、2番目の球体 (右上) は下の最初の球体と同じソースをスクリーンテクスチャに使用しているため、最初の球体は「消える」か、表示されません。

2Dでは、これは BackBufferCopy ノードを介して修正できます。これは両方の球の間でインスタンス化できます。 BackBufferCopyは、画面領域または画面全体を指定することで機能します:

../../_images/texscreen_bbc.png

バックバッファを正しくコピーすると、2つの球体が正しくブレンドされます:

../../_images/texscreen_demo2.png

警告

3Dでは hint_screen_texture を使用するマテリアルはそれ自体が透明であるとみなされ、他のマテリアルの結果として得られるスクリーンテクスチャには表示されません。 hint_screen_texture を持つマテリアルを使用するシーンをインスタンス化する場合は、BackBufferCopyノードを使用する必要があります。

3Dではスクリーンテクスチャが1回しかキャプチャされないため、この特定の問題を解決する柔軟性が低くなります。ただしスクリーンテクスチャを3Dで使用する場合は、透明なオブジェクトはキャプチャされない、またスクリーンテクスチャを使用するオブジェクトの前面にある不透明なオブジェクトもキャプチャされる可能性があります。

オブジェクトと同じ位置にカメラを備えた Viewport を作成し、 Viewport の テクスチャの代わりに Viewport テクスチャを使用することで、スクリーンテクスチャの代わりにバックバッファロジックを3Dで再現できます。

バックバッファロジック

分かりやすくするために、Godotの2Dでバックバッファのコピーロジックがどのように動作するかを次に示します。

  • ノードが hint_screen_texture を使用する場合、そのノードを描画する前に画面全体がバックバッファにコピーされます。これは初回のみ発生します。後続のノードはこれをトリガーしません。

  • 上記の状況が発生する前に BackBufferCopy ノードが処理された場合 (hint_screen_texture が使用されなかった場合でも)、上記の点で説明した動作は発生しません。言い換えれば、画面全体の自動コピーは、ノードで hint_screen_texture が初めて使用され、ツリー順序で以前に BackBufferCopy ノードが見つからなかった (無効になっていない) 場合にのみ発生します。

  • BackBufferCopyは、画面全体または一部領域をコピーできます。 (画面全体ではなく)領域のみに設定され、シェーダーがコピーされた領域にないピクセルを使用する場合、その読み取りの結果は未定義になります (前のフレームからのガベージである可能性が高くなります)。つまりBackBufferCopyを使用して画面の領域をコピーバックし、別の領域からスクリーンテクスチャを読み取ることができます。このような操作は避けてください!

深度テクスチャ

3Dシェーダーの場合、スクリーンの深度バッファにアクセスすることもできます。このために hint_depth_texture ヒントが使用されます。このテクスチャは線形ではありません。逆射影行列を使用して変換する必要があります。

次のコードは、描画中のピクセルの下の3D位置を取得します:

uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;

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, 1.0);
    vec3 pixel_position = upos.xyz / upos.w;
}

法線とラフネスのテクスチャ

注釈

法線とラフネスのテクスチャは、Forward+レンダリングでのみサポートされており、モバイルまたはGL互換ではサポートされていません。

法線ラフネステクスチャを使用して、深度プリパスでレンダリングされたオブジェクトの法線とラフネス値を読み取ることができます。法線は .xyz チャネル (0~1の範囲にマッピング) に保存され、ラフネス値は .w チャネルに保存されます。

uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;

void fragment() {
    float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
    vec3 screen_normal = texture(normal_roughness_texture, SCREEN_UV).xyz;
    screen_normal = screen_normal * 2.0 - 1.0;

スクリーンテクスチャの再定義

スクリーンテクスチャヒント (hint_screen_texturehint_depth_texturehint_normal_roughness_texture) は複数のUniformで使用できます。たとえば異なる繰り返しフラグやフィルターフラグを使用してテクスチャから複数回読み取りたい場合があります。

次の例は、線形フィルタリングを使用してスクリーン空間の法線を読み取りますが、最近傍フィルタリングを使用してスクリーン空間のラフネス値を読み取るシェーダーを示しています。

uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest;
uniform sampler2D normal_roughness_texture2 : hint_normal_roughness_texture, repeat_enable, filter_linear;

void fragment() {
    float screen_roughness = texture(normal_roughness_texture, SCREEN_UV).w;
    vec3 screen_normal = texture(normal_roughness_texture2, SCREEN_UV).xyz;
    screen_normal = screen_normal * 2.0 - 1.0;