本節我們來複原一個彩色的正方形,前提告知,本節的shaders和main的程式碼從結構上有調整,我會更加詳細的描述每行的程式碼意思;
原始碼下載地址:https://github.com/jack1232/webgpu07
一、首先需要你安裝所有的軟體包
安裝完軟體包,確保你的本地生成node_modules資料夾
npm install
二、建立你的index.html檔案
1.<canvas>的"canvas-webgpu"你可以理解為呼叫webgpu內的canvas介面;
2.<script>是呼叫打包好對webgpu操作指令的指令碼檔案bundle.js檔案;
<!DOCTYPE html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>WebGPU Step-by-Step 7</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div> <h1>Create Square using GPU Buffer</h1><br> <canvas id="canvas-webgpu" width="640" height="480"></canvas> </div> <script src="main.bundle.js"></script> </body> </html>
二、建立你的helper.ts檔案
前面說過,程式碼結構上有調整,是因為我們把原來寫在main.ts檔案裡面的一些宣告方法,現在在helper.ts進行了宣告封裝,根據原始碼作者的意思,把一些通用的webgpuAPI呼叫介面宣告出這樣獨立的一個外部方法檔案,我們在main檔案中進行直接呼叫函式方法即可;
我使用了三種顏色區分來說明每個程式碼塊的用法,;
■代表:自定義建立一個CreateGPUBuffer函式,這個函式內包含建立device\data\usageFlage分別來應用webgpuAPI介面來實現buffer緩衝區啟用GPUBuffer;
■代表:在此自定義物件函式,裡面的定義好了大多通用的webgpu方法< device, canvas, format, context>,不需要你在宣告方法,只接呼叫即可
■代表:這是一個檢查函式,判定你的瀏覽器是否支援webgpu;
其中,我有個疑問還未解決:在helper.ts中,建立的CreateGPUBuffer緩衝區物件,其中輸入變數usageFlag的.VERTEX和.COPY_DST代表什麼意思呢?
usageFlag:GPUBufferUsageFlags = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
程式碼如下:
1 export const CreateGPUBuffer = (device:GPUDevice, data:Float32Array, //自定義建立一個CreateGPUBuffer函式,這個函式內包含建立device\data\usageFlage分別來應用webgpuAPI介面來實現 2 usageFlag:GPUBufferUsageFlags = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST) => { //device:裝置啟用;data:資料型別;usageFlag:緩衝區資源使用 3 const buffer = device.createBuffer({ //使用device.createBuffer來產生GPUBuffer 4 size: data.byteLength, //size:分配的長度,以位元組為單位;byteLength是js的一個屬性,用來獲取陣列位元組長度 5 usage: usageFlag, //對應上面usageFlag 6 mappedAtCreation: true //true表示這個API定義了GPU緩衝區可以被CPU擁有使用,可以通過typescript讀取或者寫入 7 }); 8 new Float32Array(buffer.getMappedRange()).set(data); //一但程式處於map狀態,我們的資料便會把data寫入buffer 9 buffer.unmap(); //unmap是GPU使用的狀態,map是CPU使用的狀態 10 return buffer; //這裡的buffer是返回unmap狀態的buffer 11 } 12 13 export const InitGPU = async () => { //在此自定義物件函式,裡面的定義好了大多通用的webgpu方法< device, canvas, format, context>,不需要你在宣告方法,直接呼叫即可 14 const checkgpu = CheckWebGPU(); 15 if(checkgpu.includes('Your current browser does not support WebGPU!')){ 16 console.log(checkgpu); 17 throw('Your current browser does not support WebGPU!'); 18 } 19 const canvas = document.getElementById('canvas-webgpu') as HTMLCanvasElement; 20 const adapter = await navigator.gpu?.requestAdapter(); 21 const device = await adapter?.requestDevice() as GPUDevice; 22 const context = canvas.getContext('webgpu') as unknown as GPUCanvasContext; 23 24 const devicePixelRatio = window.devicePixelRatio || 1; 25 const size = [ 26 canvas.clientWidth * devicePixelRatio, 27 canvas.clientHeight * devicePixelRatio, 28 ]; 29 const format = context.getPreferredFormat(adapter!); 30 31 context.configure({ 32 device: device, 33 format: format, 34 size: size 35 }); 36 return{ device, canvas, format, context }; 37 }; 38 39 export const CheckWebGPU = () => { //這是一個檢查函式,判定你的瀏覽器是否支援webgpu; 40 let result = 'Great, your current browser supports WebGPU!'; 41 if (!navigator.gpu) { 42 result = `Your current browser does not support WebGPU! Make sure you are on a system 43 with WebGPU enabled. Currently, SPIR-WebGPU is only supported in 44 <a href="https://www.google.com/chrome/canary/">Chrome canary</a> 45 with the flag "enable-unsafe-webgpu" enabled. See the 46 <a href="https://github.com/gpuweb/gpuweb/wiki/Implementation-Status"> 47 Implementation Status</a> page for more details. 48 `; 49 } 50 return result; 51 }
三、開始編寫你的main.ts檔案
需要你注意的是,原先我們把建立頂點和顏色的程式碼部分放到了shaders下實現,現在把建立頂點和顏色的資料放在main下實現,把頂點座標和顏色資料放在緩衝區寫,是因為main.ts是typescript寫成的,有程式碼提示,相比文字的著色器,除錯起來更方便。
程式碼如下:
1 import { InitGPU, CreateGPUBuffer } from './helper'; //呼叫InitGPU(對GPU的許可權讀取等操作指令的封裝方法介面) CreateGPUBuffer(對GPU緩衝區的設定封裝) 2 import { Shaders } from './shaders'; //呼叫著色器 3 4 const CreateSquare = async () => { //宣告一個常量塊,並非同步對映給定範圍 5 const gpu = await InitGPU(); //第一個範圍,宣告gpu常量,給定範圍是InitGPU介面 6 const device = gpu.device; //第二個範圍,宣告device常量,給定範圍是gpu中的.device 7 8 const vertexData = new Float32Array([ //開始建立頂點具體資料,逆時針給定三角形頂點形成一個正方形,並初始化這個變數陣列型別Float32Array 9 -0.5, -0.5, // vertex a 10 0.5, -0.5, // vertex b 11 -0.5, 0.5, // vertex d 12 -0.5, 0.5, // vertex d 13 0.5, -0.5, // vertex b 14 0.5, 0.5, // vertex c 15 ]); 16 17 const colorData = new Float32Array([ //開始給定顏色賦值,同樣按照逆時針方向給定顏色,並初始化陣列型別(這裡的陣列沒有浮點小數,是因為typescript不區分1.0和1的區別;反之著色器程式碼要嚴格區分小數) 18 1, 0, 0, // vertex a: red 19 0, 1, 0, // vertex b: green 20 1, 1, 0, // vertex d: yellow 21 1, 1, 0, // vertex d: yellow 22 0, 1, 0, // vertex b: green 23 0, 0, 1 // vertex c: blue 24 ]); 25 26 const vertexBuffer = CreateGPUBuffer(device, vertexData); //建立物件,引用helper.ts中的CreateBuffer方法賦予建立的vertexBuffer資料vertexData 27 const colorBuffer = CreateGPUBuffer(device, colorData) //建立物件,引用helper.ts中的CreateBuffer方法賦予建立的colorBuffer資料colorData 28 29 const shader = Shaders(); //建立Shader變數引入著色器 30 const pipeline = device.createRenderPipeline({ //產生渲染管線,進行vertex和fragment設定 31 vertex: { //vertex描述pipeline入口著色器及輸入的緩衝區佈局 32 module: device.createShaderModule({ //建立著色器模組 33 code: shader.vertex //呼叫著色器程式碼部分的vertex方法 34 }), 35 entryPoint: "main", //入口位置 36 buffers:[ //buffers陣列,具體定義前面建立的頂點和顏色 37 { //這是具體描述你前面建立的vertexData的頂點資料 38 // 描述vertexDara 39 arrayStride: 8, //arrayStride是一個api函式,表示該陣列的步幅(以位元組為單位)為什麼是8?每一個頂點有兩個步幅(理解為位元組或座標),2(x,y)x4(四個頂點)=8 40 attributes: [{ //結構成員,是一個api函式 41 shaderLocation: 0, //shaderLocation是一個API,它表示每個屬性由數字location繫結,這裡暫時我還不太理解為什麼定義為0,目前只要記住vertexData的location定義為0 42 format: "float32x2", //定義頂點格式 ,因為有2個元素,所有float32x2 43 offset: 0 44 }] 45 }, 46 { 47 //描述colorData 48 arrayStride: 12, //為什麼是12?3(3個座標)x4(四個頂點)=12 49 attributes: [{ 50 shaderLocation: 1, //colorData的location定義為1 51 format: "float32x3", 52 offset: 0 53 }] 54 } 55 ] 56 }, 57 fragment: { //fragment 描述 pipeline 的片段著色器入口點及其輸出顏色 58 module: device.createShaderModule({ 59 code: shader.fragment 60 }), 61 entryPoint: "main", 62 targets: [ 63 { 64 format: gpu.format as GPUTextureFormat 65 } 66 ] 67 }, 68 primitive:{ 69 topology: "triangle-list", 70 } 71 }); 72 73 const commandEncoder = device.createCommandEncoder(); 74 const textureView = gpu.context.getCurrentTexture().createView(); 75 const renderPass = commandEncoder.beginRenderPass({ 76 colorAttachments: [{ 77 view: textureView, 78 loadValue: { r: 0.5, g: 0.5, b: 0.8, a: 1.0 }, //background color 79 storeOp: 'store' 80 }] 81 }); 82 renderPass.setPipeline(pipeline); //繫結管線到渲染通道上 83 renderPass.setVertexBuffer(0, vertexBuffer); //把 vertexBuffer內的頂點資料繫結到渲染通道上,<<這裡的插槽理解還沒有解決>> 84 renderPass.setVertexBuffer(1, colorBuffer); 85 renderPass.draw(6); //兩個三角形,六個頂點,需要進行draw繪製 86 renderPass.endPass(); 87 88 device.queue.submit([commandEncoder.finish()]); //結束device的渲染 89 } 90 91 CreateSquare();
從第8行開始建立的頂點資料,如下圖進行理解,需要注意頂點和著色全部按照逆時針進行陣列排列
在main檔案中我有兩個疑問沒有解答,我把我在社群發問的地址公佈出來,以便一些讀者或者社群回覆可以看到:
1.關於setVertexBuffer()插槽的理解(已解決)
四、接下來就是編寫你的shaders檔案
可以看到著色器程式碼簡化了很多,是因為可以直接使用緩衝區裡的vertexData頂點座標和colorData顏色資料進行聯絡,聯絡方式就是location。
export const Shaders = () => { const vertex = ` struct Output { [[builtin(position)]] Position : vec4<f32>; [[location(0)]] vColor : vec4<f32>; }; [[stage(vertex)]] fn main([[location(0)]] pos: vec4<f32>, [[location(1)]] color: vec4<f32>) -> Output { var output: Output; output.Position = pos; output.vColor = color; return output; }`; const fragment = ` [[stage(fragment)]] fn main([[location(0)]] vColor: vec4<f32>) -> [[location(0)]] vec4<f32> { return vColor; }`; return { vertex, fragment }; }
五、捆綁檔案,啟用瀏覽器
npm run prod
學習資料:
嗶哩嗶哩視訊教程:https://www.bilibili.com/video/BV1M64y1r7zL?spm_id_from=333.999.0.0
WebGPU文件:https://www.orillusion.com/zh/webgpu.html#intro
WGSL文件:https://www.orillusion.com/zh/wgsl.html#intro
原始碼地址:https://github.com/jack1232/webgpu07