Animometer

2024-01-18  本文已影响0人  不决书

代码分析说明:

import { assert, makeSample, SampleInit } from '../../components/SampleLayout';

import animometerWGSL from './animometer.wgsl';

const init: SampleInit = async ({ canvas, pageState, gui }) => {
  const adapter = await navigator.gpu.requestAdapter();
  assert(adapter, 'requestAdapter returned null');
  const device = await adapter.requestDevice();

  if (!pageState.active) return;

  const perfDisplayContainer = document.createElement('div');
  perfDisplayContainer.style.color = 'white';
  perfDisplayContainer.style.background = 'black';
  perfDisplayContainer.style.position = 'absolute';
  perfDisplayContainer.style.top = '10px';
  perfDisplayContainer.style.left = '10px';

  const perfDisplay = document.createElement('pre');
  perfDisplayContainer.appendChild(perfDisplay);
  if (canvas.parentNode) {
    canvas.parentNode.appendChild(perfDisplayContainer);
  } else {
    console.error('canvas.parentNode is null');
  }

// 新建一组URL参数
  const params = new URLSearchParams(window.location.search);
  const settings = {
    numTriangles: Number(params.get('numTriangles')) || 20000,
    renderBundles: Boolean(params.get('renderBundles')),
    dynamicOffsets: Boolean(params.get('dynamicOffsets')),
  };

  const context = canvas.getContext('webgpu') as GPUCanvasContext;

  const devicePixelRatio = window.devicePixelRatio;
  canvas.width = canvas.clientWidth * devicePixelRatio;
  canvas.height = canvas.clientHeight * devicePixelRatio;
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

  context.configure({
    device,
    format: presentationFormat,
    alphaMode: 'premultiplied',
    usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
  });

  const timeBindGroupLayout = device.createBindGroupLayout({
    entries: [
      {
        binding: 0,
        visibility: GPUShaderStage.VERTEX,
        buffer: {
          type: 'uniform',
          minBindingSize: 4,
        },
      },
    ],
  });

  const bindGroupLayout = device.createBindGroupLayout({
    entries: [
      {
        binding: 0,
        visibility: GPUShaderStage.VERTEX,
        buffer: {
          type: 'uniform',
          minBindingSize: 20,
        },
      },
    ],
  });

  const dynamicBindGroupLayout = device.createBindGroupLayout({
    entries: [
      {
        binding: 0,
        visibility: GPUShaderStage.VERTEX,
        buffer: {
          type: 'uniform',
          hasDynamicOffset: true,
          minBindingSize: 20,
        },
      },
    ],
  });

  const vec4Size = 4 * Float32Array.BYTES_PER_ELEMENT;
  const pipelineLayout = device.createPipelineLayout({
    bindGroupLayouts: [timeBindGroupLayout, bindGroupLayout],
  });
  const dynamicPipelineLayout = device.createPipelineLayout({
    bindGroupLayouts: [timeBindGroupLayout, dynamicBindGroupLayout],
  });

  const shaderModule = device.createShaderModule({
    code: animometerWGSL,
  });
  const pipelineDesc: GPURenderPipelineDescriptor = {
    layout: 'auto',
    vertex: {
      module: shaderModule,
      entryPoint: 'vert_main',
      buffers: [
        {
          // vertex buffer
          arrayStride: 2 * vec4Size,
          stepMode: 'vertex',
          attributes: [
            {
              // vertex positions
              shaderLocation: 0,
              offset: 0,
              format: 'float32x4',
            },
            {
              // vertex colors
              shaderLocation: 1,
              offset: vec4Size,
              format: 'float32x4',
            },
          ],
        },
      ],
    },
    fragment: {
      module: shaderModule,
      entryPoint: 'frag_main',
      targets: [
        {
          format: presentationFormat,
        },
      ],
    },
    primitive: {
      topology: 'triangle-list',
      frontFace: 'ccw',
      cullMode: 'none',
    },
  };

  const pipeline = device.createRenderPipeline({
    ...pipelineDesc,
    layout: pipelineLayout,
  });

  const dynamicPipeline = device.createRenderPipeline({
    ...pipelineDesc,
    layout: dynamicPipelineLayout,
  });

  const vertexBuffer = device.createBuffer({
    size: 2 * 3 * vec4Size,
    usage: GPUBufferUsage.VERTEX,
    mappedAtCreation: true,
  });

  // prettier-ignore
  new Float32Array(vertexBuffer.getMappedRange()).set([
    // position data  /**/ color data
    0, 0.1, 0, 1,     /**/ 1, 0, 0, 1,
    -0.1, -0.1, 0, 1, /**/ 0, 1, 0, 1,
    0.1, -0.1, 0, 1,  /**/ 0, 0, 1, 1,
  ]);
  vertexBuffer.unmap();

  function configure() {
    const numTriangles = settings.numTriangles;
    const uniformBytes = 5 * Float32Array.BYTES_PER_ELEMENT;
    const alignedUniformBytes = Math.ceil(uniformBytes / 256) * 256;
    const alignedUniformFloats =
      alignedUniformBytes / Float32Array.BYTES_PER_ELEMENT;
    const uniformBuffer = device.createBuffer({
      size: numTriangles * alignedUniformBytes + Float32Array.BYTES_PER_ELEMENT,
      usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
    });
    const uniformBufferData = new Float32Array(
      numTriangles * alignedUniformFloats
    );
    const bindGroups = new Array(numTriangles);
    for (let i = 0; i < numTriangles; ++i) {
      uniformBufferData[alignedUniformFloats * i + 0] =
        Math.random() * 0.2 + 0.2; // scale
      uniformBufferData[alignedUniformFloats * i + 1] =
        0.9 * 2 * (Math.random() - 0.5); // offsetX
      uniformBufferData[alignedUniformFloats * i + 2] =
        0.9 * 2 * (Math.random() - 0.5); // offsetY
      uniformBufferData[alignedUniformFloats * i + 3] =
        Math.random() * 1.5 + 0.5; // scalar
      uniformBufferData[alignedUniformFloats * i + 4] = Math.random() * 10; // scalarOffset

      bindGroups[i] = device.createBindGroup({
        layout: bindGroupLayout,
        entries: [
          {
            binding: 0,
            resource: {
              buffer: uniformBuffer,
              offset: i * alignedUniformBytes,
              size: 6 * Float32Array.BYTES_PER_ELEMENT,
            },
          },
        ],
      });
    }

    const dynamicBindGroup = device.createBindGroup({
      layout: dynamicBindGroupLayout,
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uniformBuffer,
            offset: 0,
            size: 6 * Float32Array.BYTES_PER_ELEMENT,
          },
        },
      ],
    });

    const timeOffset = numTriangles * alignedUniformBytes;
    const timeBindGroup = device.createBindGroup({
      layout: timeBindGroupLayout,
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uniformBuffer,
            offset: timeOffset,
            size: Float32Array.BYTES_PER_ELEMENT,
          },
        },
      ],
    });

    // writeBuffer too large may OOM. TODO: The browser should internally chunk uploads.
    const maxMappingLength =
      (14 * 1024 * 1024) / Float32Array.BYTES_PER_ELEMENT;
    for (
      let offset = 0;
      offset < uniformBufferData.length;
      offset += maxMappingLength
    ) {
      const uploadCount = Math.min(
        uniformBufferData.length - offset,
        maxMappingLength
      );

      device.queue.writeBuffer(
        uniformBuffer,
        offset * Float32Array.BYTES_PER_ELEMENT,
        uniformBufferData.buffer,
        uniformBufferData.byteOffset + offset * Float32Array.BYTES_PER_ELEMENT,
        uploadCount * Float32Array.BYTES_PER_ELEMENT
      );
    }

    function recordRenderPass(
      passEncoder: GPURenderBundleEncoder | GPURenderPassEncoder
    ) {
      if (settings.dynamicOffsets) {
        passEncoder.setPipeline(dynamicPipeline);
      } else {
        passEncoder.setPipeline(pipeline);
      }
      passEncoder.setVertexBuffer(0, vertexBuffer);
      passEncoder.setBindGroup(0, timeBindGroup);
      const dynamicOffsets = [0];
      for (let i = 0; i < numTriangles; ++i) {
        if (settings.dynamicOffsets) {
          dynamicOffsets[0] = i * alignedUniformBytes;
          // 使用动态渲染管线,每一次设置都设置不同的dynamicOffsets
          passEncoder.setBindGroup(1, dynamicBindGroup, dynamicOffsets);
        } else {
         // 不适用动态的管线
          passEncoder.setBindGroup(1, bindGroups[i]);
        }
        passEncoder.draw(3);
      }
    }

    let startTime: number | undefined = undefined;
    const uniformTime = new Float32Array([0]);

    const renderPassDescriptor = {
      colorAttachments: [
        {
          view: undefined as GPUTextureView, // Assigned later
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
          loadOp: 'clear' as const,
          storeOp: 'store' as const,
        },
      ],
    };
  
    // 创建一个
    const renderBundleEncoder = device.createRenderBundleEncoder({
      colorFormats: [presentationFormat],
    });
    recordRenderPass(renderBundleEncoder);
    const renderBundle = renderBundleEncoder.finish();

    return function doDraw(timestamp: number) {
      if (startTime === undefined) {
        startTime = timestamp;
      }
      uniformTime[0] = (timestamp - startTime) / 1000;
      device.queue.writeBuffer(uniformBuffer, timeOffset, uniformTime.buffer);

      renderPassDescriptor.colorAttachments[0].view = context
        .getCurrentTexture()
        .createView();

      const commandEncoder = device.createCommandEncoder();
      const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);

      if (settings.renderBundles) {
        passEncoder.executeBundles([renderBundle]);
      } else {
        recordRenderPass(passEncoder);
      }

      passEncoder.end();
      device.queue.submit([commandEncoder.finish()]);
    };
  }

  let doDraw = configure();

  const updateSettings = () => {
    doDraw = configure();
  };
  if (gui === undefined) {
    console.error('GUI not initialized');
  } else {
    gui
      .add(settings, 'numTriangles', 0, 200000)
      .step(1)
      .onFinishChange(updateSettings);
    gui.add(settings, 'renderBundles');
    gui.add(settings, 'dynamicOffsets');
  }

  let previousFrameTimestamp: number | undefined = undefined;
  let jsTimeAvg: number | undefined = undefined;
  let frameTimeAvg: number | undefined = undefined;
  let updateDisplay = true;

  function frame(timestamp: number) {
    // Sample is no longer the active page.
    if (!pageState.active) return;

    let frameTime = 0;
    if (previousFrameTimestamp !== undefined) {
      frameTime = timestamp - previousFrameTimestamp;
    }
    previousFrameTimestamp = timestamp;

    const start = performance.now();
    doDraw(timestamp);
    const jsTime = performance.now() - start;
    if (frameTimeAvg === undefined) {
      frameTimeAvg = frameTime;
    }
    if (jsTimeAvg === undefined) {
      jsTimeAvg = jsTime;
    }

    const w = 0.2;
    frameTimeAvg = (1 - w) * frameTimeAvg + w * frameTime;
    jsTimeAvg = (1 - w) * jsTimeAvg + w * jsTime;

    if (updateDisplay) {
      perfDisplay.innerHTML = `Avg Javascript: ${jsTimeAvg.toFixed(
        2
      )} ms\nAvg Frame: ${frameTimeAvg.toFixed(2)} ms`;
      updateDisplay = false;
      setTimeout(() => {
        updateDisplay = true;
      }, 100);
    }
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
};
struct Time {
  value : f32,
}

