Up to date

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

使用計算著色器

本教學將引導您完成建立最小計算著色器的過程。但首先,我們先了解計算著色器的背景知識以及它們如何與 Godot 搭配使用。

備註

本教學假設您通常熟悉著色器。如果您是著色器新手,請在繼續本教學之前閱讀 匯出簡介您的第一個著色器

計算著色器是一種特殊型別的著色器程式,面向通用編程。換句話說,它們比頂點著色器和片段著色器更靈活,因為它們沒有固定的用途(即轉換頂點或將顏色寫入圖像)。與片段著色器和頂點著色器不同,計算著色器在幕後幾乎沒有進行任何操作。您編寫的程式碼是 GPU 運作的程式碼,除此之外幾乎沒有其他程式碼。這使得它們成為一個非常有用的工具,可以將繁重的計算解除安裝到 GPU。

現在讓我們開始建立一個簡短的計算著色器。

首先,在您選擇的**外部**文字編輯器中,在專案資料夾中建立一個名為「compute_example.glsl」的新檔案。當您在 Godot 中編寫計算著色器時,您可以直接在 GLSL 中編寫它們。 Godot 著色器語言是基於 GLSL。如果您熟悉 Godot 中的普通著色器,那麼下面的語法看起來有些熟悉。

備註

計算著色器只能在基於 RenderingDevice 的渲染器(Forward+ 或 Mobile 渲染器)中使用。若要按照本教學進行操作,請確保您使用的是 Forward+ 或 Mobile 渲染器。其設定位於編輯器的右上角。

請注意,計算著色器在行動裝置上的支援通常很差(由於驅動程式錯誤),即使它們在技術上得到支援。

我們把它調成藍色:

#[compute]
#version 450

// Invocations in the (x, y, z) dimension
layout(local_size_x = 2, local_size_y = 1, local_size_z = 1) in;

// A binding to the buffer we create in our script
layout(set = 0, binding = 0, std430) restrict buffer MyDataBuffer {
    float data[];
}
my_data_buffer;

// The code we want to execute in each invocation
void main() {
    // gl_GlobalInvocationID.x uniquely identifies this invocation across all work groups
    my_data_buffer.data[gl_GlobalInvocationID.x] *= 2.0;
}

此程式碼採用浮點數陣列,將每個元素乘以 2,並將結果儲存回緩衝區陣列中。現在讓我們逐行看一下。

#[compute]
#version 450

這兩行傳達了兩件事:

  1. 以下程式碼是計算著色器。這是編輯器正確匯入著色器檔案所需的特定於 Godot 的提示。

  2. 程式碼使用 GLSL 版本 450。

您永遠不必為自訂計算著色器更改這兩行。

// Invocations in the (x, y, z) dimension
layout(local_size_x = 2, local_size_y = 1, local_size_z = 1) in;

接下來,我們傳達每個工作組中要使用的呼叫次數。呼叫是在同一工作群組中執行的著色器的實例。當我們從 CPU啟動計算著色器時,我們會告訴它要運作多少個工作組。工作小組彼此並行運作。執行一個工作群組時,您無法存取另一工作群組中的資訊。但是,同一工作群組中的呼叫可以對其他呼叫具有一些有限的存取權。

將工作群組和呼叫視為一個巨大的巢狀“for”循環。

for (int x = 0; x < workgroup_size_x; x++) {
  for (int y = 0; y < workgroup_size_y; y++) {
     for (int z = 0; z < workgroup_size_z; z++) {
        // Each workgroup runs independently and in parallel.
        for (int local_x = 0; local_x < invocation_size_x; local_x++) {
           for (int local_y = 0; local_y < invocation_size_y; local_y++) {
              for (int local_z = 0; local_z < invocation_size_z; local_z++) {
                 // Compute shader runs here.
              }
           }
        }
     }
  }
}

工作小組和呼叫是一個高級主題。現在,請記住,我們將為每個工作組執行兩次呼叫。

// A binding to the buffer we create in our script
layout(set = 0, binding = 0, std430) restrict buffer MyDataBuffer {
    float data[];
}
my_data_buffer;

在這裡,我們提供有關計算著色器將存取的記憶體的資訊。 “layout” 屬性允許我們告訴著色器在哪裡尋找緩衝區,稍後我們需要從 CPU 端配對這些“set” 和“binding” 位置。

“restrict” 關鍵字告訴著色器該緩衝區只能從該著色器中的一個位置存取。換句話說,我們不會將此緩衝區綁定到另一個「set」或「binding」索引中。這很重要,因為它允許著色器編譯器最佳化著色器程式碼。盡可能使用“restrict”。

這是一個*未確定大小的*緩衝區,這表示它可以是任意大小。所以我們需要小心,不要從大於緩衝區大小的索引中讀取資料。

// The code we want to execute in each invocation
void main() {
    // gl_GlobalInvocationID.x uniquely identifies this invocation across all work groups
    my_data_buffer.data[gl_GlobalInvocationID.x] *= 2.0;
}

最後,我們編寫“main”函式,這是所有邏輯發生的地方。我們使用內建變數「gl_GlobalInitationID」來存取儲存緩衝區中的位置。 “gl_GlobalInitationID” 為您提供目前呼叫的全域唯一 ID。

要繼續,請將上面的程式碼寫入新建立的“compute_example.glsl”檔案中。

