大家好,本文學習Chrome->webgpu-samplers->helloTriangle示例。
上一篇博文:
WebGPU學習(一): 開篇
下一篇博文:
WebGPU學習(三):MSAA
准備Sample代碼
克隆webgpu-samplers Github Repo到本地。
(備注:當前的version為0.0.2)
實際的sample代碼在src/examples/文件夾中,是typescript代碼寫的:
學習helloTriangle.ts
打開helloTriangle.ts文件,我們來看下init函數的內容。
首先是shader代碼
const vertexShaderGLSL = `#version 450
const vec2 pos[3] = vec2[3](vec2(0.0f, 0.5f), vec2(-0.5f, -0.5f), vec2(0.5f, -0.5f));
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
}
`;
const fragmentShaderGLSL = `#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
這里是vertex shader和fragment shader的glsl代碼。
(webgpu支持vertex shader、fragment shader、compute shader,這里只使用了前面兩個)
“#version 450”聲明了glsl版本為4.5(它要放在glsl的第一行)
第2行定義了三角形的三個頂點坐標,使用2維數組保存(每個元素為vec2類型)。因為都在一個平面,所以頂點只定義了x、y坐標(頂點的z為0.0)
第5行的gl_VertexIndex為頂點序號,每次執行時值依次為0、1、2(vertex shader被執行了3次,因為只有3個頂點)(具體見本文末尾對draw的分析)
第9行是fragment shader,因為三角形為一個顏色,所以所有片段的顏色為同一個固定值
然后我們繼續看下面的代碼
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
// 准備編譯glsl的庫
const glslang = await glslangModule();
// 獲得webgpu上下文
const context = canvas.getContext('gpupresent');
第4行的glslangModule是import的第三方庫:
import glslangModule from '../glslang';
繼續往下看
// 定義swapbuffer的格式為RGBA8位的無符號歸一化格式
const swapChainFormat = "bgra8unorm";
// @ts-ignore:
const swapChain: GPUSwapChain = context.configureSwapChain({
device,
format: swapChainFormat,
});
@ts-ignore是typescript用來忽略錯誤的。因為context的類型是RenderingContext,它沒有定義configureSwapChain函數,如果編譯該行typescript會報錯,所以需要忽略錯誤。
第5行配置了swap chain。vulkan tutorial對此進行了說明:
swap chain是一個緩沖結構,webgpu會先將內容渲染到swap chain的buffer中,然后再將其顯示到屏幕上;
swap chain本質上是等待呈現在屏幕上的一個圖片隊列。
接下來就是創建render pipeline
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [] }),
vertexStage: {
module: device.createShaderModule({
code: glslang.compileGLSL(vertexShaderGLSL, "vertex"),
// @ts-ignore
source: vertexShaderGLSL,
transform: source => glslang.compileGLSL(source, "vertex"),
}),
entryPoint: "main"
},
fragmentStage: {
module: device.createShaderModule({
code: glslang.compileGLSL(fragmentShaderGLSL, "fragment"),
// @ts-ignore
source: fragmentShaderGLSL,
transform: source => glslang.compileGLSL(source, "fragment"),
}),
entryPoint: "main"
},
primitiveTopology: "triangle-list",
colorStates: [{
format: swapChainFormat,
}],
});
了解pipeline
WebGPU有兩種pipeline:render pipeline和compute pipeline,這里只用了render pipeline
這里使用render pipeline descriptor來創建render pipeline,它的定義如下:
dictionary GPUPipelineDescriptorBase : GPUObjectDescriptorBase {
required GPUPipelineLayout layout;
};
...
dictionary GPURenderPipelineDescriptor : GPUPipelineDescriptorBase {
required GPUProgrammableStageDescriptor vertexStage;
GPUProgrammableStageDescriptor fragmentStage;
required GPUPrimitiveTopology primitiveTopology;
GPURasterizationStateDescriptor rasterizationState = {};
required sequence<GPUColorStateDescriptor> colorStates;
GPUDepthStencilStateDescriptor depthStencilState;
GPUVertexStateDescriptor vertexState = {};
unsigned long sampleCount = 1;
unsigned long sampleMask = 0xFFFFFFFF;
boolean alphaToCoverageEnabled = false;
// TODO: other properties
};
render pipeline可以設置綁定的資源布局、編譯的shader、fixed functions(如混合、深度、模版、cullMode等各種狀態和頂點數據的格式vertexState),相對於WebGL(WebGL的一個API只能設置一個,如使用gl.cullFace設置cull mode),提升了性能(靜態設置了各種狀態,不需要在運行時設置),便於管理(把各個狀態集中到了一起設置)。
分析render pipeline descriptor
vertexStage和fragmentStage分別設置vertex shader和fragment shader:
使用第三方庫,將glsl編譯為字節碼(格式為SPIR-V);
source和transform字段是多余的,可以刪除。
因為shader沒有綁定資源(如uniform buffer, texture等),所以第2行的bindGroupLayouts為空數組,不需要bind group和bind group layout
第25行的primitiveTopology指定片元的拓撲結構,此處為三角形。
它可以為以下值:
enum GPUPrimitiveTopology {
"point-list",
"line-list",
"line-strip",
"triangle-list",
"triangle-strip"
};
現在先忽略colorStates
我們繼續分析后面的代碼,接下來定義了frame函數
frame函數定義了每幀執行的邏輯:
function frame() {
const commandEncoder = device.createCommandEncoder({});
const textureView = swapChain.getCurrentTexture().createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [{
attachment: textureView,
loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
}],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.endPass();
device.defaultQueue.submit([commandEncoder.finish()]);
}
return frame;
學習command buffer
我們不能直接操作command buffer,需要創建command encoder,使用它將多個commands(如render pass的draw)設置到一個command buffer中,然后執行submit,把command buffer提交到gpu driver的隊列中。
根據 webgpu設計文檔->Command Submission:
Command buffers carry sequences of user commands on the CPU side. They can be recorded independently of the work done on GPU, or each other. They go through the following stages:
creation -> "recording" -> "ready" -> "executing" -> done
我們知道,command buffer有
creation, recording,ready,executing,done五種狀態。
根據該文檔,結合代碼來分析command buffer的操作流程:
第2行創建command encoder時,應該是創建了command buffer,它的狀態為creation;
第12行開始render pass(webgpu還支持compute pass,不過這里沒用到),command buffer的狀態變為recording;
13-14行將“設置pipeline”、“繪制”的commands設置到command buffer中;
第15行結束render pass,(可以設置下一個pass,如compute pass,不過這里只用了一個pass);
第17行“commandEncoder.finish()”將command buffer的狀態變為ready;
然后執行subimit,command buffer狀態變為executing,被提交到gpu driver的隊列中,不能再在cpu端被操作;
如果提交成功,gpu會決定在某個時間處理它。
分析render pass
第5行的renderPassDescriptor描述了render pass,它的定義為:
dictionary GPURenderPassDescriptor : GPUObjectDescriptorBase {
required sequence<GPURenderPassColorAttachmentDescriptor> colorAttachments;
GPURenderPassDepthStencilAttachmentDescriptor depthStencilAttachment;
};
這里只用到了colorAttachments。它類似於WebGL->framebuffer的colorAttachments。這里只用到了一個color buffer attachment。
我們來看下colorAttachment的定義:
dictionary GPURenderPassColorAttachmentDescriptor {
required GPUTextureView attachment;
GPUTextureView resolveTarget;
required (GPULoadOp or GPUColor) loadValue;
GPUStoreOp storeOp = "store";
};
這里設置attachment,將其與swap chain關聯:
attachment: textureView,
我們現在忽略resolveTarget。
loadValue和storeOp決定渲染前和渲染后怎樣處理attachment中的數據。
我們看下它的類型:
enum GPULoadOp {
"load"
};
enum GPUStoreOp {
"store",
"clear"
};
...
dictionary GPUColorDict {
required double r;
required double g;
required double b;
required double a;
};
typedef (sequence<double> or GPUColorDict) GPUColor;
loadValue如果為GPULoadOp類型,則只有一個值:“load”,它的意思是渲染前保留attachment中的數據;
如果為GPUColor類型(如這里的{ r: 0.0, g: 0.0, b: 0.0, a: 1.0 }),則不僅為"load",而且設置了渲染前的初始值,類似於WebGL的clearColor。
storeOp如果為“store”,意思是渲染后保存被渲染的內容到內存中,后面可以被讀取;
如果為“clear”,意思是渲染后清空內容。
現在我們回頭看下render pipeline中的colorStates:
colorStates: [{
format: swapChainFormat,
}],
colorStates與colorAttachments對應,也只有一個,它的format應該與swap chain的format相同
我們繼續看render pass代碼:
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.endPass();
draw的定義為:
void draw(unsigned long vertexCount, unsigned long instanceCount,
unsigned long firstVertex, unsigned long firstInstance);
三角形有3個頂點,這里只繪制1個實例,兩者都從0開始(所以vertex shader中的gl_VertexIndex依次為0、1、2),所以第3行為“draw(3, 1, 0, 0)”
最終渲染結果
參考資料
webgpu-samplers Github Repo
vulkan tutorial
webgpu設計文檔->Command Submission
WebGPU-4