struct Uniforms {
  scale : f32,
  offsetX : f32,
  offsetY : f32,
  scalar : f32,
  scalarOffset : f32,
}

@binding(0) @group(0) var<uniform> time : Time;
@binding(0) @group(1) var<uniform> uniforms : Uniforms;

struct VertexOutput {
  @builtin(position) Position : vec4<f32>,
  @location(0) v_color : vec4<f32>,
}

@vertex
fn vert_main(
  @location(0) position : vec4<f32>,
  @location(1) color : vec4<f32>
) -> VertexOutput {
  var fade = (uniforms.scalarOffset + time.value * uniforms.scalar / 10.0) % 1.0;
  if (fade < 0.5) {
    fade = fade * 2.0;
  } else {
    fade = (1.0 - fade) * 2.0;
  }
  var xpos = position.x * uniforms.scale;
  var ypos = position.y * uniforms.scale;
  var angle = 3.14159 * 2.0 * fade;
  var xrot = xpos * cos(angle) - ypos * sin(angle);
  var yrot = xpos * sin(angle) + ypos * cos(angle);
  xpos = xrot + uniforms.offsetX;
  ypos = yrot + uniforms.offsetY;

  var output : VertexOutput;
  output.v_color = vec4(fade, 1.0 - fade, 0.0, 1.0) + color;
  output.Position = vec4(xpos, ypos, 0.0, 1.0);
  return output;
}