建立局部 RenderingDevice

為了與計算著色器互動並執行計算著色器,我們需要一個腳本。使用您選擇的語言建立新腳本並將其附加到場景中的任何節點。

現在要執行我們的著色器,我們需要一個本地 class_RenderingDevice ,可以使用 class_RenderingServer 來建立它:

# Create a local rendering device.
var rd := RenderingServer.create_local_rendering_device()

之後,我們可以載入新建立的著色器檔案“compute_example.glsl”並使用以下指令建立它的預編譯版本:

# Load GLSL shader
var shader_file := load("res://compute_example.glsl")
var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
var shader := rd.shader_create_from_spirv(shader_spirv)

警告

Local RenderingDevices cannot be debugged using tools such as RenderDoc.

提供輸入資料

您可能還記得,我們希望將輸入陣列傳遞給著色器,將每個元素乘以 2 並獲得結果。

我們需要建立一個緩衝區來將值傳遞給計算著色器。我們正在處理浮點陣列,因此我們將在本範例中使用儲存緩衝區。儲存緩衝區採用位元組陣列,並允許 CPU 與 GPU 之間傳輸資料。

因此,讓我們初始化一個浮點陣列並建立一個儲存緩衝區:

# Prepare our data. We use floats in the shader, so we need 32 bit.
var input := PackedFloat32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
var input_bytes := input.to_byte_array()

# Create a storage buffer that can hold our float values.
# Each float has 4 bytes (32 bit) so 10 x 4 = 40 bytes
var buffer := rd.storage_buffer_create(input_bytes.size(), input_bytes)

緩衝區就位後,我們需要告訴渲染裝置使用該緩衝區。為此,我們需要建立一個均勻(就像在普通著色器中一樣)並將其指派給一個均勻集,稍後我們可以將其傳遞給著色器。

# Create a uniform to assign the buffer to the rendering device
var uniform := RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform.binding = 0 # this needs to match the "binding" in our shader file
uniform.add_id(buffer)
var uniform_set := rd.uniform_set_create([uniform], shader, 0) # the last parameter (the 0) needs to match the "set" in our shader file

定義計算管線

下一步是建立 GPU 可以執行的一組指令。我們需要一個管道和一個計算列表。

計算結果需要執行的步驟是:

  1. 建立新專案

  2. 開始我們的 GPU 執行的指令列表。

  3. 將我們的計算列表綁定到我們的管道

  4. 將我們的緩衝區均勻綁定到我們的管道

  5. 指定要使用的工作群組數量

  6. 編輯器整合

# Create a compute pipeline
var pipeline := rd.compute_pipeline_create(shader)
var compute_list := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
rd.compute_list_dispatch(compute_list, 5, 1, 1)
rd.compute_list_end()

請注意,我們將計算著色器分派給 X 軸上的 5 個工作組,以及其他一個工作組。由於我們在 X 軸上有 2 個本地呼叫(在著色器中指定),因此總共將啟動 10 個計算著色器呼叫。如果您讀取或寫入緩衝區範圍之外的索引,則可能會存取著色器控制之外的記憶體或其他變數的一部分,這可能會導致某些硬體出現問題。

執行計算著色器

所有這些之後我們就快完成了,但我們仍然需要執行我們的管道。到目前為止,我們只記錄了我們希望 GPU 執行的操作;我們還沒有真正運作著色器程式。

要執行計算著色器,我們需要將管道提交給 GPU 並等待執行完成:

# Submit to GPU and wait for sync
rd.submit()
rd.sync()

理想情況下,您不會立即呼叫「sync()」來同步 RenderingDevice,因為這會導致 CPU 等待 GPU 完成工作。在我們的範例中,我們立即同步,因為我們希望資料可供立即讀取。一般來說,您需要在同步之前等待「至少」2 或 3 影格,以便 GPU 能夠與 CPU 並行運作。

警告

由於 Windows 觸發 TDR(逾時偵測和復原),長時間運算可能會導致 Windows 圖形驅動程式「當機」。這是一種在圖形驅動程式沒有任何活動(通常為 5 到 10 秒)一段時間後重新初始化圖形驅動程式的機制。

根據計算著色器執行所需的持續時間,您可能需要將其拆分為多個調度,以減少每個調度所需的時間並減少觸發 TDR 的機會。鑑於 TDR 與時間相關,與較快的 GPU 相比,在運作給定計算著色器時,較慢的 GPU 可能更容易出現 TDR。

測試結果

您可能已經注意到,在範例著色器中,我們修改了儲存緩衝區的內容。換句話說,著色器從我們的陣列中讀取資料並將資料再次儲存在同一陣列中,因此我們的結果已經存在。讓我們檢索資料並將結果列印到控制台。

# Read back the data from the buffer
var output_bytes := rd.buffer_get_data(buffer)
var output := output_bytes.to_float32_array()
print("Input: ", input)
print("Output: ", output)

這樣,您就擁有了開始使用計算著色器所需的一切。

也參考

示範專案儲存庫包含一個「Compute Shader Heightmap demo <https://github.com/godotengine/godot-demo-projects/tree/master/misc/compute_shader_heightmap>`__ 此專案分別在CPU 和GPU 上執行高度圖片圖像生成,它可以讓您比較如何以兩種不同的方式實作類似的演算法(大多數情況下 GPU 實作速度更快)。