WebGL 與 WebGPU比對[3] - 頂點緩衝


1. WebGL 中的 VBO

1.1. 創建 WebGLBuffer

WebGL 使用 TypedArray 進行數據傳遞,這點 WebGPU 也是一樣的。

下面的代碼是 WebGL 1.0 常規的 VertexBuffer 創建、賦值、配置過程。

const positions = [
  0, 0,
  0, 0.5,
  0.7, 0,
]

/*
創建着色器程序 program...
*/

// 獲取 vertex attribute 在着色器中的位置
const positionAttributeLocation = gl.getAttribLocation(program, "a_position")

//#region 創建 WebGLBuffer 並綁定,隨即寫入數據
const positionBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
//#endregion

//#region 啓用頂點着色器中對應的 attribute,再次綁定數據,並告知 WebGL 如何讀取 VertexBuffer
gl.enableVertexAttribArray(positionAttributeLocation)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset)
//#endregion

WebGL 通過 gl 變量的 createBufferbindBufferbufferData 方法來創建緩衝、綁定當前要用什麼緩衝及緩衝的用途、向緩衝傳遞 CPU 端的 TypedArray 數據並指明繪製模式,通過 gl 變量的 enableVertexAttribArrayvertexAttribPointer 方法來啓用着色器中 attribute 的坑位、告訴着色器如何從 VertexBuffer 中獲取頂點數據。

1.2. 頂點着色器

一個非常簡單的頂點着色器:

precision mediump float;
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0.0, 0.0);
}

如果用高版本的語法(譬如 WebGL 2.0 中用更高版本的 glsl 語法),你可以這樣寫:

#version 300 es
precision mediump float;
layout(location = 0) in vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0.0, 0.0);
}

2. WebGPU

2.1. 創建 GPUBuffer 與傳遞數據

const verticesData = [
  // 座標 xy      // 顏色 RGBA
  -0.5, 0.0,     1.0, 0.0, 0.0, 1.0, // ← 頂點 1
  0.0, 0.5,      0.0, 1.0, 0.0, 1.0, // ← 頂點 2
  0.5, 0.0,      0.0, 0.0, 1.0, 1.0  // ← 頂點 3
])
const verticesBuffer = device.createBuffer({
  size: vbodata.byteLength,
  usage: GPUBufferUsage.VERTEX,
  mappedAtCreation: true // 創建時立刻映射,讓 CPU 端能讀寫數據
})

// 讓 GPUBuffer 映射出一塊 CPU 端的內存,即 ArrayBuffer,此時這個 Float32Array 仍是空的
const verticesBufferArray = new Float32Array(verticesBuffer.getMappedRange())

// 將數據傳入這個 Float32Array
verticesBufferArray.set(verticesData)
// 令 GPUBuffer 解除映射,此時 verticesBufferArray 那塊內存才能被 GPU 訪問
verticesBuffer.unmap()

WebGPU 創建 VertexBuffer 是調取設備對象的 createBuffer 方法,返回一個 GPUBuffer 對象,它所需要的是指定 GPUBuffer 的類型以及緩衝的大小。如何寫入這塊緩衝呢?那還要提到“映射”這個概念。

映射簡單的說就是讓 CPU/GPU 單邊訪問。此處創建 GPUBuffer 的參數中有一個 mappedAtCreation 表示創建時就映射。

關於 WebGPU 中 Buffer 的映射、解映射,我有一篇專門的文章介紹,這裏不展開過多了。

上面代碼中 verticesBuffer.getMappedRange() 返回的是一個 ArrayBuffer,隨後才進行 set 操作來填充數據。數據填充完畢後,還需要 unmap 來解映射,以供後續 GPU 能訪問。

2.2. 將頂點緩衝的格式信息傳遞給頂點着色器

頂點着色階段是 渲染管線(GPURenderPipeline) 的一個組成部分,管線需要知道頂點緩衝的數據規格,由着色器模塊告知。

