WebGL 與 WebGPU比對[4] - Uniform
眾所周知,在 GPU 跑可程式設計管線的時候,著色器是並行執行的,每個著色器入口函式都會在 GPU 中並行執行。每個著色器對一大片統一格式的資料進行衝鋒,體現 GPU 多核心的優勢,可以小核同時處理資料;不過,有的資料對每個著色器都是一樣的,這種資料的型別是“uniform”,也叫做統一值。
這篇文章羅列了原生 WebGL 1/2 中的 uniform 資料,以及 WebGPU 中的 uniform 資料,有一些例子供參考,以用來比對它們之間的差異。
1. WebGL 1.0 Uniform
1.1. 用 WebGLUniformLocation 定址
在 WebGL 1.0 中,通常是在 JavaScript 端儲存 WebGLUniformLocation
使用 gl.getUniformLocation()
方法獲取這個 location,有如下幾種方式
- 全名:
gl.getUniformLocation(program, 'u_someUniformVar')
- 分量:通常是向量的一部分,譬如
gl.getUniformLocation(program, 'u_someVec3[0]')
是獲取第 0 個元素(元素型別是 vec3)的 location - 結構體成員:
gl.getUniformLocation(program, 'u_someStruct.someMember')
上面三種情況與之對應的著色器程式碼:
// 全名
uniform float u_someUniformVar;
// 分量
uniform vec3 u_someVec3[3]; // 注意,這裡是 3 個 vec3
// 結構體成員
struct SomeStruct {
bool someMember;
};
uniform SomeStruct u_someStruct;
傳值分三類,標量/向量、矩陣、取樣紋理,見下文。
1.2. 矩陣賦值用 uniformMatrix[234]fv
對於矩陣,使用 gl.uniformMatrix[234]fv()
方法即可傳遞,其中,f 代表 float,v 代表 vector,即傳入引數要是一個向量(即陣列);
以傳遞一個 4×4 的矩陣為例:
// 獲取 location(初始化時)
const matrixLocation = gl.getUniformLocation(program, "u_matrix")
// 建立或更新列主序變換矩陣(渲染時)
const matrix = [/* ... */]
// 傳遞值(渲染時)
gl.uniformMatrix4fv(matrixLocation, false, matrix)
1.3. 標量與向量用 uniform[1234][fi][v]
對於普通標量和向量,使用 gl.uniform[1234][fi][v]()
方法即可傳遞,其中,1、2、3、4 代表標量或向量的維度(1就是標量啦),f/i 代表 float 或 int,v 代表 vector(即你傳遞的資料在著色器中將解析為向量陣列)。
舉例:
- 語句1,
gl.uniform1fv(someFloatLocation, [4.5, 7.1])
- 語句2,
gl.uniform4i(someIVec4Location, 5, 2, 1, 3)
- 語句3,
gl.uniform4iv(someIVec4Location, [5, 2, 1, 3, 2, 12, 0, 6])
- 語句4,
gl.uniform3f (someVec3Location, 7.1, -0.8, 2.1)
上述 4 個賦值語句對應的著色器中的程式碼為:
// 語句 1 可以適配 1~N 個浮點數
// 只傳單元素陣列時,可直接宣告 uniform float u_someFloat;
uniform float u_someFloat[2];
// 語句 2 適配一個 ivec4
uniform ivec4 u_someIVec4;
// 語句 3 適配 1~N 個 ivec4
// 只傳單元素陣列時,可直接宣告 uniform float u_someIVec4;
uniform ivec4 u_someIVec4[2];
// 語句 4 適配一個 vec3
uniform vec3 u_someVec3;
到了 WebGL 2.0,在組分值型別會有一些擴充,請讀者自行查閱相關文件。
1.4. 傳遞紋理
在頂點著色器階段,可以使用頂點的紋理座標對紋理進行取樣:
attribute vec3 a_pos;
attribute vec2 a_uv;
uniform sampler2D u_texture;
varying vec4 v_color;
void main() {
v_color = texture2D(u_texture, a_uv);
gl_Position = a_pos; // 假設頂點不需要變換
}
那麼,在 JavaScript 端,可以使用 gl.uniform1i()
來告訴著色器我把紋理剛剛傳遞到哪個紋理坑位上了:
const texture = gl.createTexture()
const samplerLocation = gl.getUniformLocation(/* ... */)
// ... 設定紋理資料 ...
gl.activeTexture(gl[`TEXTURE${5}`]) // 告訴 WebGL 使用第 5 個坑上的紋理
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.uniform1i(samplerLocation, 5) // 告訴著色器待會讀紋理的時候去第 5 個坑位讀
2. WebGL 2.0 Uniform
2.1. 標量/向量/矩陣傳值方法擴充
WebGL 2.0 的 Uniform 系統對非方陣型別的矩陣提供了支援,例如
const mat2x3 = [
1, 2, 3,
4, 5, 6,
]
gl.uniformMatrix2x3fv(loc, false, mat2x3)
上述方法傳遞的是 4×3
的矩陣。
而對於單值和向量,額外提供了無符號數值的方法,即由 uniform[1234][fi][v]
變成了 uniform[1234][f/ui][v]
,也就是下面 8 個新增方法:
gl.uniform1ui(/* ... */) // 傳遞資料至 1 個 uint
gl.uniform2ui(/* ... */) // 傳遞資料至 1 個 uvec2
gl.uniform3ui(/* ... */) // 傳遞資料至 1 個 uvec3
gl.uniform4ui(/* ... */) // 傳遞資料至 1 個 uvec4
gl.uniform1uiv(/* ... */) // 傳遞資料至 uint 陣列
gl.uniform2uiv(/* ... */) // 傳遞資料至 uvec2 陣列
gl.uniform3uiv(/* ... */) // 傳遞資料至 uvec3 陣列
gl.uniform4uiv(/* ... */) // 傳遞資料至 uvec4 陣列
對應 GLSL300 中的 uniform 為:
#version 300 es
#define N ? // N 取決於你的需要,JavaScript 傳遞的數量也要匹配
uniform uint u_someUint;
uniform uvec2 u_someUVec2;
uniform uvec3 u_someUVec3;
uniform uvec4 u_someUVec4;
uniform uint u_someUintArr[N];
uniform uvec2 u_someUVec2Arr[N];
uniform uvec3 u_someUVec3Arr[N];
uniform uvec4 u_someUVec4Arr[N];
需要額外注意的是,uint/uvec234
這些型別在高版本的 glsl 才能使用,也就是說不向下相容 WebGL 1.0 及 GLSL100.
然而,WebGL 2.0 帶來的不單單只是這些小修小補,最重要的莫過於 UBO 了,馬上開始。
2.1. 什麼是 UniformBlock 與 UniformBuffer 的建立
在 WebGL 1.0 的時候,任意種類的統一值一次只能設定一個,如果一幀內 uniform 有較多更新,對於 WebGL 這個狀態機來說不是什麼好事,會帶來額外的 CPU 至 GPU 端的傳遞開銷。
在 WebGL 2.0,允許一次傳送一堆 uniform,這一堆 uniform 的聚合體,就叫做 UniformBuffer,具體到程式碼中:
先是 GLSL 300
uniform Light {
highp vec3 lightWorldPos;
mediump vec4 lightColor;
};
然後是 JavaScript
const lightUniformBlockBuffer = gl.createBuffer()
const lightUniformBlockData = new Float32Array([
0, 10, 30, 0, // vec3, 光源位置, 為了 8 Byte 對齊填充一個尾 0
1, 1, 1, 1, // vec4, 光的顏色
])
gl.bindBuffer(gl.UNIFORM_BUFFER, lightUniformBlockBuffer);
gl.bufferData(gl.UNIFORM_BUFFER, lightUniformBlockData, gl.STATIC_DRAW);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, lightUniformBlockBuffer)
先別急著問為什麼,一步一步來。
首先你看到了,在 GLSL300 中允許使用類似結構體一樣的塊狀語法宣告多個 Uniform 變數,這裡用到了光源的座標和光源的顏色,分別使用了不同的精度和資料型別(vec3、vec4)。
隨後,在 JavaScript 端,你看到了用新增的方法 gl.bindBufferBase()
來繫結一個 WebGLBuffer
到 0 號位置,這個 lightUniformBlockBuffer
其實就是集合了兩個 Uniform 變數的 UniformBufferObject (UBO)
,在著色器中那塊被命名為 Light
的花括號區域,則叫 UniformBlock
.
其實,建立一個 UBO
和建立普通的 VBO
是一樣的,繫結、賦值操作也幾乎一致(第一個引數有不同)。只不過 UBO 可能更需要考慮數值上的設計,例如 8 位元組對齊等,通常會在設計著色器的時候把相同資料型別的 uniform 變數放在一起,達到記憶體使用上的最佳化。
2.2. 狀態繫結
在 WebGL 2.0 中,JavaScript 端允許你把著色器程式中的 UniformBlock 位置繫結到某個變數中:
const viewUniformBufferIndex = 0;
const materialUniformBufferIndex = 1;
const modelUniformBufferIndex = 2;
const lightUniformBufferIndex = 3;
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'View'), viewUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Model'), modelUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Material'), materialUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Light'), lightUniformBufferIndex);
這裡,使用的是 gl.getUniformBlockIndex()
獲取 UniformBlock 在著色器程式中的位置,而把這個位置繫結到你喜歡的數字上的是 gl.uniformBlockBinding()
方法。
這樣做有個好處,你可以在你的程式里人為地規定各個 UniformBlock 的順序,然後用這些 index 來更新不同的 UBO.
// 使用不同的 UBO 更新 materialUniformBufferIndex (=1) 指向的 UniformBlock
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, redMaterialUBO)
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, greenMaterialUBO)
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, blueMaterialUBO)
當然,WebGL 2.0 對 Uniform 還有別的擴充,此處不再列舉。
bindBufferBase 的作用類似於 enableVertexAttribArray,告訴 WebGL 我馬上就要用哪個坑了。
2.3. 著色器中的 Uniform
著色器使用 GLSL300 語法才能使用 UniformBlock 和 新的資料型別,除此之外和 GLSL100 沒啥區別。當然,GLSL300 有很多新語法,這裡只撿一些關於 Uniform 的來寫。
關於 uint/uvec234
型別,在 2.1 節已經有例子了,這裡不贅述。
而關於 UniformBlock,還有一點需要補充的,那就是“命名”問題。
UniformBlock 的語法如下:
uniform <BlockType> {
<BlockBody>
} ?<blockName>;
// 舉例:具名定義
uniform Model {
mat4 world;
mat4 worldInverseTranspose;
} model;
// 舉例:不具名定義
uniform Light {
highp vec3 lightWorldPos;
mediump vec4 lightColor;
};
如果使用具名定義,那麼訪問 Block 內的成員就需要使用它的 name 了,例如 model.world
、model.worldInverseTranspose
等。
舉完整的例子如下:
#version 300 es
precision highp float;
precision highp int;
// uniform 塊的佈局控制
layout(std140, column_major) uniform;
// 宣告 uniform 塊:Transform,命名為 transform 供主程式使用
// 也可以不命名,就直接用 mvpMatrix 即可
uniform Transform
{
mat4 mvpMatrix;
} transform;
layout(location = 0) in vec2 pos;
void main() {
gl_Position = transform.mvpMatrix * vec4(pos, 0.0, 1.0);
}
注意,即使給 UniformBlock 命名為 transform,但是立面的 mvpMatrix 是不能與其它 Block 裡面的成員共名的,transform 沒有名稱空間的作用。
再看 JavaScript:
//#region 獲取著色器程式中的 uniform 位置並繫結
const uniformTransformLocation = gl.getUniformBlockIndex(program, 'Transform')
gl.uniformBlockBinding(program, uniformTransformLocation, 0)
//endregion
//#region 建立 ubo
const uniformTransformBuffer = gl.createBuffer()
//#endregion
//#region 建立矩陣所需的 ArrayBufferView,列主序
const transformsMatrix = new Float32Array([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
//#endregion
//#region 傳遞資料給 WebGLBuffer
gl.bindBuffer(gl.UNIFORM_BUFFER, uniformTransformBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, transformsMatrix, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
//#endregion
// ---------- 在你需要繪製時 ----------
//#region 繫結 ubo 到 0 號索引上的 uniformLocation 以供著色器使用
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniformTransformBuffer)
// ... 渲染
// -------------
2.4. 傳遞紋理
紋理與 WebGL 1.0 一致,但是 GLSL300 的紋理函式有變,讀者請自行查詢資料比對。
3. WebGPU Uniform
WebGPU 有三個型別的 Uniform 資源:標量/向量/矩陣、紋理、取樣器。
各自有各自的容器,第一種統一使用 GPUBuffer
,也就是所謂的 UBO;第二和第三種使用 GPUTexture
和 GPUSampler
.
3.1. 三類資源的建立與打組傳遞
上述三類資源,把它們通過打成一組,也就是 GPUBindGroup
,我叫它資源繫結組,進而傳遞給組織了著色器模組(GPUShaderModule
)的各種管線(GPURenderPipeline
、GPUComputePipeline
)。
統一起來好辦事,這裡為節約篇幅,資料傳遞就不再細說,著重看看它們的打組成繫結組的程式碼:
const someUbo = device.createBuffer({ /* 注意 usage 要有 UNIFORM */ })
const texture = device.createTexture({ /* 建立常規紋理 */ })
const sampler = device.createSampler({ /* 建立常規取樣器 */ })
// 佈局物件聯絡管線佈局和繫結組本身
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0, // <- 繫結在 0 號資源
visibility: GPUShaderStage.FRAGMENT,
sampler: {
type: 'filtering'
}
},
{
binding: 1, // <- 繫結在 1 號資源
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'float'
}
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: 'uniform'
}
}
]
})
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: sampler, // <- 傳入取樣器物件
},
{
binding: 1,
resource: texture.createView() // <- 傳入紋理物件的檢視
},
{
binding: 2,
resource: {
buffer: someUbo // <- 傳入 UBO
}
}
]
})
// 管線
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout]
})
const renderingPipeline = device.createRenderPipeline({
layout: pipelineLayout
// ... 其它配置
})
// ... renderPass 切換 pipeline 和 bindGroup 進行繪製 ...
3.2. 更新 Uniform 與繫結組的意義
更新 Uniform 資源其實很簡單。
如果是 UBO,一般會更新前端修改的燈光、材質、時間幀引數以及單幀變化的矩陣等,使用 device.queue.writeBuffer
即可:
device.queue.writeBuffer(
someUbo, // 傳給誰
0,
buffer, // 傳遞 ArrayBuffer,即當前幀中的新資料
byteOffset, // 從哪裡開始
byteLength // 取多長
)
使用 writeBuffer 就可以保證用的還是原來建立那個 GPUBuffer,它與繫結組、管線的繫結關係還在;不用對映、解對映的方式傳值是減少 CPU/GPU 雙端通訊成本
如果是紋理,那就用 影象拷貝操作 中的幾個方法進行紋理物件更新;
一般不直接對取樣器和紋理的更新,而是在編碼器上切換不同的繫結組來切換管線所需的資源。尤其是紋理,若頻繁更新資料,CPU/GPU 雙端通訊成本會增加的。
延遲渲染、離屏繪製等需要更新顏色附件的,其實只需要建立新的 colorAttachments 物件即可實現“上一幀繪製的下一幀我能用”,不需要直接從 CPU 記憶體再刷入資料到 GPU 中。
更新 Uniform 需要對每一幀幾乎都要改的、幾乎不變的資源進行合理分組,分到不同的繫結組中,這樣就可以有針對性地更新,而無需把管線、繫結組重設一次,僅僅在通道編碼器上進行切換即可。
3.3. 著色器中的 Uniform
此處不涉及太多 WGSL 語法。
與 UniformBlock 類似,需要指定“一塊東西”,WGSL 直接使用的結構體。
首先,是 UBO:
// -- 頂點著色器 --
// 宣告一個結構體型別
struct Uniforms {
modelViewProjectionMatrix: mat4x4<f32>;
};
// 宣告指定其繫結ID是0,繫結組序號是0
@binding(2)
@group(0)
var<uniform> myUniforms: Uniforms;
// —— 然後這個 myUniforms 變數就可以在函式中呼叫了 ——
然後是紋理和取樣器:
@group(0)
@binding(1)
var mySampler: sampler;
@group(0)
@binding(2)
var myTexture: texture_2d<f32>;
// ... 片元著色器主函式中進行紋理取樣
textureSample(myTexture, mySampler, fragUV);
4. 對比總結
WebGL 以 2 為比對基準,它與 WebGPU 相比,沒有資源繫結組,沒有采樣器物件(取樣引數通過另外的方法設定)。
比起 WebGPU 的傳 descriptor 式的寫法,使用一條條方法切換 UniformBlock、紋理等資源可能會有所遺漏,這是全域性狀態寫法的特點之一。當然,上層封裝庫會幫我們遮蔽這些問題的。
與語法風格相比,其實 WebGPU 改進的更多的是這些 uniform 在每一幀更新時 CPU 到GPU 的負載問題,它是事先由編碼器編碼成指令緩衝最後一次性發送的,比起 WebGL 一條一條傳送是更優的,在圖形渲染、GPU運算這種地方,積少成多,效能就高了起來。
關於 WebGL 2.0 的 Uniform 和 GLSL300 我學識不精,若有錯誤請指出。