WebGPU學習(六):學習“rotatingCube”示例
大家好,本文學習Chrome->webgpu-samplers->rotatingCube示例。
上一篇博文:
WebGPU學習(五): 現代圖形API技術要點和WebGPU支援情況調研
學習rotatingCube.ts
我們已經學習了“繪製三角形”的示例,與它相比,本示例增加了以下的內容:
- 增加一個uniform buffer object(簡稱為ubo),用於傳輸model矩陣view矩陣projection矩陣的結果矩陣(簡稱為mvp矩陣),並在每幀被更新
- 設定頂點
- 開啟面剔除
- 開啟深度測試
下面,我們開啟rotatingCube.ts檔案,依次來看下新增內容:
增加一個uniform buffer object
介紹
在WebGL 1中,我們通過uniform1i,uniform4fv等函式傳遞每個gameObject對應的uniform變數(如diffuseMap, diffuse color, model matrix等)到shader中。
其中很多相同的值是不需要被傳遞的,舉例如下:
如果gameObject1和gameObject3使用同一個shader1,它們的diffuse color相同,那麼只需要傳遞其中的一個diffuse color,而在WebGL 1中我們一般把這兩個diffuse color都傳遞了,造成了重複的開銷。
WebGPU使用uniform buffer object來傳遞uniform變數。uniform buffer是一個全域性的buffer,我們只需要設定一次值,然後在每次draw之前,設定使用的資料範圍(通過offset, size來設定),從而複用相同的資料。如果uniform值有變化,則只需要修改uniform buffer對應的資料。
在WebGPU中,我們可以把所有gameObject的model矩陣設為一個ubo,所有相機的view和projection矩陣設為一個ubo,每一種material(如phong material,pbr material等)的資料(如diffuse color,specular color等)設為一個ubo,每一種light(如direction light、point light等)的資料(如light color、light position等)設為一個ubo,這樣可以有效減少uniform變數的傳輸開銷。
另外,我們需要注意ubo的記憶體佈局:
預設的佈局為std140,我們可以粗略地理解為,它約定了每一列都有4個元素。
下面的ubo對應的uniform block,定義佈局為std140:
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
它在記憶體中的實際佈局為:
layout (std140) uniform ExampleBlock
{
// base alignment // aligned offset
float value; // 4 // 0
vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16)
mat4 matrix; // 16 // 32 (column 0)
// 16 // 48 (column 1)
// 16 // 64 (column 2)
// 16 // 80 (column 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
也就是說,這個ubo的第一個元素為value,第2-4個元素為0(為了對齊);
第5-7個元素為vector的x、y、z的值,第8個元素為0;
第9-24個元素為matrix的值(列優先);
第25-27個元素為values陣列的值,第28個元素為0;
第29個元素為boolean轉為float的值,第30-32個元素為0;
第33個元素為integer轉為float的值,第34-36個元素為0。
分析本示例對應的程式碼
- 在vertex shader中定義uniform block
程式碼如下:
const vertexShaderGLSL = `#version 450
layout(set = 0, binding = 0) uniform Uniforms {
mat4 modelViewProjectionMatrix;
} uniforms;
...
void main() {
gl_Position = uniforms.modelViewProjectionMatrix * position;
fragColor = color;
}
`;
佈局為預設的std140,指定了set和binding,包含一個mvp矩陣
- 建立uniformsBindGroupLayout
程式碼如下:
const uniformsBindGroupLayout = device.createBindGroupLayout({
bindings: [{
binding: 0,
visibility: 1,
type: "uniform-buffer"
}]
});
visibility為GPUShaderStage.VERTEX(等於1),指定type為“uniform-buffer”
- 建立uniform buffer
程式碼如下:
const uniformBufferSize = 4 * 16; // BYTES_PER_ELEMENT(4) * matrix length(4 * 4 = 16)
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
- 建立uniform bind group
程式碼如下:
const uniformBindGroup = device.createBindGroup({
layout: uniformsBindGroupLayout,
bindings: [{
binding: 0,
resource: {
buffer: uniformBuffer,
},
}],
});
- 每一幀更新uniform buffer的mvp矩陣資料
程式碼如下:
//因為是固定相機,所以只需要計算一次projection矩陣
const aspect = Math.abs(canvas.width / canvas.height);
let projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0);
...
//計算mvp矩陣
function getTransformationMatrix() {
let viewMatrix = mat4.create();
mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -5));
let now = Date.now() / 1000;
mat4.rotate(viewMatrix, viewMatrix, 1, vec3.fromValues(Math.sin(now), Math.cos(now), 0));
let modelViewProjectionMatrix = mat4.create();
mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix);
return modelViewProjectionMatrix;
}
...
return function frame() {
uniformBuffer.setSubData(0, getTransformationMatrix());
...
}
- draw之前設定bind group
程式碼如下:
return function frame() {
...
passEncoder.setBindGroup(0, uniformBindGroup);
passEncoder.draw(36, 1, 0, 0);
...
}
詳細分析“更新uniform buffer”
本示例使用setSubData來更新uniform buffer:
return function frame() {
uniformBuffer.setSubData(0, getTransformationMatrix());
...
}
我們在WebGPU學習(五): 現代圖形API技術要點和WebGPU支援情況調研->Approaching zero driver overhead->persistent map buffer中,提到了WebGPU目前有兩種方法實現“CPU把資料傳輸到GPU“,即更新GPUBuffer的值:
1.呼叫GPUBuffer->setSubData方法
2.使用persistent map buffer技術
我們看下如何在本示例中使用第2種方法:
function setBufferDataByPersistentMapBuffer(device, commandEncoder, uniformBufferSize, uniformBuffer, mvpMatricesData) {
const [srcBuffer, arrayBuffer] = device.createBufferMapped({
size: uniformBufferSize,
usage: GPUBufferUsage.COPY_SRC
});
new Float32Array(arrayBuffer).set(mvpMatricesData);
srcBuffer.unmap();
commandEncoder.copyBufferToBuffer(srcBuffer, 0, uniformBuffer, 0, uniformBufferSize);
const commandBuffer = commandEncoder.finish();
const queue = device.defaultQueue;
queue.submit([commandBuffer]);
srcBuffer.destroy();
}
return function frame() {
//uniformBuffer.setSubData(0, getTransformationMatrix());
...
const commandEncoder = device.createCommandEncoder({});
setBufferDataByPersistentMapBuffer(device, commandEncoder, uniformBufferSize, uniformBuffer, getTransformationMatrix());
...
}
為了驗證效能,我做了benchmark測試,建立一個ubo,包含160000個mat4,進行js profile:
使用setSubData(呼叫setBufferDataBySetSubData函式):
setSubData佔91.54%
使用persistent map buffer(呼叫setBufferDataByPersistentMapBuffer函式):
createBufferMapped和setBufferDataByPersistentMapBuffer佔72.72+18.06=90.78%
可以看到兩個的效能差不多。但考慮到persistent map buffer從實現原理上要更快(cpu和gpu共用一個buffer,不需要copy),因此應該優先使用該方法。
另外,WebGPU社群現在還在討論如何優化更新buffer資料(如有人提出增加GPUUploadBuffer pass),因此我們還需要繼續關注該方面的進展。
參考資料
Advanced-GLSL->Uniform buffer objects
設定頂點
- 傳輸頂點的position和color資料到vertex shader的attribute(in)中
程式碼如下:
const vertexShaderGLSL = `#version 450
...
layout(location = 0) in vec4 position;
layout(location = 1) in vec4 color;
layout(location = 0) out vec4 fragColor;
void main() {
gl_Position = uniforms.modelViewProjectionMatrix * position;
fragColor = color;
}
const fragmentShaderGLSL = `#version 450
layout(location = 0) in vec4 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = fragColor;
}
`;
這裡設定color為fragColor(out,相當於WebGL 1的varying變數),然後在fragment shader中接收fragColor,將其設定為outColor,從而將fragment的color設定為對應頂點的color
- 建立vertices buffer,設定立方體的頂點資料
程式碼如下:
cube.ts:
//每個頂點包含position,color,uv資料
export const cubeVertexArray = new Float32Array([
// float4 position, float4 color, float2 uv,
1, -1, 1, 1, 1, 0, 1, 1, 1, 1,
-1, -1, 1, 1, 0, 0, 1, 1, 0, 1,
-1, -1, -1, 1, 0, 0, 0, 1, 0, 0,
1, -1, -1, 1, 1, 0, 0, 1, 1, 0,
1, -1, 1, 1, 1, 0, 1, 1, 1, 1,
-1, -1, -1, 1, 0, 0, 0, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, -1, 1, 1, 1, 0, 1, 1, 0, 1,
1, -1, -1, 1, 1, 0, 0, 1, 0, 0,
1, 1, -1, 1, 1, 1, 0, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, -1, -1, 1, 1, 0, 0, 1, 0, 0,
-1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 0, 1,
1, 1, -1, 1, 1, 1, 0, 1, 0, 0,
-1, 1, -1, 1, 0, 1, 0, 1, 1, 0,
-1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
1, 1, -1, 1, 1, 1, 0, 1, 0, 0,
-1, -1, 1, 1, 0, 0, 1, 1, 1, 1,
-1, 1, 1, 1, 0, 1, 1, 1, 0, 1,
-1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
-1, -1, -1, 1, 0, 0, 0, 1, 1, 0,
-1, -1, 1, 1, 0, 0, 1, 1, 1, 1,
-1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-1, 1, 1, 1, 0, 1, 1, 1, 0, 1,
-1, -1, 1, 1, 0, 0, 1, 1, 0, 0,
-1, -1, 1, 1, 0, 0, 1, 1, 0, 0,
1, -1, 1, 1, 1, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, -1, -1, 1, 1, 0, 0, 1, 1, 1,
-1, -1, -1, 1, 0, 0, 0, 1, 0, 1,
-1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
1, 1, -1, 1, 1, 1, 0, 1, 1, 0,
1, -1, -1, 1, 1, 0, 0, 1, 1, 1,
-1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
]);
rotatingCube.ts:
const verticesBuffer = device.createBuffer({
size: cubeVertexArray.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
});
verticesBuffer.setSubData(0, cubeVertexArray);
因為只需要設定一次頂點資料,所以這裡可以使用setSubData來設定,對效能影響不大
- 建立render pipeline時,指定vertex shader的attribute
程式碼如下:
cube.ts:
export const cubeVertexSize = 4 * 10; // Byte size of one cube vertex.
export const cubePositionOffset = 0;
export const cubeColorOffset = 4 * 4; // Byte offset of cube vertex color attribute.
rotatingCube.ts:
const pipeline = device.createRenderPipeline({
...
vertexState: {
vertexBuffers: [{
arrayStride: cubeVertexSize,
attributes: [{
// position
shaderLocation: 0,
offset: cubePositionOffset,
format: "float4"
}, {
// color
shaderLocation: 1,
offset: cubeColorOffset,
format: "float4"
}]
}],
},
...
});
- render pass->draw指定頂點個數為36
程式碼如下:
return function frame() {
...
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
...
passEncoder.draw(36, 1, 0, 0);
passEncoder.endPass();
...
}
開啟面剔除
相關程式碼為:
const pipeline = device.createRenderPipeline({
...
rasterizationState: {
cullMode: 'back',
},
...
});
相關的定義為:
enum GPUFrontFace {
"ccw",
"cw"
};
enum GPUCullMode {
"none",
"front",
"back"
};
...
dictionary GPURasterizationStateDescriptor {
GPUFrontFace frontFace = "ccw";
GPUCullMode cullMode = "none";
...
};
其中ccw表示逆時針,cw表示順時針。
因為本示例設定了cullMode為back,沒有設定frontFace(frontFace為預設的ccw),所以WebGPU會把逆時針方向設為外側,把所有背面的三角形(頂點連線方向為內側,即順時針方向的三角形)剔除掉
參考資料
[WebGL入門]六,頂點和多邊形
Investigation: Rasterization State
開啟深度測試
現在分析相關程式碼,並忽略與模版測試相關的程式碼:
- 建立render pipeline時,設定depthStencilState
程式碼如下:
const pipeline = device.createRenderPipeline({
...
depthStencilState: {
//開啟深度測試
depthWriteEnabled: true,
//設定比較函式為less,後面會繼續說明
depthCompare: "less",
//設定depth為24bit
format: "depth24plus-stencil8",
},
...
});
- 建立depth texture(注意它的size->depth為1,格式也為24bit),將它的view設定為render pass->depth和stencil attachment->attachment
程式碼如下:
const depthTexture = device.createTexture({
size: {
width: canvas.width,
height: canvas.height,
depth: 1
},
format: "depth24plus-stencil8",
usage: GPUTextureUsage.OUTPUT_ATTACHMENT
});
const renderPassDescriptor: GPURenderPassDescriptor = {
...
depthStencilAttachment: {
attachment: depthTexture.createView(),
depthLoadValue: 1.0,
depthStoreOp: "store",
...
}
};
其中,depthStencilAttachment的定義為:
dictionary GPURenderPassDepthStencilAttachmentDescriptor {
required GPUTextureView attachment;
required (GPULoadOp or float) depthLoadValue;
required GPUStoreOp depthStoreOp;
...
};
depthLoadValue和depthStoreOp與WebGPU學習(二): 學習“繪製一個三角形”示例->分析render pass->colorAttachment的loadOp和StoreOp類似,我們直接分析本示例的相關程式碼:
const pipeline = device.createRenderPipeline({
...
depthStencilState: {
...
depthCompare: "less",
...
},
...
});
...
const renderPassDescriptor: GPURenderPassDescriptor = {
...
depthStencilAttachment: {
...
depthLoadValue: 1.0,
depthStoreOp: "store",
...
}
};
在深度測試時,gpu會將fragment的z值(範圍為[0.0-1.0])與這裡設定的depthLoadValue值(這裡為1.0)比較。其中比較的函式使用depthCompare定義的函式(這裡為less,意思是所有z值大於等於1.0的fragment會被剔除)
參考資料
Depth testing
最終渲染結果
參考資料
WebGPU規範
webgpu-samplers Github Repo
WebGP