3. WebGPU Uniforms

上一篇文章讲的是阶段间变量。这篇文章将是关于uniforms的基础知识。

Uniforms are kind of like global variables for your shader. You can set their values before you execute the shader and they’ll have those values for every iteration of the shader. You can them set them to something else the next time you ask the GPU to execute the shader.

uniforms有点像着色器的全局变量。您可以在执行着色器之前设置它们的值,它们可以在着色器的每次迭代中使用这些值。下次您要求 GPU 执行着色器时,您可以将它们设置为其他值。

我们将从第一篇文章中的三角形示例重新开始并修改它以使用一些uniforms

  const module = device.createShaderModule({
    
    
    label: 'triangle shaders with uniforms',
    code: `
      struct OurStruct {
        color: vec4f,
        scale: vec2f,
        offset: vec2f,
      };
 
      @group(0) @binding(0) var<uniform> ourStruct: OurStruct;
 
      @vertex fn vs(
        @builtin(vertex_index) vertexIndex : u32
      ) -> @builtin(position) vec4f {
        var pos = array<vec2f, 3>(
          vec2f( 0.0,  0.5),  // top center
          vec2f(-0.5, -0.5),  // bottom left
          vec2f( 0.5, -0.5)   // bottom right
        );
 
        // return vec4f(pos[vertexIndex], 0.0, 1.0);
        return vec4f(
          pos[vertexIndex] * ourStruct.scale + ourStruct.offset, 0.0, 1.0);
      }
 
      @fragment fn fs() -> @location(0) vec4f {
        //return vec4f(1, 0, 0, 1);
        return ourStruct.color;
      }
    `,
  });
 
  });

首先我们声明了一个有 3 个成员的结构体

      struct OurStruct {
    
    
        color: vec4f,
        scale: vec2f,
        offset: vec2f,
      };

然后我们声明了一个具有该结构类型的uniform变量。变量名称是 ourStruct ,类型是刚刚定义的结构体类型OurStruct 。

      @group(0) @binding(0) var<uniform> ourStruct: OurStruct;

接下来更改从 顶点着色器 返回的内容以使用uniforms

      @vertex fn vs(
         ...
      ) ... {
    
    
        ...
        return vec4f(
          pos[vertexIndex] * ourStruct.scale + ourStruct.offset, 0.0, 1.0);
      }

您可以看到我们将顶点位置乘以scale,然后与offset 相加。这将让我们设置三角形的大小并定位它。

我们还更改片段着色器从 uniforms返回颜色

      @fragment fn fs() -> @location(0) vec4f {
    
    
        return ourStruct.color;
      }

现在我们已经将着色器设置为使用uniforms,需要在 GPU 上创建一个缓冲区来保存它们的值。

这是一个新的领域,如果你从未处理过原生数据和大小,那么有很多东西需要学习。这是一个很大的话题,所以这里有一篇关于这个话题的单独文章。如果您不知道如何在内存中布局结构,请阅读文章。然后回到这里。本文假设您已经阅读过它。

阅读文章后,我们现在可以继续使用与着色器中的结构相匹配的数据填充缓冲区。

首先,我们创建一个缓冲区并为其分配使用标志,以便它可以与uniforms一起使用,并且我们可以通过将数据复制到它来进行更新。

  const uniformBufferSize =
    4 * 4 + // color is 4 32bit floats (4bytes each)
    2 * 4 + // scale is 2 32bit floats (4bytes each)
    2 * 4;  // offset is 2 32bit floats (4bytes each)
  const uniformBuffer = device.createBuffer({
    
    
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });

然后我们创建一个 TypedArray ,这样我们就可以在 JavaScript 中设置值

  // create a typedarray to hold the values for the uniforms in JavaScript
  const uniformValues = new Float32Array(uniformBufferSize / 4);

and we’ll fill out 2 of the values of our struct that won’t be changing later. The offsets were computed using what we covered in the article on memory-layout.
我们将填写我们的结构的 2 个值,这些值以后不会改变。偏移量是使用我们在内存布局一文中介绍的内容计算的。

  // offsets to the various uniform values in float32 indices
  const kColorOffset = 0;
  const kScaleOffset = 4;
  const kOffsetOffset = 6;
 
  uniformValues.set([0, 1, 0, 1], kColorOffset);        // set the color
  uniformValues.set([-0.5, -0.25], kOffsetOffset);      // set the offset

Above we’re setting the color to green. The offset will move the triangle to the left 1/4th of the canvas and down 1/8th. (remember, clip space goes from -1 to 1 which is 2 units wide so 0.25 is 1/8 of 2).

