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

はじめに

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

フルスクリーン矩形

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

  1. 深度バッファにアクセスできません

  2. ポストプロセッシングシェーダーの効果はエディタに表示されません

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

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

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

shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;

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

注釈

Godot 4.3 より前のバージョンでは、このコードは POSITION = vec4(VERTEX, 1.0); を使用することを推奨していましたが、これはクリップ空間のニアプレーンが 0.0 であると暗黙的に想定していました。このコードは現在間違っており、バージョン 4.3 以降では動作しません。4.3 以降ではニアプレーンが 1.0 である reversed-z 深度バッファを使用しているためです。

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

  1. QuadMeshをカメラの子として追加し、カメラが常にそれを指すようにする

  2. QuadMeshでGeometryプロパティ extra_cull_margin をできるだけ大きく設定します

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

深度テクスチャ

深度テクスチャから読み取るには、まず hint_depth_texture を使用して深度バッファにセットされたテクスチャUniformを作成する必要があります。

uniform sampler2D depth_texture : hint_depth_texture;

一度定義すれば、深度テクスチャは texture() 関数で読み取ることができます。

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

注釈

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

The values returned by depth_texture are between 1.0 and 0.0 (corresponding to the near and far plane, respectively, because of using a "reverse-z" depth buffer) and are nonlinear. When displaying depth directly from the depth_texture, everything will look almost black unless it is very close due to that nonlinearity. 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はVulkan バックエンドでは、 x, y 方向は -1.01.0 まで、 z 方向は 0.01.0 まで実行されます。 x軸とy軸に SCREEN_UV を使用し、z 軸に深度値を使用してNDCを再構築します。

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

注釈

This tutorial assumes the use of the Forward+ or Mobile renderers, which both use Vulkan NDCs with a Z-range of [0.0, 1.0]. In contrast, the Compatibility renderer uses OpenGL NDCs with a Z-range of [-1.0, 1.0]. For the Compatibility renderer, replace the NDC calculation with this instead:

vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;

You can also use the CURRENT_RENDERER and RENDERER_COMPATIBILITY built-in defines for a shader that will work in all renderers:

#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
#endif

NDCに INV_PROJECTION_MATRIX を掛けてビュー空間へ変換します。ビュー空間はカメラからの相対的な位置になるため、 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) する必要があります。

The world position can be constructed from the depth buffer using the following code, using the INV_VIEW_MATRIX to transform the position from view space into world space.

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

Example shader

Once we add a line to output to ALBEDO, we have a complete shader that looks something like this. This shader lets you visualize the linear depth or world space coordinates, depending on which line is commented out.

shader_type spatial;
// Prevent the quad from being affected by lighting and fog. This also improves performance.
render_mode unshaded, fog_disabled;

uniform sampler2D depth_texture : hint_depth_texture;

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

void fragment() {
  float depth = texture(depth_texture, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;

  vec4 world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;

  // Visualize linear depth
  ALBEDO.rgb = vec3(fract(linear_depth));

  // Visualize world coordinates
  //ALBEDO.rgb = fract(world_position).xyz;
}

最適化

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

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

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

extends MeshInstance3D

func _ready():
  # Create a single triangle out of vertices:
  var verts = PackedVector3Array()
  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はX方向とY方向の両方で -1.01.0 まで実行されることを思い出してください。これにより画面の幅が 2 単位、高さが 2 単位になります。画面全体を1つの三角形で覆うには、幅が 4 単位、高さが 4 単位の、幅と高さを2倍にした三角形を使用します。

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

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