WebGPU學習(六):學習「rotatingCube」示例

你們好,本文學習Chrome->webgpu-samplers->rotatingCube示例。html

上一篇博文:
WebGPU學習(五): 現代圖形API技術要點和WebGPU支持狀況調研git

下一篇博文:
WebGPU學習(七):學習「twoCubes」和「instancedCube」示例github

學習rotatingCube.ts

咱們已經學習了「繪製三角形」的示例,與它相比,本示例增長了如下的內容:web

  • 增長一個uniform buffer object(簡稱爲ubo),用於傳輸model矩陣view矩陣projection矩陣的結果矩陣(簡稱爲mvp矩陣),並在每幀被更新
  • 設置頂點
  • 開啓面剔除
  • 開啓深度測試

下面,咱們打開rotatingCube.ts文件,依次來看下新增內容:canvas

增長一個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對應的數據。app

在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變量的傳輸開銷。less

另外,咱們須要注意ubo的內存佈局:
默認的佈局爲std140,咱們能夠粗略地理解爲,它約定了每一列都有4個元素。
咱們來舉例說明:
下面的ubo對應的uniform block,定義佈局爲std140:ide

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函數):
截屏2019-12-22上午10.09.43.png-38.6kB

setSubData佔91.54%

使用persistent map buffer(調用setBufferDataByPersistentMapBuffer函數):
截屏2019-12-22上午10.09.50.png-52.9kB

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

最終渲染結果

截屏2019-12-22下午12.01.20.png-54.8kB

參考資料

WebGPU規範
webgpu-samplers Github Repo
WebGPU-5

相關文章
相關標籤/搜索