上面我们将颜色设置为绿色。偏移量会将三角形移动到画布左侧 1/4 处和下方 1/8 处。 (请记住,剪辑空间从 -1 到 1,即 2 个单位宽,因此 0.25 是 2 的 1/8)。

Next, as the diagram showed in the first article, to tell a shader about our buffer we need to create a bind group and bind the buffer to the same @binding(?) we set in our shader.

接下来,如第一篇文章中的图表所示,为了告诉着色器我们的缓冲区,我们需要创建一个绑定组并将缓冲区绑定到我们在着色器中设置的相同 @binding(?) 。

  const bindGroup = device.createBindGroup({
    
    
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      {
    
     binding: 0, resource: {
    
     buffer: uniformBuffer }},
    ],
  });

现在,在我们提交命令缓冲区之前的某个时间,我们需要设置 uniformValues 的其他值,然后将这些值复制到 GPU 上的缓冲区。我们将在 render 函数的顶部执行此操作。

  function render() {
    
    
    // Set the uniform values in our JavaScript side Float32Array
    const aspect = canvas.width / canvas.height;
    uniformValues.set([0.5 / aspect, 0.5], kScaleOffset); // set the scale
 
    // copy the values from JavaScript to the GPU
    device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

我们将scale 设置为一半大小并考虑到画布的纵横比,因此无论画布的大小如何,三角形都将保持相同的宽高比。

最后,我们需要在绘制前设置bind group

    pass.setPipeline(pipeline);
    pass.setBindGroup(0, bindGroup); //<===here
    pass.draw(3);  // call our vertex shader 3 times
    pass.end();

这样我们就得到了一个绿色三角形,和前面所述的一样

在这里插入图片描述
对于这个三角形,我们在执行绘制命令时的状态是这样的
在这里插入图片描述
到目前为止,我们在着色器中使用的所有数据都是硬编码的(顶点着色器中的三角形顶点位置,以及片段着色器中的颜色)。现在我们可以将值传递到我们的着色器中,我们可以使用不同的数据多次调用 draw 。

我们可以通过更新我们的单个缓冲区在不同的地方绘制不同的偏移量、比例和颜色。要谨记,虽然我们的命令被放入命令缓冲区,但在我们submit它们之前它们并没有真正执行。所以不能这样做

    // BAD!
    for (let x = -1; x < 1; x += 0.1) {
    
    
      uniformValues.set([x, x], kOffsetOffset);
      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
      pass.draw(3);
    }
    pass.end();
 
    // Finish encoding and submit the commands
    const commandBuffer = encoder.finish();
    device.queue.submit([commandBuffer]);

因为,正如您在上面看到的, device.queue.xxx 函数发生在“队列”上,而 pass.xxx 函数只是在命令缓冲区中对命令进行编码。

当我们实际使用我们的命令缓冲区调用 submit 时,缓冲区中唯一的东西就是我们写入的最后一个值。

我们可以改成这样

    // BAD! Slow!
    for (let x = -1; x < 1; x += 0.1) {
    
    
      uniformValues.set([x, 0], kOffsetOffset);
      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
 
      const encoder = device.createCommandEncoder();
      const pass = encoder.beginRenderPass(renderPassDescriptor);
      pass.setPipeline(pipeline);
      pass.setBindGroup(0, bindGroup);
      pass.draw(3);
      pass.end();
 
      // Finish encoding and submit the commands
      const commandBuffer = encoder.finish();
      device.queue.submit([commandBuffer]);
    }

上面的代码更新一个缓冲区,创建一个命令缓冲区,添加绘制一件事的命令,然后完成命令缓冲区并提交。这行得通,但由于多种原因速度很慢。最大的是在单个命令缓冲区中完成更多工作是最佳实践。

因此,我们可以为每个要绘制的对象创建一个统一缓冲区。而且,由于缓冲区是通过绑定组间接使用的,因此我们还需要为每个我们想要绘制的对象使用一个绑定组。然后我们可以将所有我们想要绘制的东西放入一个命令缓冲区中。

我们开始做吧

首先让我们做一个随机函数

// A random number between [min and max)
// With 1 argument it will be [0 to min)
// With no arguments it will be [0 to 1)
const rand = (min, max) => {
    
    
  if (min === undefined) {
    
    
    min = 0;
    max = 1;
  } else if (max === undefined) {
    
    
    max = min;
    min = 0;
  }
  return min + Math.random() * (max - min);
};

现在让我们用一堆颜色和偏移设置缓冲区,我们可以绘制一堆单独的东西。

  // offsets to the various uniform values in float32 indices
  const kColorOffset = 0;
  const kScaleOffset = 4;
  const kOffsetOffset = 6;
 
  const kNumObjects = 100;
  const objectInfos = [];
 
  for (let i = 0; i < kNumObjects; ++i) {
    
    
    const uniformBuffer = device.createBuffer({
    
    
      label: `uniforms for obj: ${
      
      i}`,
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
 
    // create a typedarray to hold the values for the uniforms in JavaScript
    const uniformValues = new Float32Array(uniformBufferSize / 4);
 // uniformValues.set([0, 1, 0, 1], kColorOffset);        // set the color
 // uniformValues.set([-0.5, -0.25], kOffsetOffset);      // set the offset
    uniformValues.set([rand(), rand(), rand(), 1], kColorOffset);        // set the color
    uniformValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)], kOffsetOffset);      // set the offset
 
    const bindGroup = device.createBindGroup({
    
    
      label: `bind group for obj: ${
      
      i}`,
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
    
     binding: 0, resource: {
    
     buffer: uniformBuffer }},
      ],
    });
 
    objectInfos.push({
    
    
      scale: rand(0.2, 0.5),
      uniformBuffer,
      uniformValues,
      bindGroup,
    });
  }

