WebGPU 是一種全新的現(xiàn)代 API,用于在 Web 應(yīng)用程序中訪問 GPU 的功能。
在 WebGPU 之前,有 WebGL,它提供了 WebGPU 功能的子集。 它啟用了新一類豐富的網(wǎng)絡(luò)內(nèi)容,開發(fā)人員用它構(gòu)建了令人驚嘆的東西。 然而,它基于 2007 年發(fā)布的 OpenGL ES 2.0 API,而該 API 又基于更舊的 OpenGL API。 GPU 在那段時間發(fā)生了顯著的發(fā)展,用于與其交互的本機 API 也隨著 Direct3D 12、Metal 和 Vulkan 的發(fā)展而發(fā)展。
推薦:用 NSDT設(shè)計器 快速搭建可編程3D場景。
WebGPU 將這些現(xiàn)代 API 的進步帶到了 Web 平臺。 它專注于以跨平臺的方式啟用 GPU 功能,同時提供一個在網(wǎng)絡(luò)上感覺自然的 API,并且比它所構(gòu)建的一些本機 API 更簡潔。
GPU 通常與渲染快速、詳細的圖形聯(lián)系在一起,WebGPU 也不例外。 它具有支持當今桌面和移動 GPU 上許多最流行的渲染技術(shù)所需的功能,并為未來隨著硬件功能的不斷發(fā)展添加新功能提供了途徑。
除了渲染之外,WebGPU 還可以釋放 GPU 執(zhí)行通用、高度并行工作負載的潛力。 這些計算著色器可以獨立使用,無需任何渲染組件,也可以作為渲染管道的緊密集成部分。
在今天的 Codelab 中,我們將學(xué)習(xí)如何利用 WebGPU 的渲染和計算功能來創(chuàng)建一個簡單的入門項目!
1、初始化WebGPU
如果你只想使用 WebGPU 進行計算,則無需在屏幕上顯示任何內(nèi)容即可使用 WebGPU。 但是,如果你想要渲染任何內(nèi)容,就像我們將在 Codelab 中所做的那樣,你需要一個畫布。 所以這是一個很好的起點!
1.1 從canvas開始
創(chuàng)建一個新的 HTML 文檔,其中包含一個 元素,以及一個用于查詢 canvas 元素的
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU Life</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
// Your WebGPU code will begin here!
</script>
</body>
</html>
1.2 請求GPU設(shè)備
現(xiàn)在可以進入 WebGPU 了! 首先,你應(yīng)該考慮到像 WebGPU 這樣的 API 可能需要一段時間才能在整個 Web 生態(tài)系統(tǒng)中傳播。 因此,第一個預(yù)防措施是檢查用戶的瀏覽器是否可以使用 WebGPU。
要檢查充當 WebGPU 入口點的 navigator.gpu 對象是否存在,請?zhí)砑右韵麓a:
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
理想情況下,如果 WebGPU 不可用,你希望通過讓頁面回退到不使用 WebGPU 的模式來通知用戶。 (也許它可以使用 WebGL 來代替?)不過,出于本 Codelab 的目的,我們只需拋出一個錯誤來阻止代碼進一步執(zhí)行。
一旦知道瀏覽器支持 WebGPU,為應(yīng)用程序初始化 WebGPU 的第一步就是請求 GPUAdapter。 可以將適配器視為設(shè)備中特定 GPU 硬件的 WebGPU 表示。
要獲取適配器,請使用 navigator.gpu.requestAdapter() 方法。 它返回一個承諾,因此用await 調(diào)用它是最方便的。
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No appropriate GPUAdapter found.");
}
如果找不到合適的適配器,則返回的適配器值可能為 null,因此你需要處理這種可能性。 如果用戶的瀏覽器支持 WebGPU,但他們的 GPU 硬件不具備使用 WebGPU 所需的所有功能,則可能會發(fā)生這種情況。
大多數(shù)時候,只需讓瀏覽器選擇一個默認適配器就可以了,就像你在此處所做的那樣,但是對于更高級的需求,可以將參數(shù)傳遞給 requestAdapter() 來指定是要使用低功耗還是高功耗 - 具有多個 GPU 的設(shè)備(如某些筆記本電腦)上的性能硬件。
擁有適配器后,開始使用 GPU 之前的最后一步是請求 GPUDevice。 設(shè)備是與 GPU 進行大多數(shù)交互的主要接口。
通過調(diào)用adapter.requestDevice()獲取設(shè)備,它也會返回一個promise。
const device = await adapter.requestDevice();
與 requestAdapter() 一樣,可以在此處傳遞一些選項以實現(xiàn)更高級的用途,例如啟用特定的硬件功能或請求更高的限制,但對于本文的目的,默認值工作得很好。
1.3 配置canvas
現(xiàn)在已經(jīng)有了一個設(shè)備,如果想使用它來顯示頁面上的任何內(nèi)容,還需要做一件事:將畫布配置為與剛剛創(chuàng)建的設(shè)備一起使用。
為此,首先通過調(diào)用 canvas.getContext(“webgpu”) 從畫布請求 GPUCanvasContext。 這與您用來初始化 Canvas 2D 或 WebGL 上下文的調(diào)用相同,分別使用 2d 和 webgl 上下文類型。然后,它返回的上下文必須使用 configure() 方法與設(shè)備關(guān)聯(lián),例如 :
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
這里可以傳遞一些選項,但最重要的是你要使用上下文的設(shè)備和格式,即上下文應(yīng)使用的紋理格式。
紋理是 WebGPU 用于存儲圖像數(shù)據(jù)的對象,每個紋理都有一種格式,可以讓 GPU 了解數(shù)據(jù)在內(nèi)存中的布局方式。 紋理內(nèi)存工作原理的詳細信息超出了本 Codelab 的范圍。 需要了解的重要一點是,canvas上下文為你的代碼提供了要繪制的紋理,并且你使用的格式可能會影響畫布顯示這些圖像的效率。 不同類型的設(shè)備在使用不同的紋理格式時性能最佳,如果你不使用設(shè)備的首選格式,則可能會導(dǎo)致在圖像作為頁面的一部分顯示之前在幕后發(fā)生額外的內(nèi)存復(fù)制。
幸運的是,你不必太擔(dān)心這些,因為 WebGPU 會告訴你畫布使用哪種格式! 幾乎在所有情況下,你都希望傳遞通過調(diào)用 navigator.gpu.getPreferredCanvasFormat() 返回的值,如上所示。
1.4 清除canvas
現(xiàn)在我們已經(jīng)擁有了設(shè)備并且已經(jīng)使用它配置了畫布,可以開始使用該設(shè)備來更改畫布的內(nèi)容。 首先,用純色清除它。
為了做到這一點(或者 WebGPU 中的幾乎任何其他事情),需要向 GPU 提供一些命令,指示它做什么。
為此,讓設(shè)備創(chuàng)建一個 GPUCommandEncoder,它提供用于記錄 GPU 命令的接口。
const encoder = device.createCommandEncoder();
我們想要發(fā)送到 GPU 的命令與渲染相關(guān)(在本例中為清除畫布),因此下一步是使用編碼器開始渲染通道。
渲染通道是指 WebGPU 中的所有繪圖操作發(fā)生時。 每個都以 beginRenderPass() 調(diào)用開始,該調(diào)用定義接收所執(zhí)行的任何繪圖命令的輸出的紋理。 更高級的用途可以提供多種紋理(稱為附件),具有各種用途,例如存儲渲染幾何體的深度或提供抗鋸齒。 然而,對于這個應(yīng)用程序,我們只需要一個。
通過調(diào)用 context.getCurrentTexture() 從之前創(chuàng)建的畫布上下文中獲取紋理,該紋理返回一個像素寬度和高度與畫布的寬度和高度屬性以及調(diào)用 context.configure() 時指定的格式相匹配的紋理。
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
storeOp: "store",
}]
});
紋理作為 colorAttachment 的 view 屬性給出。 渲染通道要求你提供 GPUTextureView 而不是 GPUTexture,它告訴它要渲染到紋理的哪些部分。 這僅對于更高級的用例才真正重要,因此在這里我們調(diào)用 createView() 時不帶紋理參數(shù),表明希望渲染通道使用整個紋理。
還必須指定渲染通道在開始和結(jié)束時對紋理執(zhí)行的操作:
- loadOp 值為“clear”表示我們希望在渲染通道開始時清除紋理。
- storeOp 值為“store”表示渲染通道完成后,我們希望將渲染通道期間完成的任何繪制的結(jié)果保存到紋理中。
一旦渲染輪開始,就什么都不做! 最起碼到現(xiàn)在。 使用 loadOp: “clear” 啟動渲染通道的行為足以清除紋理視圖和畫布。
通過在 beginRenderPass() 之后立即添加以下調(diào)用來結(jié)束渲染輪:
pass.end();
重要的是要知道,僅僅進行這些調(diào)用并不會導(dǎo)致 GPU 實際上執(zhí)行任何操作。 它們只是記錄命令供 GPU 稍后執(zhí)行。
為了創(chuàng)建 GPUCommandBuffer,請在命令編碼器上調(diào)用 finish()。 命令緩沖區(qū)是記錄命令的不透明句柄。
const commandBuffer = encoder.finish();
使用GPUDevice的隊列將命令緩沖區(qū)提交給GPU。 隊列執(zhí)行所有 GPU 命令,確保它們的執(zhí)行有序且正確同步。 隊列的submit()方法接受一組命令緩沖區(qū),盡管在本例中只有一個。
device.queue.submit([commandBuffer]);
一旦提交命令緩沖區(qū),就無法再次使用它,因此無需保留它。 如果要提交更多命令,則需要構(gòu)建另一個命令緩沖區(qū)。 這就是為什么這兩個步驟合二為一的情況相當常見,正如本 Codelab 的示例頁面中所做的那樣:
// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);
將命令提交給 GPU 后,讓 JavaScript 將控制權(quán)返回給瀏覽器。 此時,瀏覽器會發(fā)現(xiàn)我們已更改上下文的當前紋理,并更新畫布以將該紋理顯示為圖像。 如果之后想再次更新畫布內(nèi)容,則需要記錄并提交新的命令緩沖區(qū),再次調(diào)用 context.getCurrentTexture() 來獲取渲染通道的新紋理。
重新加載頁面。 請注意,畫布充滿了黑色。 恭喜! 這意味著我們已經(jīng)成功創(chuàng)建了第一個 WebGPU 應(yīng)用程序。
1.5 選擇一種顏色
但說實話,黑色方塊很無聊。 因此,在進入下一部分之前請花點時間對其進行個性化設(shè)置。
在 device.beginRenderPass() 調(diào)用中,向 colorAttachment 添加一個帶有clearValue 的新行,如下所示:
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1 }, // New line
storeOp: "store",
}],
});
clearValue 指示渲染通道在通道開始時執(zhí)行清除操作時應(yīng)使用哪種顏色。 傳遞給它的字典包含四個值:r 代表紅色,g 代表綠色,b 代表藍色,a 代表 alpha(透明度)。 每個值的范圍為 0 到 1,它們一起描述該顏色通道的值。 例如:
- { r: 1, g: 0, b: 0, a: 1 } 為亮紅色。
- { r: 1, g: 0, b: 1, a: 1 } 為亮紫色。
- {r:0,g:0.3,b:0,a:1}為深綠色。
- {r:0.5,g:0.5,b:0.5,a:1}是中灰色。
- { r: 0, g: 0, b: 0, a: 0 } 是默認的透明黑色。
此 Codelab 中的示例代碼和屏幕截圖使用深藍色,但你可以隨意選擇想要的任何顏色!
選擇顏色后,重新加載頁面。 應(yīng)該在畫布中看到我們選擇的顏色。
2、繪制幾何圖形
在本節(jié)結(jié)束時,我們的應(yīng)用程序?qū)⒃诋嫴忌侠L制一些簡單的幾何圖形:彩色正方形。 現(xiàn)在請注意,對于如此簡單的輸出來說,看起來需要做很多工作,但這是因為 WebGPU 旨在非常有效地渲染大量幾何圖形。 這種效率的一個副作用是,做相對簡單的事情可能會感覺異常困難,但如果轉(zhuǎn)向像 WebGPU 這樣的 API,這就是我們的期望 - 想做一些更復(fù)雜的事情。
2.1 了解 GPU 如何繪圖
在進行更多代碼更改之前,有必要對 GPU 如何創(chuàng)建我們在屏幕上看到的形狀進行快速、簡化、高級概述。 (如果你已經(jīng)熟悉 GPU 渲染工作原理的基礎(chǔ)知識,請隨意跳到定義頂點部分。)
與 Canvas 2D 等具有大量形狀和選項可供使用的 API 不同, GPU 實際上只處理幾種不同類型的形狀(或 WebGPU 所指的圖元):點、線和三角形 。 出于本 Codelab 的目的,我們將僅使用三角形。
GPU 幾乎專門處理三角形,因為三角形具有許多良好的數(shù)學(xué)屬性,使它們易于以可預(yù)測且高效的方式進行處理。 幾乎所有用 GPU 繪制的東西都需要先被分割成三角形,然后 GPU 才能繪制它,并且這些三角形必須由它們的角點定義。
這些點或頂點以 X、Y 和(對于 3D 內(nèi)容)Z 值的形式給出,這些值定義了由 WebGPU 或類似 API 定義的笛卡爾坐標系上的點。 坐標系的結(jié)構(gòu)最容易考慮它與頁面上畫布的關(guān)系。 無論畫布有多寬或多高,左邊緣始終位于 X 軸上的 -1 處,右邊緣始終位于 X 軸上的 +1 處。 同樣,Y 軸上的底部邊緣始終為 -1,Y 軸上的頂部邊緣始終為 +1。 這意味著 (0, 0) 始終是畫布的中心,(-1, -1) 始終是左下角,(1, 1) 始終是右上角。 這稱為剪輯空間(Clip Space)。
頂點最初很少在此坐標系中定義,因此 GPU 依靠稱為頂點著色器(Vertex Shader)的小程序來執(zhí)行將頂點轉(zhuǎn)換為剪輯空間所需的任何數(shù)學(xué)運算,以及繪制頂點所需的任何其他計算。 例如,著色器可以應(yīng)用一些動畫或計算從頂點到光源的方向。 這些著色器由WebGPU 開發(fā)人員(我們)編寫,它們提供了對 GPU 工作方式的大量控制。
從那里,GPU 獲取由這些變換后的頂點組成的所有三角形,并確定需要屏幕上的哪些像素來繪制它們。 然后它運行我們編寫的另一個小程序,稱為片段著色器(fragment shader),用于計算每個像素應(yīng)該是什么顏色。 該計算可以像返回綠色一樣簡單,也可以像計算表面相對于從附近其他表面反射的陽光的角度一樣復(fù)雜,通過霧過濾,并根據(jù)表面的金屬程度進行修改。 它完全在我們的控制之下,這既可以賦予我們權(quán)力,又可以帶來壓倒性的影響。
然后,這些像素顏色的結(jié)果會累積到紋理中,然后可以在屏幕上顯示。
2.2 定義頂點
如前所述,生命游戲模擬顯示為單元格網(wǎng)格。 我們的應(yīng)用程序需要一種可視化網(wǎng)格的方法,區(qū)分活動單元格和非活動單元格。 此 Codelab 使用的方法是在活動單元格中繪制彩色方塊,并將非活動單元格留空。
這意味著我們需要為 GPU 提供四個不同的點,每個點對應(yīng)正方形的四個角。 例如,在畫布中心繪制的正方形,從邊緣拉入一定距離,其角坐標如下:
為了將這些坐標提供給 GPU,我們需要將這些值放入 TypedArray 中。 如果你還不熟悉,TypedArray 是一組 JavaScript 對象,允許我們分配連續(xù)的內(nèi)存塊并將系列中的每個元素解釋為特定的數(shù)據(jù)類型。 例如,在 Uint8Array 中,數(shù)組中的每個元素都是單個無符號字節(jié)。 TypedArray 非常適合使用對內(nèi)存布局敏感的 API 來回發(fā)送數(shù)據(jù),例如 WebAssembly、WebAudio 和(當然)WebGPU。
對于平方示例,因為值是小數(shù),所以 Float32Array 是合適的。
通過在代碼中放置以下數(shù)組聲明來創(chuàng)建一個包含圖中所有頂點位置的數(shù)組。 放置它的好地方是靠近頂部,就在 context.configure() 調(diào)用下方。
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8,
0.8, -0.8,
0.8, 0.8,
-0.8, 0.8,
]);
請注意,間距和注釋對值沒有影響; 這只是為了方便并使其更具可讀性。 它可以幫助我們了解每一對值構(gòu)成一個頂點的 X 和 Y 坐標。
但有一個問題! GPU 按照三角形工作,還記得嗎? 因此,這意味著我們必須以三個為一組提供頂點。解決方案是重復(fù)兩個頂點以創(chuàng)建兩個三角形,它們共享穿過正方形中間的一條邊。
要從圖中形成正方形,我們必須列出 (-0.8, -0.8) 和 (0.8, 0.8) 頂點兩次,一次用于藍色三角形,一次用于紅色三角形。 也可以選擇將正方形與其他兩個角分開;這沒有什么區(qū)別。
更新之前的頂點數(shù)組,使其看起來像這樣:
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
盡管為了清晰起見,該圖顯示了兩個三角形之間的分離,但頂點位置完全相同,并且 GPU 渲染它們時沒有間隙。 它將呈現(xiàn)為單個實心正方形。
2.3 創(chuàng)建頂點緩沖區(qū)
GPU 無法使用 JavaScript 數(shù)組中的數(shù)據(jù)繪制頂點。 GPU 通常擁有針對渲染進行高度優(yōu)化的自己的內(nèi)存,因我們您希望 GPU 在繪制時使用的任何數(shù)據(jù)都需要放置在該內(nèi)存中。
對于許多值(包括頂點數(shù)據(jù)),GPU 端內(nèi)存是通過 GPUBuffer 對象進行管理的。 緩沖區(qū)是 GPU 可以輕松訪問并標記用于某些目的的內(nèi)存塊。 我們可以將其想象為有點像 GPU 可見的 TypedArray。
要創(chuàng)建緩沖區(qū)來保存頂點,請在頂點數(shù)組的定義之后添加對 device.createBuffer() 的以下調(diào)用。
const vertexBuffer = device.createBuffer({
label: "Cell vertices",
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
首先要注意的是我們給緩沖區(qū)一個標簽。 創(chuàng)建的每個 WebGPU 對象都可以被賦予一個可選標簽,你肯定想這樣做! 標簽是任何你想要的字符串,只要它能幫助你識別對象是什么。 如果遇到任何問題,WebGPU 生成的錯誤消息中會使用這些標簽來幫助我們了解出了什么問題。
接下來,給出緩沖區(qū)的大?。ㄒ宰止?jié)為單位)。 我們需要一個 48 字節(jié)的緩沖區(qū),可以通過將 32 位浮點數(shù)(4 字節(jié))的大小乘以頂點數(shù)組中的浮點數(shù) (12) 來確定。 令人高興的是,TypedArrays 已經(jīng)為我們計算了它們的 byteLength,因此可以在創(chuàng)建緩沖區(qū)時使用它。
最后,我們需要指定緩沖區(qū)的使用情況。 這是一個或多個 GPUBufferUsage 標志,多個標志與 | 組合在一起。 (按位或)運算符。 在這種情況下,我們指定希望緩沖區(qū)用于頂點數(shù)據(jù) (GPUBufferUsage.VERTEX),并且還希望能夠?qū)?shù)據(jù)復(fù)制到其中 (GPUBufferUsage.COPY_DST)。
返回給我們的緩沖區(qū)對象是不透明的 - 無法(輕松)檢查它保存的數(shù)據(jù)。 此外,它的大多數(shù)屬性都是不可變的——創(chuàng)建 GPUBuffer 后我們無法調(diào)整其大小,也無法更改使用標志。 可以更改的是其內(nèi)存的內(nèi)容。
當緩沖區(qū)最初創(chuàng)建時,它包含的內(nèi)存將被初始化為零。 有多種方法可以更改其內(nèi)容,但最簡單的方法是使用要復(fù)制的 TypedArray 調(diào)用 device.queue.writeBuffer() 。
要將頂點數(shù)據(jù)復(fù)制到緩沖區(qū)的內(nèi)存中,請?zhí)砑右韵麓a:
device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, vertices);
2.4 定義頂點布局
現(xiàn)在我們有了一個包含頂點數(shù)據(jù)的緩沖區(qū),但就 GPU 而言,它只是一個字節(jié)塊。 如果你想用它畫任何東西,需要提供更多的信息。 我們需要能夠告訴 WebGPU 有關(guān)頂點數(shù)據(jù)結(jié)構(gòu)的更多信息。
使用 GPUVertexBufferLayout 字典定義頂點數(shù)據(jù)結(jié)構(gòu):
const vertexBufferLayout = {
arrayStride: 8,
attributes: [{
format: "float32x2",
offset: 0,
shaderLocation: 0, // Position, see vertex shader
}],
};
乍一看這可能有點令人困惑,但它相對容易分解。
我們給出的第一件事是 arrayStride。 這是 GPU 在查找下一個頂點時需要在緩沖區(qū)中向前跳過的字節(jié)數(shù)。 正方形的每個頂點都由兩個 32 位浮點數(shù)組成。 前面提到,一個 32 位浮點數(shù)是 4 個字節(jié),所以兩個浮點數(shù)是 8 個字節(jié)。
接下來是attributes 屬性,它是一個數(shù)組。 屬性是編碼到每個頂點中的單獨信息。 我們的頂點僅包含一個屬性(頂點位置),但更高級的用例經(jīng)常包含具有多個屬性的頂點,例如頂點的顏色或幾何表面指向的方向。 不過,這超出了本 Codelab 的范圍。
在單個屬性中,我們首先定義數(shù)據(jù)的格式。 它來自 GPUVertexFormat 類型的列表,這些類型描述了 GPU 可以理解的每種頂點數(shù)據(jù)類型。 我們的頂點各有兩個 32 位浮點數(shù),因此使用 float32x2 格式。 例如,如果頂點數(shù)據(jù)由四個 16 位無符號整數(shù)組成,則可以使用 uint16x4。 看出規(guī)律了嗎?
接下來,偏移量描述了該特定屬性從頂點開始的字節(jié)數(shù)。 如果緩沖區(qū)中有多個屬性,實際上只需擔(dān)心這一點,而在此 Codelab 中不會出現(xiàn)這種情況。
最后,我們得到了著色器位置。 這是 0 到 15 之間的任意數(shù)字,并且對于我們定義的每個屬性都必須是唯一的。 它將這個屬性鏈接到頂點著色器中的特定輸入,我們將在下一節(jié)中了解該輸入。
請注意,雖然現(xiàn)在定義了這些值,但實際上尚未將它們傳遞到任何地方的 WebGPU API。 這即將到來,但在定義頂點時考慮這些值是最容易的,因此我們現(xiàn)在就可以設(shè)置它們以供以后使用。
2.5 從著色器開始
現(xiàn)在我們已經(jīng)有了想要渲染的數(shù)據(jù),但仍然需要告訴 GPU 到底如何處理它。 其中很大一部分發(fā)生在著色器上。
著色器是我們編寫并在 GPU 上執(zhí)行的小程序。 每個著色器在不同的數(shù)據(jù)階段上運行:頂點處理、片段處理或一般計算。 因為它們位于 GPU 上,所以它們的結(jié)構(gòu)比普通 JavaScript 更嚴格。 但這種結(jié)構(gòu)使它們能夠非??焖俚貓?zhí)行,而且最重要的是,可以并行執(zhí)行!
WebGPU 中的著色器是用稱為 WGSL(WebGPU 著色語言)的著色語言編寫的。 從語法上講,WGSL 有點像 Rust,其功能旨在使常見類型的 GPU 工作(如向量和矩陣數(shù)學(xué))更容易、更快。 教授整個著色語言遠遠超出了本 Codelab 的范圍,但希望您在完成一些簡單示例時能夠掌握一些基礎(chǔ)知識。
著色器本身作為字符串傳遞到 WebGPU。
通過將以下內(nèi)容復(fù)制到 vertexBufferLayout 下方的代碼中,創(chuàng)建一個用于輸入著色器代碼的位置:
const cellShaderModule = device.createShaderModule({
label: "Cell shader",
code: `
// Your shader code will go here
`
});
要創(chuàng)建著色器,請調(diào)用 device.createShaderModule(),向其提供可選標簽和 WGSL 代碼作為字符串。 (請注意,此處使用反引號以允許多行字符串!)添加一些有效的 WGSL 代碼后,該函數(shù)將返回一個包含編譯結(jié)果的 GPUShaderModule 對象。
2.6 定義頂點著色器
從頂點著色器開始,因為這也是 GPU 開始的地方!
頂點著色器被定義為一個函數(shù),GPU 為 vertexBuffer 中的每個頂點調(diào)用該函數(shù)一次。 由于我們的 vertexBuffer 有六個位置(頂點),因此定義的函數(shù)將被調(diào)用六次。 每次調(diào)用它時,vertexBuffer 中的不同位置都會作為參數(shù)傳遞給函數(shù),而頂點著色器函數(shù)的工作就是返回剪輯空間中的相應(yīng)位置。
重要的是要了解它們也不一定會按順序調(diào)用。 相反,GPU 擅長并行運行此類著色器,有可能同時處理數(shù)百(甚至數(shù)千?。﹤€頂點! 這是 GPU 實現(xiàn)令人難以置信的速度的重要原因,但它也有局限性。 為了確保極端并行化,頂點著色器之間不能進行通信。 每個著色器調(diào)用一次只能查看單個頂點的數(shù)據(jù),并且只能輸出單個頂點的值。
在 WGSL 中,頂點著色器函數(shù)可以隨意命名,但它前面必須有 @vertex 屬性,以指示它代表哪個著色器階段。 WGSL 表示帶有 fn 關(guān)鍵字的函數(shù),使用括號聲明任何參數(shù),并使用花括號定義范圍。
創(chuàng)建一個空的 @vertex 函數(shù),如下所示:
@vertex
fn vertexMain() {
}
但這是無效的,因為頂點著色器必須至少返回剪輯空間中正在處理的頂點的最終位置。 它始終以 4 維向量的形式給出。 向量在著色器中使用非常常見,因此它們被視為語言中的一流基元,具有自己的類型,例如 4 維向量的 vec4f。 2D 向量 (vec2f) 和 3D 向量 (vec3f) 也有類似的類型!
要指示返回的值是所需的位置,請使用 @builtin(position) 屬性對其進行標記。 -> 符號用于指示這是函數(shù)返回的內(nèi)容。
@vertex
fn vertexMain() -> @builtin(position) vec4f {
}
當然,如果函數(shù)有返回類型,則需要在函數(shù)體內(nèi)實際返回一個值。 可以使用語法 vec4f(x, y, z, w) 構(gòu)造一個新的 vec4f 來返回。 x、y 和 z 值都是浮點數(shù),它們在返回值中指示頂點在剪輯空間中的位置。
可以返回靜態(tài)值 (0, 0, 0, 1),從技術(shù)上講,我們就擁有了一個有效的頂點著色器,盡管該著色器從不顯示任何內(nèi)容,因為 GPU 識別出它生成的三角形只是一個點,然后將其丟棄。
@vertex
fn vertexMain() -> @builtin(position) vec4f {
return vec4f(0, 0, 0, 1); // (X, Y, Z, W)
}
相反,我們想要的是利用創(chuàng)建的緩沖區(qū)中的數(shù)據(jù),并通過使用 @location() 屬性和與在 vertexBufferLayout 中描述的類型匹配的類型聲明函數(shù)的參數(shù)來實現(xiàn)這一點。 我們將 ShaderLocation 指定為 0,因此在 WGSL 代碼中,使用 @location(0) 標記參數(shù)。 我們還將格式定義為 float32x2,它是一個 2D 向量,因此在 WGSL 中的參數(shù)是 vec2f。 可以將其命名為任何你喜歡的名稱,但由于這些代表我們的頂點位置,因此像 pos 這樣的名稱似乎很自然。
將著色器函數(shù)更改為以下代碼:
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
現(xiàn)在我們需要返回該位置。 由于位置是 2D 向量并且返回類型是 4D 向量,因此必須對其進行一些更改。 我們想要做的是將位置參數(shù)中的兩個分量放入返回向量的前兩個分量中,將最后兩個分量分別保留為 0 和 1。
通過明確說明要使用的位置組件來返回正確的位置:
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos.x, pos.y, 0, 1);
}
但是,由于這些類型的映射在著色器中非常常見,因此還可以以方便的速記方式將位置向量作為第一個參數(shù)傳遞,這意味著同樣的事情。
使用以下代碼重寫 return 語句:
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
這就是我們最初的頂點著色器! 這非常簡單,只需將位置有效地傳遞出去,但它足以開始使用。
2.7 定義片段著色器
接下來是片段著色器。 片段著色器的操作方式與頂點著色器非常相似,但它們不是為每個頂點調(diào)用,而是為每個繪制的像素調(diào)用。
片段著色器總是在頂點著色器之后調(diào)用。 GPU 獲取頂點著色器的輸出并對它進行三角測量,從三個點的集合中創(chuàng)建三角形。 然后,它通過確定輸出顏色附件的哪些像素包含在該三角形中來光柵化每個三角形,然后為每個像素調(diào)用一次片段著色器。 片段著色器返回一種顏色,通常根據(jù)從頂點著色器發(fā)送到它的值和 GPU 寫入顏色附件的紋理等資源來計算。
就像頂點著色器一樣,片段著色器以大規(guī)模并行方式執(zhí)行。 它們在輸入和輸出方面比頂點著色器更靈活,但可以認為它們只是為每個三角形的每個像素返回一種顏色。
WGSL 片段著色器函數(shù)用 @fragment 屬性表示,并且它還返回 vec4f。 但在這種情況下,矢量代表顏色,而不是位置。 需要為返回值提供 @location 屬性,以便指示返回的顏色寫入 beginRenderPass 調(diào)用中的哪個 colorAttachment。 由于我們只有一個附件,因此位置為 0。
創(chuàng)建一個空的 @fragment 函數(shù),如下所示:
@fragment
fn fragmentMain() -> @location(0) vec4f {
}
返回向量的四個分量是紅色、綠色、藍色和 alpha 顏色值,它們的解釋方式與我們之前在 beginRenderPass 中設(shè)置的clearValue 完全相同。 所以 vec4f(1, 0, 0, 1) 是亮紅色,這對于我們的正方形來說似乎是一個不錯的顏色。 不過,可以隨意將其設(shè)置為你想要的任何顏色!
設(shè)置返回的顏色向量,如下所示:
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}
這就是一個完整的片段著色器! 這并不是一件非常有趣的事。 它只是將每個三角形的每個像素設(shè)置為紅色,但現(xiàn)在就足夠了。
回顧一下,添加上面詳細介紹的著色器代碼后,我們的 createShaderModule 調(diào)用現(xiàn)在如下所示:
const cellShaderModule = device.createShaderModule({
label: 'Cell shader',
code: `
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
2.8 創(chuàng)建渲染管道
著色器模塊不能單獨用于渲染。 相反,我們必須將其用作通過調(diào)用 device.createRenderPipeline() 創(chuàng)建的 GPURenderPipeline 的一部分。 渲染管道控制幾何圖形的繪制方式,包括使用哪些著色器、如何解釋頂點緩沖區(qū)中的數(shù)據(jù)、應(yīng)渲染哪種幾何圖形(線、點、三角形…)等等!
渲染管道是整個 API 中最復(fù)雜的對象,但不用擔(dān)心! 可以傳遞給它的大多數(shù)值都是可選的,我們只需提供一些即可開始。
創(chuàng)建渲染管道,如下所示:
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: "auto",
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
每個管道都需要一個布局來描述管道需要什么類型的輸入(頂點緩沖區(qū)除外),但實際上沒有任何輸入。 幸運的是,我們現(xiàn)在可以傳遞“auto”,管道會從著色器構(gòu)建自己的布局。
接下來,我們必須提供有關(guān)頂點階段的詳細信息。 該模塊是包含頂點著色器的 GPUShaderModule,entryPoint 給出著色器代碼中為每個頂點調(diào)用調(diào)用的函數(shù)的名稱。 (可以在單個著色器模塊中擁有多個 @vertex 和 @fragment 函數(shù)!)緩沖區(qū)是 GPUVertexBufferLayout 對象的數(shù)組,用于描述如何將數(shù)據(jù)打包到與此管道一起使用的頂點緩沖區(qū)中。 幸運的是,我們之前已經(jīng)在 vertexBufferLayout 中定義了它! 這是你傳遞它的地方。
最后,我們將了解有關(guān)片段階段的詳細信息。 這還包括著色器模塊和入口點,就像頂點階段一樣。 最后一位是定義該管道使用的目標。 這是一個字典數(shù)組,提供管道輸出到的顏色附件的詳細信息(例如紋理格式)。 這些細節(jié)需要與該管道所使用的任何渲染通道的 colorAttachments 中給出的紋理相匹配。 我們的渲染通道使用畫布上下文中的紋理,并使用我們在 canvasFormat 中保存的值作為其格式,因此可以在此處傳遞相同的格式。
這甚至還沒有接近您在創(chuàng)建渲染管道時可以指定的所有選項,但足以滿足此 Codelab 的需求!
2.9 繪制正方形
這樣,我們現(xiàn)在就擁有了繪制正方形所需的一切!
要繪制正方形,請?zhí)氐絜ncoder.beginRenderPass() 和 pass.end() 對調(diào)用,然后在它們之間添加這些新命令:
// After encoder.beginRenderPass()
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2); // 6 vertices
// before pass.end()
這為 WebGPU 提供了繪制正方形所需的所有信息。 首先,我們使用 setPipeline() 來指示應(yīng)使用哪個管道進行繪制。 這包括所使用的著色器、頂點數(shù)據(jù)的布局以及其他相關(guān)狀態(tài)數(shù)據(jù)。
接下來,我們使用包含正方形頂點的緩沖區(qū)調(diào)用 setVertexBuffer()。 可以使用 0 來調(diào)用它,因為該緩沖區(qū)對應(yīng)于當前管道的 vertex.buffers 定義中的第 0 個元素。
最后,進行draw() 調(diào)用,在完成之前的所有設(shè)置之后,這似乎非常簡單。 我們唯一需要傳入的是它應(yīng)該渲染的頂點數(shù)量,它從當前設(shè)置的頂點緩沖區(qū)中提取并使用當前設(shè)置的管道進行解釋。 可以將其硬編碼為 6,但是從頂點數(shù)組(每個頂點 12 個浮點/2 個坐標 == 6 個頂點)計算它意味著,如果我們決定用圓形等替換正方形,則數(shù)量會更少 手動更新。
刷新屏幕并(最終)看到所有辛勤工作的結(jié)果:一個大的彩色方塊。
3、繪制柵格
首先,花點時間祝賀一下自己! 對于大多數(shù) GPU API 而言,在屏幕上獲取第一批幾何圖形通常是最困難的步驟之一。 我們從這里所做的一切都可以通過更小的步驟完成,從而更輕松地驗證我們的進度。
在本節(jié)中,我們將學(xué)習(xí):
- 如何從 JavaScript 將變量(稱為uniform)傳遞給著色器。
- 如何使用uniforms來改變渲染行為。
- 如何使用實例繪制同一幾何體的許多不同變體。
3.1 定義柵格
為了渲染柵格,我們需要了解有關(guān)它的非?;镜男畔ⅰ?它包含多少個單元格(寬度和高度)? 這取決于開發(fā)人員,但為了讓事情變得更簡單,請將柵格視為正方形(相同的寬度和高度)并使用 2 的冪的大小。 (這使得稍后的一些數(shù)學(xué)計算變得更容易。)我們最終希望將其變得更大,但對于本節(jié)的其余部分,將網(wǎng)格大小設(shè)置為 4x4,因為這樣可以更輕松地演示本節(jié)中使用的一些數(shù)學(xué)。 之后再放大!
通過在 JavaScript 代碼頂部添加常量來定義網(wǎng)格大小。
const GRID_SIZE = 4;
接下來,我們需要更新渲染正方形的方式,以便可以在畫布上容納 GRID_SIZE 乘以 GRID_SIZE 的正方形。 這意味著正方形需要小得多,而且需要有很多。
現(xiàn)在,解決這個問題的一種方法是使頂點緩沖區(qū)變得更大,并在其中以正確的大小和位置定義 GRID_SIZE 乘以 GRID_SIZE 的正方形。 事實上,該代碼不會太糟糕! 只需幾個 for 循環(huán)和一些數(shù)學(xué)知識。 但這也沒有充分利用 GPU,并且使用了超出實現(xiàn)效果所需的內(nèi)存。 本節(jié)探討一種對 GPU 更友好的方法。
3.2 創(chuàng)建uniform緩沖區(qū)
首先,我們需要將選擇的柵格大小傳達給著色器,因為它使用它來更改事物的顯示方式。 可以將大小硬編碼到著色器中,但這意味著每當想要更改網(wǎng)格大小時,都必須重新創(chuàng)建著色器和渲染管道,這是昂貴的。 更好的方法是將柵格大小作為uniform提供給著色器。
我們之前了解到,頂點緩沖區(qū)中的不同值會傳遞給頂點著色器的每次調(diào)用。 uniform是來自緩沖區(qū)的值,對于每次調(diào)用都是相同的。 它們對于傳達幾何圖形(例如其位置)、完整動畫幀(例如當前時間)甚至應(yīng)用程序的整個生命周期(例如用戶首選項)的常見值非常有用。
通過添加以下代碼創(chuàng)建uniform緩沖區(qū):
// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]);
const uniformBuffer = device.createBuffer({
label: "Grid Uniforms",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
這看起來應(yīng)該非常熟悉,因為它與之前用于創(chuàng)建頂點緩沖區(qū)的代碼幾乎完全相同! 這是因為uniform是通過與頂點相同的 GPUBuffer 對象與 WebGPU API 通信的,主要區(qū)別在于這次的使用包括 GPUBufferUsage.UNIFORM 而不是 GPUBufferUsage.VERTEX。
3.3 在著色器中訪問uniform
通過添加以下代碼來定義uniform:
// At the top of the `code` string in the createShaderModule() call
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos / grid, 0, 1);
}
// ...fragmentMain is unchanged
這在著色器中定義了一個名為 grid 的uniform,它是一個 2D 浮點向量,與剛剛復(fù)制到統(tǒng)一緩沖區(qū)中的數(shù)組相匹配。 它還指定uniform在 @group(0) 和 @binding(0) 處綁定。 稍后你就會了解這些值的含義。
然后,在著色器代碼的其他位置,可以根據(jù)需要使用柵格向量。 在此代碼中,我們將頂點位置除以柵格向量。 由于 pos 是一個 2D 向量,而 grid 是一個 2D 向量,因此 WGSL 執(zhí)行按分量劃分。 換句話說,結(jié)果與 vec2f(pos.x / grid.x, pos.y / grid.y) 相同。
這些類型的矢量運算在 GPU 著色器中非常常見,因為許多渲染和計算技術(shù)都依賴于它們!
在我們的情況下,這意味著(如果使用的網(wǎng)格大小為 4)渲染的正方形將是其原始大小的四分之一。 如果想將其中四個放入一行或一列,那就完美了!
3.4 創(chuàng)建綁定組
不過,在著色器中聲明uniform并不會將其與我們創(chuàng)建的緩沖區(qū)連接起來。 為此,需要創(chuàng)建并設(shè)置一個綁定組。
綁定組是我們希望著色器可以同時訪問的資源的集合。 它可以包含多種類型的緩沖區(qū)(例如統(tǒng)一緩沖區(qū))以及其他資源(例如此處未介紹的紋理和采樣器),但它們是 WebGPU 渲染技術(shù)的常見部分。
通過在創(chuàng)建uniform緩沖區(qū)和渲染管道后添加以下代碼,使用uniform緩沖區(qū)創(chuàng)建綁定組:
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}],
});
除了現(xiàn)在的標準標簽之外,我們還需要一個布局來描述此綁定組包含哪些類型的資源。 這是在以后的步驟中進一步深入研究的內(nèi)容,但目前你可以愉快地向管道詢問綁定組布局,因為我們使用布局創(chuàng)建了管道:“auto”。 這會導(dǎo)致管道根據(jù)我們在著色器代碼本身中聲明的綁定自動創(chuàng)建綁定組布局。 在本例中,我們要求它 getBindGroupLayout(0),其中 0 對應(yīng)于我們在著色器中鍵入的 @group(0)。
指定布局后,我們提供一個條目數(shù)組。 每個條目都是一個至少包含以下值的字典:
- binding,它與您在著色器中輸入的 @binding() 值相對應(yīng)。 在這種情況下,0。
- resource,這是想要向指定綁定索引處的變量公開的實際資源。 在這種情況下,你的uniform緩沖區(qū)。
該函數(shù)返回一個 GPUBindGroup,它是一個不透明、不可變的句柄。 創(chuàng)建綁定組后,我們將無法更改其指向的資源,但可以更改這些資源的內(nèi)容。 例如,如果更改uniform緩沖區(qū)以包含新的柵格大小,則使用此綁定組的未來繪制調(diào)用會反映這一點。
3.5 綁定綁定組
現(xiàn)在綁定組已創(chuàng)建,我們?nèi)匀恍枰嬖V WebGPU 在繪圖時使用它。 幸運的是,這非常簡單。
跳回渲染通道并在draw() 方法之前添加此新行:
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup); // New line!
pass.draw(vertices.length / 2);
作為第一個參數(shù)傳遞的 0 對應(yīng)于著色器代碼中的 @group(0)。 意思是說屬于 @group(0) 一部分的每個 @binding 都使用此綁定組中的資源。
現(xiàn)在uniform緩沖區(qū)已暴露給我們的著色器!
刷新頁面,然后應(yīng)該看到類似這樣的內(nèi)容:
太棒了! 我們的正方形現(xiàn)在大小是以前的四分之一! 這并不多,但它表明我們的uniform已實際應(yīng)用,并且著色器現(xiàn)在可以訪問柵格的大小。
3.6 在著色器中操縱幾何體
現(xiàn)在我們可以在著色器中引用柵格大小,可以開始做一些工作來操縱正在渲染的幾何體以適合我們所需的網(wǎng)格圖案。 為此,請仔細考慮想要實現(xiàn)的目標。
我們需要從概念上將畫布劃分為各個單元。 為了保持 X 軸隨著向右移動而增加、Y 軸隨著向上移動而增加的慣例,假設(shè)第一個單元格位于畫布的左下角。 這會給你一個看起來像這樣的布局,當前的方形幾何圖形位于中間:
我們面臨的挑戰(zhàn)是在著色器中找到一種方法,可以在給定單元坐標的任何單元中定位方形幾何體。
首先,可以看到我們的正方形與任何單元格都沒有很好地對齊,因為它被定義為圍繞畫布的中心。 我們希望將正方形移動半個單元格,以便它在它們內(nèi)部很好地對齊。
解決此問題的一種方法是更新正方形的頂點緩沖區(qū)。 例如,通過移動頂點使右下角位于 (0.1, 0.1) 而不是 (-0.8, -0.8),可以移動該正方形以更好地與單元格邊界對齊。 但是,由于我們可以完全控制著色器中頂點的處理方式,因此使用著色器代碼將它們推到位也同樣容易!
使用以下代碼更改頂點著色器模塊:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Add 1 to the position before dividing by the grid size.
let gridPos = (pos + 1) / grid;
return vec4f(gridPos, 0, 1);
}
這會將每個頂點向上和向左移動 1(請記住,這是剪輯空間的一半),然后將其除以柵格大小。 結(jié)果是一個與原點完全對齊的正方形。
接下來,因為畫布的坐標系將 (0, 0) 放置在中心,將 (-1, -1) 放置在左下角,并且我們希望 (0, 0) 位于左下角,所以需要平移幾何體的 除以網(wǎng)格大小后將位置除以 (-1, -1),以便將其移動到該角落。
平移幾何體的位置,如下所示:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Subtract 1 after dividing by the grid size.
let gridPos = (pos + 1) / grid - 1;
return vec4f(gridPos, 0, 1);
}
現(xiàn)在你的方塊已經(jīng)很好地位于單元格 (0, 0) 中!
如果想將其放置在不同的單元格中怎么辦? 通過在著色器中聲明一個單元向量并用靜態(tài)值填充它來解決這個問題,例如 let cell = vec2f(1, 1)。
如果將其添加到 gridPos 中,它將撤消算法中的 - 1,因此這不是我們想要的。 相反,我們只想為每個單元格將正方形移動一個網(wǎng)格單位(畫布的四分之一)。 聽起來需要再按網(wǎng)格除一次!
更改網(wǎng)格定位,如下所示:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1); // Cell(1,1) in the image above
let cellOffset = cell / grid; // Compute the offset to cell
let gridPos = (pos + 1) / grid - 1 + cellOffset; // Add it here!
return vec4f(gridPos, 0, 1);
}
如果現(xiàn)在刷新,會看到以下內(nèi)容:
嗯。 不完全是我們想要的。
這樣做的原因是,由于畫布坐標從 -1 到 +1,它實際上是 2 個單位。 這意味著如果想將畫布的四分之一移動頂點,則必須將其移動 0.5 個單位。 在使用 GPU 坐標進行推理時,這是一個很容易犯的錯誤! 幸運的是,修復(fù)也同樣簡單。
將偏移量乘以 2,如下所示:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
這正是我們想要的。
屏幕截圖如下所示:
此外,現(xiàn)在可以將單元格設(shè)置為柵格邊界內(nèi)的任何值,然后刷新以查看所需位置的正方形渲染。
3.7 繪制實例
現(xiàn)在可以通過一些數(shù)學(xué)運算將正方形放置在我們想要的位置,下一步是在柵格的每個單元格中渲染一個正方形。
實現(xiàn)它的一種方法是將單元格坐標寫入統(tǒng)一緩沖區(qū),然后為網(wǎng)格中的每個方塊調(diào)用一次繪制,每次更新統(tǒng)一。 然而,這會非常慢,因為 GPU 每次都必須等待 JavaScript 寫入新坐標。 從 GPU 獲得良好性能的關(guān)鍵之一是最大限度地減少 GPU 等待系統(tǒng)其他部分的時間!
相反,可以使用一種稱為實例化的技術(shù)。 實例化是一種告訴 GPU 通過一次調(diào)用繪制同一幾何圖形的多個副本的方法,這比為每個副本調(diào)用一次繪制要快得多。 幾何體的每個副本都稱為一個實例。
要告訴 GPU 需要足夠的正方形實例來填充網(wǎng)格,請向現(xiàn)有繪制調(diào)用添加一個參數(shù):
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
這告訴系統(tǒng)我們希望它繪制正方形的六個 (vertices.length / 2) 頂點 16 (GRID_SIZE * GRID_SIZE) 次。 但如果刷新頁面,仍然會看到以下內(nèi)容:
為什么? 嗯,這是因為我們將所有 16 個正方形繪制在同一個位置。 需要在著色器中添加一些額外的邏輯,以根據(jù)每個實例重新定位幾何體。
在著色器中,除了來自頂點緩沖區(qū)的 pos 等頂點屬性之外,還可以訪問所謂的 WGSL 內(nèi)置值。 這些是由 WebGPU 計算的值,其中一個值是 instance_index。 instance_index 是一個無符號 32 位數(shù)字,范圍為 0 到實例數(shù) - 1,可以將其用作著色器邏輯的一部分。 對于屬于同一實例的每個已處理頂點,其值是相同的。 這意味著我們的頂點著色器將被調(diào)用六次,instance_index 為 0,對于頂點緩沖區(qū)中的每個位置調(diào)用一次。 然后,再進行六次,instance_index 為 1,然后再進行六次,instance_index 為 2,依此類推。
要查看其實際效果,必須將內(nèi)置的instance_index 添加到著色器輸入中。 以與位置相同的方式執(zhí)行此操作,但不要使用 @location 屬性標記它,而是使用 @builtin(instance_index),然后將參數(shù)命名為想要的任何名稱。 (可以將其稱為實例以匹配示例代碼。)然后將其用作著色器邏輯的一部分!
使用實例代替單元格坐標:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance); // Save the instance_index as a float
let cell = vec2f(i, i);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
如果你現(xiàn)在刷新,會發(fā)現(xiàn)確實有不止一個正方形! 但無法看到全部 16 個。
這是因為我們生成的單元格坐標為 (0, 0)、(1, 1)、(2, 2)…一直到 (15, 15),但只有其中的前四個適合畫布。 要創(chuàng)建所需的網(wǎng)格,需要轉(zhuǎn)換instance_index,以便每個索引映射到網(wǎng)格中的唯一單元格,如下所示:
其數(shù)學(xué)原理相當簡單。 對于每個單元格的 X 值,需要對 instance_index 和網(wǎng)格寬度取模,這可以在 WGSL 中使用 % 運算符執(zhí)行。 對于每個單元格的 Y 值,我們希望將 instance_index 除以網(wǎng)格寬度,并丟棄任何小數(shù)余數(shù)。 可以使用 WGSL 的 Floor() 函數(shù)來做到這一點。
更改計算,如下所示:
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
更新代碼后,終于得到了期待已久的正方形網(wǎng)格!
現(xiàn)在它可以工作了,返回并增大柵格大?。?/p>
const GRID_SIZE = 32;
完美!文章來源:http://www.zghlxwxcb.cn/news/detail-597376.html
原文鏈接:WebGPU應(yīng)用開發(fā)入門 — BimAnt文章來源地址http://www.zghlxwxcb.cn/news/detail-597376.html
到了這里,關(guān)于WebGPU開發(fā)簡明教程【2023】的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!