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
這兩行傳達了兩件事:
以下程式碼是計算著色器。這是編輯器正確匯入著色器檔案所需的特定於 Godot 的提示。
程式碼使用 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()
// Create a local rendering device.
var rd = RenderingServer.CreateLocalRenderingDevice();
之後,我們可以載入新建立的著色器檔案“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)
// Load GLSL shader
var shaderFile = GD.Load<RDShaderFile>("res://compute_example.glsl");
var shaderBytecode = shaderFile.GetSpirV();
var shader = rd.ShaderCreateFromSpirV(shaderBytecode);
警告
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)
// Prepare our data. We use floats in the shader, so we need 32 bit.
var input = new float[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var inputBytes = new byte[input.Length * sizeof(float)];
Buffer.BlockCopy(input, 0, inputBytes, 0, inputBytes.Length);
// 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.StorageBufferCreate((uint)inputBytes.Length, inputBytes);
緩衝區就位後,我們需要告訴渲染裝置使用該緩衝區。為此,我們需要建立一個均勻(就像在普通著色器中一樣)並將其指派給一個均勻集,稍後我們可以將其傳遞給著色器。
# 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
// Create a uniform to assign the buffer to the rendering device
var uniform = new RDUniform
{
UniformType = RenderingDevice.UniformType.StorageBuffer,
Binding = 0
};
uniform.AddId(buffer);
var uniformSet = rd.UniformSetCreate(new Array<RDUniform> { uniform }, shader, 0);
定義計算管線¶
下一步是建立 GPU 可以執行的一組指令。我們需要一個管道和一個計算列表。
計算結果需要執行的步驟是:
建立新專案
開始我們的 GPU 執行的指令列表。
將我們的計算列表綁定到我們的管道
將我們的緩衝區均勻綁定到我們的管道
指定要使用的工作群組數量
編輯器整合
# 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()
// Create a compute pipeline
var pipeline = rd.ComputePipelineCreate(shader);
var computeList = rd.ComputeListBegin();
rd.ComputeListBindComputePipeline(computeList, pipeline);
rd.ComputeListBindUniformSet(computeList, uniformSet, 0);
rd.ComputeListDispatch(computeList, xGroups: 5, yGroups: 1, zGroups: 1);
rd.ComputeListEnd();
請注意,我們將計算著色器分派給 X 軸上的 5 個工作組,以及其他一個工作組。由於我們在 X 軸上有 2 個本地呼叫(在著色器中指定),因此總共將啟動 10 個計算著色器呼叫。如果您讀取或寫入緩衝區範圍之外的索引,則可能會存取著色器控制之外的記憶體或其他變數的一部分,這可能會導致某些硬體出現問題。
執行計算著色器¶
所有這些之後我們就快完成了,但我們仍然需要執行我們的管道。到目前為止,我們只記錄了我們希望 GPU 執行的操作;我們還沒有真正運作著色器程式。
要執行計算著色器,我們需要將管道提交給 GPU 並等待執行完成:
# Submit to GPU and wait for sync
rd.submit()
rd.sync()
// 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)
// Read back the data from the buffers
var outputBytes = rd.BufferGetData(buffer);
var output = new float[input.Length];
Buffer.BlockCopy(outputBytes, 0, output, 0, outputBytes.Length);
GD.Print("Input: ", string.Join(", ", input));
GD.Print("Output: ", string.Join(", ", output));
這樣,您就擁有了開始使用計算著色器所需的一切。
也參考
示範專案儲存庫包含一個「Compute Shader Heightmap demo <https://github.com/godotengine/godot-demo-projects/tree/master/misc/compute_shader_heightmap>`__ 此專案分別在CPU 和GPU 上執行高度圖片圖像生成,它可以讓您比較如何以兩種不同的方式實作類似的演算法(大多數情況下 GPU 實作速度更快)。