我们还没有在我们的缓冲区中设置,因为我们希望它考虑画布的外观,并且在渲染时间之前我们不会知道画布的外观。

在渲染时,我们将使用正确的纵横比调整比例更新所有缓冲区。

  function render() {
    
    
    // Set the uniform values in our JavaScript side Float32Array
    //const aspect = canvas.width / canvas.height;
    //uniformValues.set([0.5 / aspect, 0.5], kScaleOffset); // set the scale
 
    // copy the values from JavaScript to the GPU
    //device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
 
    // Get the current texture from the canvas context and
    // set it as the texture to render to.
    renderPassDescriptor.colorAttachments[0].view =
        context.getCurrentTexture().createView();
 
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass(renderPassDescriptor);
    pass.setPipeline(pipeline);
 
    // Set the uniform values in our JavaScript side Float32Array
    const aspect = canvas.width / canvas.height;
 
    for (const {
    
    scale, bindGroup, uniformBuffer, uniformValues} of objectInfos) {
    
    
      uniformValues.set([scale / aspect, scale], kScaleOffset); // set the scale
      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
       pass.setBindGroup(0, bindGroup);
       pass.draw(3);  // call our vertex shader 3 times
    }
    pass.end();
 
    const commandBuffer = encoder.finish();
    device.queue.submit([commandBuffer]);
  }

同样,请记住 encoder 和 pass 对象只是将命令编码到命令缓冲区中。所以当 render 函数存在时,我们已经有效地按此顺序发出了这些命令。

device.queue.writeBuffer(...) // update uniform buffer 0 with data for object 0
device.queue.writeBuffer(...) // update uniform buffer 1 with data for object 1
device.queue.writeBuffer(...) // update uniform buffer 2 with data for object 2
device.queue.writeBuffer(...) // update uniform buffer 3 with data for object 3
...
// execute commands that draw 100 things, each with their own uniform buffer.
device.queue.submit([commandBuffer]);

结果如下

在这里插入图片描述
当我们在这里时,还有一件事要讲。您可以在着色器中自由引用多个统一缓冲区。在我们上面的示例中,每次绘制时都会更新比例scale,然后我们 writeBuffer 将该对象的 uniformValues 上传到相应的统一缓冲区。但是,只有比例更新,颜色和偏移量没有更新,所以我们在上传颜色和偏移量浪费了时间。

我们可以将uniforms 分成需要设置一次的uniforms 和每次绘制时更新的uniforms 。

  const module = device.createShaderModule({
    
    
    code: `
      struct OurStruct {
        color: vec4f,
       // scale: vec2f,
        offset: vec2f,
      };
 
      struct OtherStruct {
        scale: vec2f,
      };
 
      @group(0) @binding(0) var<uniform> ourStruct: OurStruct;
      @group(0) @binding(1) var<uniform> otherStruct: OtherStruct;
 
      @vertex fn vs(
        @builtin(vertex_index) vertexIndex : u32
      ) -> @builtin(position) vec4f {
        var pos = array<vec2f, 3>(
          vec2f( 0.0,  0.5),  // top center
          vec2f(-0.5, -0.5),  // bottom left
          vec2f( 0.5, -0.5)   // bottom right
        );
 
        return vec4f(
         // pos[vertexIndex] * ourStruct.scale + ourStruct.offset, 0.0, 1.0);
          pos[vertexIndex] * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
      }
 
      @fragment fn fs() -> @location(0) vec4f {
        return ourStruct.color;
      }
    `,
  });