創建渲染管線需要 着色器模塊對象(GPUShaderModule,頂點着色器模塊的創建參數就有一個 buffers 屬性,是一個數組,用於描述頂點着色器中訪問到的頂點數據規格:

const vsShaderModule = device.createShaderModule({
  // ...
  buffers: [
    {
      // 2 個 float32 代表 xy 座標
      shaderLocation: 0,
      offset: 0,
      format: 'float32x2'
    }, {
      // 4 個 float32 代表 rgba 色值
      shaderLocation: 1,
      offset: 2 * verticesData.BYTES_PER_ELEMENT,
      format: 'float32x4'
    }
  ]
})

詳細資料可查閱官方 API 文檔中關於設備對象的 createShaderModule 方法的要求。

2.3. 在渲染通道中設置頂點緩衝

使用 渲染通道編碼器(GPURenderPassEncoder 來編碼單個渲染通道的全流程,其中有一步要設置該通道的頂點緩衝。這個比較簡單:

// ...
renderPassEncoder.setVertexBuffer(0, verticesBuffer)
// ...

2.4. 頂點着色器

struct PositionColorInput {
  @location(0) in_position_2d: vec2<f32>;
  @location(1) in_color_rgba: vec4<f32>;
};

struct PositionColorOutput {
  @builtin(position) coords_output: vec4<f32>;
  @location(0) color_output: vec4<f32>;
};

@stage(vertex)
fn main(input: PositionColorInput) 
    -> PositionColorOutput {
  var output: PositionColorOutput;
  output.color_output = input.in_color_rgba;
  output.coords_output = vec4<f32>(input.in_position_2d, 0.0, 1.0);
  return output;
}

WGSL 着色器代碼可以自定義頂點着色器的入口函數名稱、傳入參數的結構,也可以自定義向下一階段輸出(即返回值)的結構。

可以看到,爲了接收來自 WebGPU API 傳遞進來的頂點屬性,即自定義結構中的 PositionColorInput 結構體中的 xy 座標 in_position_2d,以及顏色值 in_color_rgba,需要有一個“特性”,叫做 location,它括號裏的值與着色器模塊對象中的 shaderLocation 必須對應上。

而對於輸出,代碼中則對應了結構體 PositionColorOutput,其中向下一階段(即片段着色階段)輸出用到了內置特性(builtin),叫做 position,以及自定義的一個 vec4:color_output,它是片段着色器中光柵化後的顏色,這兩個輸出,類似 glsl 中的 varying(或者out)作用。

2.5. 關於緩衝數據在內存與顯存中的申請、傳遞與銷燬

創建 GPUBuffer 的時候,如果沒有 mappedAtCreation: true,那麼內存、顯存都沒有被申請。

經過代碼測試,當執行映射請求且成功映射後,內存就會佔用掉對應的 GPUBuffer 的 size,此時完成了 ArrayBuffer 的創建,是要佔空間的。

那麼什麼時候顯存會被申請呢?猜測是 device.queue.commit() 時,指令緩衝攜帶着各種通道、各種 Buffer 一併傳遞給 GPU,執行指令緩衝,希望有高手測試我的猜測。

至於銷燬,我使用 destory 方法測試 CPU 的內存情況,發現兩分鐘內並未回收,這一點待測試 ArrayBuffer 的回收情況。

3. 比對

gl.vertexAttribPointer() 方法的作用類似於 device.createShaderModule()buffers 的作用,告訴着色器頂點緩衝單個頂點的數據規格。

gl.createBuffer()device.createBuffer() 是類似的,都是創建一個 CPU 端內存中的 Buffer 對象,但實際並沒有傳入數據。

數據傳遞則不大一致了,WebGL 同一時刻只能指定一個 VertexBuffer,所以 gl.bindBuffer()gl.bufferData() 一系列函數調用下來都沿着邏輯走;而 WebGPU 則需要經過映射和解映射。

在 WebGPU 中最重要的是,在 renderPassEncoder 記錄發出 draw 指令之前,要調用 renderPassEncoder.setVertexBuffer() 方法顯式指定用哪一個 VertexBuffer。

着色器代碼請讀者自行比對研究,只是語法上的差異。

4. VertexArrayObject

VAO 我也寫過一篇《WebGPU 中消失的 VAO》,這裏就不詳細展開了,有興趣的讀者請移步我的博客列表找找。

WebGPU 中已經不需要 VAO 了,源於 WebGPU 的機制與 WebGL 不同,VAO 本身是 OpenGL 體系提出的概念,它能節約 WebGL 切換頂點相關狀態時的負擔,也就是幫你緩存下來一個 VBO 的設定狀態,而無需再 gl.bindBuffer()gl.bufferData()gl.vertexAttribPointer() 等再來一遍。

WebGPU 的裝配式思想天然就與 VAO 是一致的。VAO 的職能轉交給 GPURenderPipeline 完成,其創建參數 GPURenderPipelineDescriptor.vertex.buffers 屬性是 GPUVertexBufferLayout[] 類型的,這每一個 GPUVertexBufferLayout 對象就有一部分 VAO 的職能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章