1. 程式人生 > >WebGPU學習(二): 學習“繪製一個三角形”示例

WebGPU學習(二): 學習“繪製一個三角形”示例

大家好,本文學習Chrome->webgl-samplers->helloTriangle示例。

上一篇文章:WebGPU學習(一): 開篇

準備Sample程式碼

克隆webgl-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)”

最終渲染結果

參考資料

webgl-samplers Github Repo
vulkan tutorial
webgpu設計文件->Command Submission
WebGPU-4