1. 程式人生 > >WebGPU學習(六):學習“rotatingCube”示例

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