@fragment
fn frag_main(@location(0) v_color : vec4<f32>) -> @location(0) vec4<f32> {
  return v_color;
}

总结步骤

  1. 创建了三个BindGroupLayout,分别是timeBindGroupLayoutbindGroupLayoutdynamicBindGroupLayout 区别是参数下面的minBindingSize分别是4 、 20 、20
  2. 创建管线布局PipelineLayout, 第一个管线布局pipelineLayout参数包含了 [timeBindGroupLayout, bindGroupLayout],第二个管线布局dynamicPipelineLayout参数包含了[timeBindGroupLayout, dynamicBindGroupLayout]
  3. 创建渲染管线RenderPipeline,第一个管线使用第一个布局pipelineLayout,第二个管线使用第二个布局dynamicPipelineLayout,其他的渲染参数都一致
  4. 使用bindGroupLayout创建多个绑定组,存放在bindGroups数组中多少个三角形就有多少个绑定组
  5. 使用dynamicBindGroupLayout 创建绑定组,参数设置 offset 为 0 , size为 6 * Float32Array.BYTES_PER_ELEMENT
  6. 使用timeBindGroupLayout 创建绑定组,参数设置offset为每个顶点的数据大小,size为Float32Array.BYTES_PER_ELEMENT
  7. 使用接口writeBuffer批量写入数据,为了防止内存不足一次吸入56M数据
  8. settings.dynamicOffsets 使用这个参数动态切换管线
  9. 动态管线是接口渲染256个字节偏移,设置bindgroup,如passEncoder.setBindGroup(1, dynamicBindGroup, dynamicOffsets);
  10. 非动态管线设置bindGroup是使用passEncoder.setBindGroup(1, bindGroups[i]);
  11. 创建bundleEncoder编码器,返回捆绑渲染
  12. 在每帧的渲染中,场景普通编码器,如果选择renderBundles, 就执行passEncoder.executeBundles([renderBundle]);,将编码器的内容采用renderBundles的数据,否则按照正常的顺序
  13. device.queue.submit([commandEncoder.finish()]); 提交编码器
  14. 本案例,渲染200000个三角形可以看出
正常渲染 单独采用动态Buffer 单独使用bundle render 同时使用 动态Buffer & bundle render
94ms 55ms 45ms 45ms
上一篇 下一篇

猜你喜欢

热点阅读