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

はじめに

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

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

SCREEN_TEXTURE ビルトインテクスチャ

Godot :ref`doc_shading_language` には、"SCREEN_TEXTURE" (および3Dの場合は深さの "DEPTH_TEXTURE")という特別なテクスチャがあります。引数として画面のUVを取り、色付きのvec3 RGBを返します。特別なビルトイン変数: SCREEN_UVを使用して、現在のフラグメントのUVを取得できます。その結果、この単純な2Dフラグメントシェーダーは次のようになります:

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

背後にあるものを表示するだけなので、不可視のオブジェクトになります。

textureLodを使用する必要がある理由は、Godotが画面のチャンクをコピーバックするときに、ミップマップに対して効率的な分離可能なガウスぼかしも行うためです。

これにより、画面から読み取るだけでなく、異なる量のブラーを使用して読み取ることができます。

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ビルトインは、描画しようとしているノードで最初に見つかったときに、バックバッファへのフルスクリーンコピーを実行します。シェーダーでそれを使用する後続のノードでは、画面がコピーされません。これは、非効率的であるためです。

その結果、SCREEN_TEXTUREを使用するシェーダーがオーバーラップする場合、2番目のシェーダーは最初のシェーダーの結果を使用せず、予期しないビジュアルになります:

../../_images/texscreen_demo1.png

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

3Dでは、不透明なレンダリングが完了するとコピーが行われるため、これは避けられません。

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

../../_images/texscreen_bbc.png

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

../../_images/texscreen_demo2.png

バックバッファロジック

したがって、わかりやすくするために、Godotでのバックバッファーコピーロジックの動作を次に示します:

  • ノードがSCREEN_TEXTUREを使用する場合、そのノードを描画する前に画面全体がバックバッファーにコピーされます。これは最初にのみ発生します。後続のノードはこれをトリガーしません。
  • 上記の状況の前にBackBufferCopyノードが処理された場合(SCREEN_TEXTUREが使用されなかった場合でも)、上記の状況で説明した動作は発生しません。言い換えると、SCREEN_TEXTUREがノードで初めて使用され、BackBufferCopyノード(無効ではない)がツリー順で以前に見つからなかった場合にのみ、画面全体の自動コピーが発生します。
  • BackBufferCopyは、画面全体または領域のいずれかをコピーできます。(画面全体ではなく)領域のみに設定され、シェーダーがコピーされた領域にないピクセルを使用する場合、その読み取りの結果は未定義です(ほとんどの場合、前のフレームからのゴミ)。つまり、BackBufferCopyを使用して画面の領域をコピーバックし、別の領域でSCREEN_TEXTUREを使用することができてしまいます。この動作は避けてください!

DEPTH_TEXTURE

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

次のコードは、描画中のピクセルの下の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;
}