Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

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

はじめに

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

In the previous post-processing tutorial, we rendered the scene to a Viewport and then rendered the Viewport in a SubViewportContainer to the main scene. One limitation of this method is that we could not access the depth buffer because the depth buffer is only available in shaders and Viewports do not maintain depth information.

フルスクリーン矩形

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

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

  2. 後処理シェーダーの効果はエディタに表示されません

To get around the limitation on using the depth buffer, use a MeshInstance3D with a QuadMesh primitive. This allows us to use a shader and to access the depth texture of the scene. Next, use a vertex shader to make the quad cover the screen at all times so that the post-processing effect will be applied at all times, including in the editor.

First, create a new MeshInstance3D and set its mesh to a QuadMesh. This creates a quad centered at position (0, 0, 0) with a width and height of 1. Set the width and height to 2 and enable Flip Faces. Right now, the quad occupies a position in world space at the origin. However, we want it to move with the camera so that it always covers the entire screen. To do this, we will bypass the coordinate transforms that translate the vertex positions through the difference coordinate spaces and treat the vertices as if they were already in clip space.

頂点シェーダーは、座標がクリップ空間で出力されることを想定しています。これは、画面の左と下の -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番目のオプションは、エディタでクワッドが表示されることを保証します。一方、最初のオプションは、カメラがカリングマージンの外側に移動しても表示されることを保証します。両方のオプションを使用することもできます。

深度テクスチャ

To read from the depth texture, we first need to create a texture uniform set to the depth buffer by using hint_depth_texture.

uniform sampler2D depth_texture : source_color, hint_depth_texture;

Once defined, the depth texture can be read with the texture() function.

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

注釈

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

The values returned by depth_texture are between 0.0 and 1.0 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.

Firstly, take the screen space coordinates and transform them into normalized device coordinates (NDC). NDC run -1.0 to 1.0 in x and y directions and from 0.0 to 1.0 in the z direction when using the Vulkan backend. Reconstruct the NDC using SCREEN_UV for the x and y axis, and the depth value for z.

注釈

This tutorial assumes the use of the Vulkan renderer, which uses NDCs with a Z-range of [0.0, 1.0]. In contrast, OpenGL uses NDCs with a Z-range of [-1.0, 1.0].

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

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)にする必要があります。

The world position can be constructed from the depth buffer using the following code. Note that the INV_VIEW_MATRIX is needed to transform the position from view space into world space, so it needs to be passed to the fragment shader with a varying.

varying mat4 CAMERA;

void vertex() {
  CAMERA = INV_VIEW_MATRIX;
}

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

最適化

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

Set the Mesh in the MeshInstance3D to an ArrayMesh. An ArrayMesh is a tool that allows you to easily construct a Mesh from Arrays for vertices, normals, colors, etc.

Now, attach a script to the MeshInstance3D and use the following code:

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)

注釈

The triangle is specified in normalized device coordinates. Recall, NDC run from -1.0 to 1.0 in both the x and y directions. This makes the screen 2 units wide and 2 units tall. In order to cover the entire screen with a single triangle, use a triangle that is 4 units wide and 4 units tall, double its height and width.

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

The one drawback to using an ArrayMesh over using a QuadMesh is that the ArrayMesh is not visible in the editor because the triangle is not constructed until the scene is run. To get around that, construct a single triangle Mesh in a modeling program and use that in the MeshInstance3D instead.