当我们想要绘制的每个东西都需要 2 个uniform 缓冲区

  // create a buffer for the uniform values
  //const uniformBufferSize =
  //  4 * 4 + // color is 4 32bit floats (4bytes each)
  //  2 * 4 + // scale is 2 32bit floats (4bytes each)
  //  2 * 4;  // offset is 2 32bit floats (4bytes each)
  // offsets to the various uniform values in float32 indices
  //const kColorOffset = 0;
  //const kScaleOffset = 4;
  //const kOffsetOffset = 6;
  // create 2 buffers for the uniform values
  const staticUniformBufferSize =
    4 * 4 + // color is 4 32bit floats (4bytes each)
    2 * 4 + // offset is 2 32bit floats (4bytes each)
    2 * 4;  // padding
  const uniformBufferSize =
    2 * 4;  // scale is 2 32bit floats (4bytes each)
 
  // offsets to the various uniform values in float32 indices
  const kColorOffset = 0;
  const kOffsetOffset = 4;
 
  const kScaleOffset = 0;
 
  const kNumObjects = 100;
  const objectInfos = [];
 
  for (let i = 0; i < kNumObjects; ++i) {
    
    
    const staticUniformBuffer = device.createBuffer({
    
    
      label: `static uniforms for obj: ${
      
      i}`,
      size: staticUniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
 
    // These are only set once so set them now
    {
    
    
      //const uniformValues = new Float32Array(uniformBufferSize / 4);
      const uniformValues = new Float32Array(staticUniformBufferSize / 4);
      uniformValues.set([rand(), rand(), rand(), 1], kColorOffset);        // set the color
      uniformValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)], kOffsetOffset);      // set the offset
 
      // copy these values to the GPU
      //device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
      device.queue.writeBuffer(staticUniformBuffer, 0, uniformValues);
    }
 
    // create a typedarray to hold the values for the uniforms in JavaScript
    const uniformValues = new Float32Array(uniformBufferSize / 4);
    const uniformBuffer = device.createBuffer({
    
    
      label: `changing uniforms for obj: ${
      
      i}`,
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
 
    const bindGroup = device.createBindGroup({
    
    
      label: `bind group for obj: ${
      
      i}`,
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
    
     binding: 0, resource: {
    
     buffer: staticUniformBuffer }},
        {
    
     binding: 1, resource: {
    
     buffer: uniformBuffer }}, //<<===here
      ],
    });
 
    objectInfos.push({
    
    
      scale: rand(0.2, 0.5),
      uniformBuffer,
      uniformValues,
      bindGroup,
    });
  }

Nothing changes in our render code. The bind group for each object contains a reference to both uniform buffers for each object. Just as before we are updating the scale. But now we’re only uploading the scale when we call device.queue.writeBuffer to update the uniform buffer that holds the scale value whereas before we were uploading the color + offset + scale for each object.
我们的渲染代码没有任何变化。每个对象的绑定组包含对每个对象的两个uniform 缓冲区的引用。就像我们更新比例之前一样。但是现在我们只在调用 device.queue.writeBuffer 更新保存比例值的统一缓冲区时上传比例scale ,而之前的代码需要为每个对象上传 颜色 + 偏移量 + 比例 。

在这里插入图片描述
虽然在这个简单的示例中,拆分为多个uniform 缓冲区可能有点过度设计,但通常根据更改的内容和时间进行拆分。示例可能包括一个用于共享矩阵的统一缓冲区。例如项目矩阵、视图矩阵、相机矩阵。由于这些对于我们想要绘制的所有东西来说通常都是相同的,所以我们可以只创建一个缓冲区并让所有对象使用相同的统一缓冲区。

另外,我们的着色器可能会引用另一个统一缓冲区,其中仅包含特定于该对象的内容,例如其世界/模型矩阵和法线矩阵。

另一个统一缓冲区可能包含材料设置。这些设置可能由多个对象共享。

当我们介绍绘制 3D 时,我们会做很多这样的事情。

接下来,存储缓冲区

猜你喜欢

转载自blog.csdn.net/xuejianxinokok/article/details/130829123
3.