美文网首页
Animometer

Animometer

作者: 不决书 | 来源:发表于2024-01-18 23: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

    相关文章

      网友评论

          本文标题:Animometer

          本文链接:https://www.haomeiwen.com/subject/jiohodtx.html