Up to date

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

高級後期處理

前言

本教學描述了一種在 Godot 中進行後期處理的高級方法。值得注意的是,它將解釋如何編寫使用深度緩衝區的後期處理著色器。您應該已經熟悉後期處理,特別是使用:ref:`自訂後期處理教學 <doc_custom_postprocessing>`中介紹的方法。

在前面的後期處理教學中,我們將場景渲染到了 Viewport 中,然後將這個 Viewport 在 ViewportContainer 中渲染到主場景。這個方法存在一個局限,我們無法存取深度緩衝區,因為深度緩衝區只在空間著色器中可用,Viewport 並不維護深度資訊。

全屏四邊形

在:ref:`自訂後期處理教學<doc_custom_postprocessing>`中,我們介紹了如何使用 Viewport 來製作自訂的後期處理特效。使用 Viewport 有兩個主要的缺點:

  1. 無法存取深度緩衝區

  2. 在編輯器中看不到後期處理著色器的效果

要解決使用深度緩衝區的限制,請使用 MeshInstance 並設定 QuadMesh 像素。這樣我們就可以使用空間著色器,並且可以存取該場景的深度紋理。接下來,請使用頂點著色器讓這個四邊形始終覆蓋螢幕,以便始終應用後期處理效果,包括在編輯器中。

首先,新建一個 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 上完成的。視錐剔除使用相機矩陣和 Mesh 的 AABB 來確定 Mesh 是否可見,然後再傳遞給 GPU。CPU 不知道我們對頂點做了什麼,所以它認為指定的座標指的是世界座標,而不是裁剪空間的座標,這就導致了 Godot 在我們旋轉、離開場景中心時對四邊形進行剔除。為了防止四邊形被剔除,有這麼幾個選項:

  1. 將 QuadMesh 作為子節點新增到相機,這樣相機就會始終指向它

  2. 在 QuadMesh 中將幾何屬性 extra_cull_margin 設定得盡可能大

第二個選項會確保四邊形在編輯器中可見,而第一個選項能夠保證即使相機移出剔除邊緣也它仍可見。您也可以同時使用這兩個選項。

深度紋理

要讀取深度紋理,我們首先需要使用「hint_depth_texture」來建立一個設定為深度緩衝區的均勻紋理。

uniform sampler2D depth_texture : source_color, hint_depth_texture;

定義後,可以使用“texture()”函式讀取深度紋理。

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

備註

與存取螢幕紋理類似,存取深度紋理只有在從目前視口讀取時才能進行。深度紋理不能從你已經渲染的另一個視口中存取。

DEPTH_TEXTURE 返回的值介於 01 之間,並且是非線性的。當直接從 DEPTH_TEXTURE 顯示深度時,除非非常接近,否則一切都會看起來幾乎是白色的。這是因為深度緩衝區會使用更多的位元來儲存更靠近相機的物件,因此深度緩衝區中的大部分細節都靠近相機。為了使深度值與世界或模型座標對齊,我們需要將值線性化,當我們將投影矩陣應用於頂點位置時,Z 值是非線性的,所以為了將其線性化,我們將它乘以投影矩陣的逆矩陣,在 Godot 中可以用變數 INV_PROJECTION_MATRIX 存取。

首先, 取螢幕空間座標並將其轉換為正規化裝置座標(NDC).NDC從 -11 , 類似於裁剪空間座標. 使用 SCREEN_UV 來重建NDC的 xy 軸, 以及 z 的深度值.

備註

本教學假設使用 Vulkan 渲染器,它使用 Z 範圍為「[0.0, 1.0]」的 NDC。相較之下,OpenGL 使用 Z 範圍為「[-1.0, 1.0]」的 NDC。

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 .

世界座標可以通過以下程式碼從深度緩衝區建構. 注意 CAMERA_MATRIX 需要將座標從視圖空間轉換到世界空間, 所以它需要以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;
}

優化

您可以使用單個大三角形而不是使用全屏四邊形. 解釋的原因在 這裡 . 但是, 這種好處非常小, 只有在運作特別複雜的片段著色器時才有用.

將MeshInstance中的Mesh設定為 ArrayMesh. ArrayMesh是一個工具, 允許您從頂點, 法線, 顏色等方便地從陣列建構網格.

現在, 將腳本附加到MeshInstance並使用以下程式碼:

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在 xy 方向都從 -11 運作. 這使得螢幕 2 單位寬, 2 單位高. 為了用一個三角形覆蓋整個螢幕, 使用一個 4 單位寬和 4 單位高的三角形, 高度和寬度加倍.

從上面分配相同的頂點著色器, 所有內容應該看起來完全相同.

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.