高度なポストプロセッシング

はじめに

このチュートリアルでは、Godotでのポストプロセッシングを行うための高度な方法について説明します。特に、深度バッファーを使用するポストプロセッシングシェーダーの作成方法について説明します。ポストプロセッシング全般、特に カスタムポストプロセッシングチュートリアル で概説されているメソッドに既に精通している必要があります。

前のポストプロセッシングチュートリアルでは、シーンを Viewport にレンダリングしてから、ビューポートをメインシーンに対して ViewportContainer を使ってレンダリングしました。この方法の1つの制限は、深度バッファーはSpatialシェーダーでのみ使用可能であり、ビューポートは深度情報を保持しないため、深度バッファーにアクセスできないことです。

フルスクリーン矩形

カスタムポストプロセッシングのチュートリアル で、ビューポートを使用してカスタムポストプロセッシングエフェクトを作成する方法を説明しました。ビューポートを使用することには、主に2つの欠点があります。

  1. 深度バッファにアクセスできません
  2. 後処理シェーダーの効果はエディタに表示されません

深度バッファの使用に関する制限を回避するには、MeshInstanceQuadMesh プリミティブとともに使用します。これにより、Spatialシェーダーを使用して、シーンの深度テクスチャにアクセスできます。次に、頂点シェーダーを使用して、矩形が常に画面を覆うようにし、エディタを含む後処理効果が常に適用されるようにします。

最初に、新しいMeshInstanceを作成し、そのメッシュをQuadMeshに設定します。これにより、(0, 0, 0) の位置を中心とするクワッドが作成され、幅と高さが 1 になります。幅と高さを 2 に設定します。現在、矩形はワールド空間の原点の位置を占めています。ただし、カメラとともに移動して、常に画面全体をカバーするようにします。これを行うには、差分座標空間を介して頂点位置を変換する座標変換をバイパスし、既にクリップ空間にあるかのように頂点を扱います。

頂点シェーダーは、座標がクリップ空間で出力されることを想定しています。これは、画面の左と下の -1 から画面の右上の 1 までの座標です。これが、QuadMeshの高さと幅を 2 にする必要がある理由です。Godot はモデルからビュー空間への変換を処理し、背景の背後にあるクリップ空間に移動するので、Godotの変換の効果を無効にする必要があります。これを行うには、ビルトインの POSITION を目的の位置に設定します。POSITION は、ビルトインの変換をバイパスし、頂点の位置を直接設定します。

shader_type spatial;

void vertex() {
  POSITION = vec4(VERTEX, 1.0);
}

この頂点シェーダーを使用しても、矩形は消えたままです。これは、CPUで実行されるフラスタム(視錐台)カリングによるものです。フラスタムカリングは、カメラマトリックスとメッシュのAABBを使用して、メッシュをGPUに渡す前にメッシュが表示されるかどうかを決定します。 CPUは頂点の処理内容を認識していないため、指定された座標はクリップスペースの位置ではなくワールドの位置を参照していると想定します。矩形がカリングされないようにするには、いくつかのオプションがあります:

  1. QuadMeshをカメラの子として追加し、カメラが常にそれを指すようにする
  2. QuadMeshでGeometryプロパティ extra_cull_margin をできるだけ大きく設定します

2番目のオプションは、エディタでクワッドが表示されることを保証します。一方、最初のオプションは、カメラがカリングマージンの外側に移動しても表示されることを保証します。両方のオプションを使用することもできます。

深度テクスチャ

深度テクスチャから読み取るには、texture() とuniform変数 DEPTH_TEXTURE を使用してテクスチャルックアップを実行します。

float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;

注釈

画面テクスチャへのアクセスと同様に、深度テクスチャへのアクセスは、現在のビューポートから読み取るときにのみ可能です。深度テクスチャは、レンダリングした別のビューポートからアクセスできません。

The values returned by DEPTH_TEXTURE are between 0 and 1 and are nonlinear. When displaying depth directly from the DEPTH_TEXTURE, everything will look almost white unless it is very close. This is because the depth buffer stores objects closer to the camera using more bits than those further, so most of the detail in depth buffer is found close to the camera. In order to make the depth value align with world or model coordinates, we need to linearize the value. When we apply the projection matrix to the vertex position, the z value is made nonlinear, so to linearize it, we multiply it by the inverse of the projection matrix, which in Godot, is accessible with the variable INV_PROJECTION_MATRIX.

まず、画面空間の座標を取得し、それらを正規化されたデバイス座標(NDC)に変換します。 NDCは、クリップ空間座標と同様に、-1 から 1 まで実行されます。x および y 軸に SCREEN_UVz に深度値を使用してNDCを再構築します。

void fragment() {
  float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
}

NDCに INV_PROJECTION_MATRIX を掛けて、NDCを表示スペースに変換します。ビュー空間はカメラに相対的な位置を与えることを思い出してください。そのため、z 値はポイントまでの距離を与えます。

void fragment() {
  ...
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;
}

カメラは負の z 方向を向いているため、位置には負の z 値が設定されます。使用可能な深度値を取得するには、view.z 分の長さを無効(-view.z)にする必要があります。

ワールド位置は、次のコードを使用して深度バッファーから構築できます。ビュー空間からワールド空間に位置を変換するには CAMERA_MATRIX が必要なので、varyingでフラグメントシェーダーに渡す必要があることに注意してください。

varying mat4 CAMERA;

void vertex() {
  CAMERA = CAMERA_MATRIX;
}

void fragment() {
  ...
  vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;
}

最適化

フルスクリーン矩形を使用するのではなく、単一の大きな三角形を使用することでメリットが得られます。この理由は ここ で説明されています。ただし、この利点は非常に小さく、特に複雑なフラグメントシェーダーを実行する場合にのみ有益です。

MeshInstanceのMeshを ArrayMesh に設定します。 ArrayMeshは、頂点、法線、色などの配列からメッシュを簡単に構築できるツールです。

次に、スクリプトをMeshInstanceにアタッチし、次のコードを使用します:

extends MeshInstance

func _ready():
  # Create a single triangle out of vertices:
  var verts = PoolVector3Array()
  verts.append(Vector3(-1.0, -1.0, 0.0))
  verts.append(Vector3(-1.0, 3.0, 0.0))
  verts.append(Vector3(3.0, -1.0, 0.0))

  # Create an array of arrays.
  # This could contain normals, colors, UVs, etc.
  var mesh_array = []
  mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
  mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array

  # Create mesh from mesh_array:
  mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)

注釈

三角形は、正規化されたデバイス座標で指定されます。 NDCは -1 から 1 まで xy の両方の方向に実行されることを思い出してください。これにより、画面の幅が 2 単位、高さが 2 単位になります。画面全体を単一の三角形で覆うには、4 単位の幅と 4 単位の高の、幅と高さを2倍にした三角形を使用します。

上から同じ頂点シェーダーを割り当てると、すべてがまったく同じに見えるはずです。

QuadMeshを使用するよりもArrayMeshを使用する場合の1つの欠点は、シーンが実行されるまで三角形が構築されないため、エディタでArrayMeshが表示されないことです。これを回避するには、モデリングプログラムで単一の三角形メッシュを作成し、代わりにそれをMeshInstanceで使用します。