WebGPU-8

第五个例子,compute_boids
在这里插入图片描述
boids指的是群落,一群鸟,一群人什么的。特指一大群什么东西相互作用运动的case,这个例子是2D的,一堆三角形在哪动来动去,相互作用,在快要碰到的时候或减速,或掉头,或换方向,看起来就像一群没头苍蝇一样在扭动。

先来看看大致的流程,个人理解流程分了三步,
第一步通过compute shader,我们计算出1000份数据,每一份数据包含速度和角度之类的数据,用来决定这个三角形最终出现的位置。
第二步,vertex shader绘制一个三角形的三个点,
通过instance drawing,绘制1000个,此时我们有1000个三角形了。将上一步的1000份数据应用到1000个三角形上,决定最终出现的位置
第三步,通过fragment shader将1000个三角形的每一份的三个点连成三角形,绘制到屏幕上。
在这里插入图片描述
以上就是最最笼统的流程。和之前的例子不一样的是,我们终于看到了compute shader的使用,也是这个sample最重要的部分。

先看vertex shader:

const vertexShaderGLSL = `#version 450
                              layout(location = 0) in vec2 a_particlePos;
                              layout(location = 1) in vec2 a_particleVel;
                              layout(location = 2) in vec2 a_pos;
                              void main() {
                                  float angle = -atan(a_particleVel.x, a_particleVel.y);
                                  vec2 pos = vec2(a_pos.x * cos(angle) - a_pos.y * sin(angle),
                                                  a_pos.x * sin(angle) + a_pos.y * cos(angle));
                                  gl_Position = vec4(pos + a_particlePos, 0, 1);
                              }
                             `

我们看到shader里有速度和位置信息,根据角度进行旋转,根据速度决定移动范围,最终决定点的位置。

再来看看fragment shader

    const fragmentShaderGLSL = `#version 450
                                layout(location = 0) out vec4 fragColor;
                                void main() {
                                    fragColor = vec4(1.0);
                                }
                               `

啥都没干,就是简简单单的画了白色(fragColor被设置成了白色)

如果只看这两个shader,基本流程和最开始hello triangle的例子是一毛一样的不是么。vertex设置了位置,fragment将点连成了三角形,然后绘制出来。仅此而已。

问题是,这些位置数据是如何算出来的。
compute shader我们来仔细研究下看看:

const computeShaderGLSL = `#version 450
								// 定义粒子的结构,粒子即一个三角形,包含位置和速度两个变量
                                struct Particle {
                                    vec2 pos;
                                    vec2 vel;
                                };
								
								// 参数列表,决定最终某一个粒子该怎么走,具体包括的参数比较多,这里先不一一解释
                                layout(std140, set = 0, binding = 0) uniform SimParams {
                                    float deltaT;
                                    float rule1Distance;
                                    float rule2Distance;
                                    float rule3Distance;
                                    float rule1Scale;
                                    float rule2Scale;
                                    float rule3Scale;
                                } params;
								// buffer A,个人理解这边可以理解为src,及上一帧,每个粒子的状态
                                layout(std140, set = 0, binding = 1) buffer ParticlesA {
                                    Particle particles[${numParticles}];
                                } particlesA;
								// buffer B,理解成dst,即结果,根据A计算出来的更新过的状态
                                layout(std140, set = 0, binding = 2) buffer ParticlesB {
                                    Particle particles[${numParticles}];
                                } particlesB;

                                void main() {
                                    // https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp

                                    uint index = gl_GlobalInvocationID.x;
                                    if (index >= ${numParticles}) { return; }

                                    vec2 vPos = particlesA.particles[index].pos;
                                    vec2 vVel = particlesA.particles[index].vel;

                                    vec2 cMass = vec2(0.0, 0.0);
                                    vec2 cVel = vec2(0.0, 0.0);
                                    vec2 colVel = vec2(0.0, 0.0);
                                    int cMassCount = 0;
                                    int cVelCount = 0;

                                    vec2 pos;
                                    vec2 vel;
                                    for (int i = 0; i < ${numParticles}; ++i) {
                                        if (i == index) { continue; }
                                        pos = particlesA.particles[i].pos.xy;
                                        vel = particlesA.particles[i].vel.xy;

                                        if (distance(pos, vPos) < params.rule1Distance) {
                                            cMass += pos;
                                            cMassCount++;
                                        }
                                        if (distance(pos, vPos) < params.rule2Distance) {
                                            colVel -= (pos - vPos);
                                        }
                                        if (distance(pos, vPos) < params.rule3Distance) {
                                            cVel += vel;
                                            cVelCount++;
                                        }
                                    }
                                    if (cMassCount > 0) {
                                        cMass = cMass / cMassCount - vPos;
                                    }
                                    if (cVelCount > 0) {
                                        cVel = cVel / cVelCount;
                                    }

                                    vVel += cMass * params.rule1Scale + colVel * params.rule2Scale + cVel * params.rule3Scale;

                                    // clamp velocity for a more pleasing simulation.
                                    vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);

                                    // kinematic update
                                    vPos += vVel * params.deltaT;

                                    // Wrap around boundary
                                    if (vPos.x < -1.0) vPos.x = 1.0;
                                    if (vPos.x > 1.0) vPos.x = -1.0;
                                    if (vPos.y < -1.0) vPos.y = 1.0;
                                    if (vPos.y > 1.0) vPos.y = -1.0;

                                    particlesB.particles[index].pos = vPos;

                                    // Write back
                                    particlesB.particles[index].vel = vVel;
                                }
                              `

计算过程可以参考代码里的link,或者其他相关资料,我们只要抓住重点即可,我们有一份buffer 的 ParticalA的数据,经过相互之间的计算,限制,加成,最终得出了三角形下一帧的速度和位置数据。

这里最最要紧的,是两个地方:

			uint index = gl_GlobalInvocationID.x;
	        if (index >= ${numParticles}) { return; }
	        ...

compute shader是针对一个instance而言的,获取当前instance是通过gl_GlobalInvocationID来获取的。

第二个地方是:

		for (int i = 0; i < ${numParticles}; ++i) {
		   if (i == index) { continue; }
		    pos = particlesA.particles[i].pos.xy;
		    vel = particlesA.particles[i].vel.xy;
		    ...
		}

每一个instance都要遍历1000遍的其他数据。
我们想一下,如果这一步要用CPU来做,那计算量就是1000 * 1000。
而通过GPU的compute shader,通过并行计算,计算量是相同的,但是同时分1000个instance来做,效率也大大提升了。

接下来我们看看外部API调用的情况:

const swapChain = context.configureSwapChain({
        device,
        format: "bgra8unorm"
      });

swapChain恢复到了最开始的情况,而不像上一个例子那样需要作为src再次使用

const computeBindGroupLayout = device.createBindGroupLayout({
        bindings: [
          { binding: 0, visibility: GPUShaderStageBit.COMPUTE, type: "uniform-buffer" },
          { binding: 1, visibility: GPUShaderStageBit.COMPUTE, type: "storage-buffer" },
          { binding: 2, visibility: GPUShaderStageBit.COMPUTE, type: "storage-buffer" },
        ],
      });

新增了computeBindGroupLayout 变量,专门供compute pass使用。其中的三个数据binding也和compute shader中的
SimParams, ParticlesA, ParticlesB一一对应。这里要解释一下:

Storage Buffer Object不同于uniform buffer在着色器不可修改,storage buffer是可读可写的。修改的内容给其他着色器调用或者应用程序本身。
我们需要通过各种遍历和判断,将最终数据写入ParticlesB中,且在帧与帧之间,ParticlesA和ParticlesB的角色是相互切换的。
直白的说,上一帧通过SRC写入DST,下一帧就反过来了。DST变成了SRC,所以不论是ParticlesA还是ParticlesB,都定义成storage-buffer。

再来看看pipleline:

	  const computePipelineLayout = device.createPipelineLayout({
        bindGroupLayouts: [computeBindGroupLayout],
      });


      const computePipeline = device.createComputePipeline({
        layout: computePipelineLayout,
        computeStage: {
          module: device.createShaderModule({
            code: Utils.compile("c", computeShaderGLSL)
          }),
          entryPoint: "main",
        }
      });

就两句话,仅仅指明layout的设置和用哪个shader就行了。

interface GPUDevice {
    readonly attribute GPUExtensions extensions;
    readonly attribute GPULimits limits;
    readonly attribute GPUAdapter adapter;

    GPUBuffer createBuffer(GPUBufferDescriptor descriptor);
    (GPUBuffer, ArrayBuffer) createBufferMapped(GPUBufferDescriptor descriptor);
    Promise<(GPUBuffer, ArrayBuffer)> createBufferMappedAsync(GPUBufferDescriptor descriptor);
    GPUTexture createTexture(GPUTextureDescriptor descriptor);
    GPUSampler createSampler(GPUSamplerDescriptor descriptor);

    GPUBindGroupLayout createBindGroupLayout(GPUBindGroupLayoutDescriptor descriptor);
    GPUPipelineLayout createPipelineLayout(GPUPipelineLayoutDescriptor descriptor);
    GPUBindGroup createBindGroup(GPUBindGroupDescriptor descriptor);

    GPUShaderModule createShaderModule(GPUShaderModuleDescriptor descriptor);
    GPUComputePipeline createComputePipeline(GPUComputePipelineDescriptor descriptor);
    GPURenderPipeline createRenderPipeline(GPURenderPipelineDescriptor descriptor);

    GPUCommandEncoder createCommandEncoder(GPUCommandEncoderDescriptor descriptor);

    GPUQueue getQueue();
};

回过头来再看看Spec上的API定义,我们可以看到对于pipleline,只有两种 createComputePipeline和createRenderPipeline,其中createComputePipeline负责接收compute shader,renderPipeline负责接收vertex和fragment shader

接下来的renderPipeline这里就不讲了,流程和之前的都一样。没有新东西

      const simParamData = new Float32Array([0.04, 0.1, 0.025, 0.025, 0.02, 0.05, 0.005]);
      const simParamBuffer = device.createBuffer({
        size: simParamData.byteLength,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.TRANSFER_DST,
      });
      simParamBuffer.setSubData(0, simParamData);

首先对boid算法中需要用到的模拟参数值进行设置,由于这7个值都是固定的值,所以设置成了UNIFORM↑↑↑↑↑↑↑

      const initialParticleData = new Float32Array(numParticles * 4);
      for (let i = 0; i < numParticles; ++i) {
        initialParticleData[4 * i + 0] = 2 * (Math.random() - 0.5);
        initialParticleData[4 * i + 1] = 2 * (Math.random() - 0.5);
        initialParticleData[4 * i + 2] = 2 * (Math.random() - 0.5) * 0.1;
        initialParticleData[4 * i + 3] = 2 * (Math.random() - 0.5) * 0.1;
      }

接下来对buffer值设定了一个随机的数组↑↑↑↑↑↑↑

      const particleBuffers = new Array(2);
      for (let i = 0; i < 2; ++i) {
        particleBuffers[i] = device.createBuffer({
          size: initialParticleData.byteLength,
          usage: GPUBufferUsage.TRANSFER_DST | GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE
        });
        particleBuffers[i].setSubData(0, initialParticleData);
      }

接下来设置了两个buffer,这里有2个点需要注意。首先是buffer的生成,是2份,不是1份,只是这两份数据都用了CPU中的同一份initialParticleData数据初始化了,第二点是两个buffer设置的usage包含了STORAGEHE VERTEX两个,storage很好理解,因为这部分数据是可改写的,而vertex是因为这块buffer还要用来传给vertex shader作为绘制前决定三角形位置的数据。↑↑↑↑↑↑↑

      const particleBindGroups = new Array(2);
      for (let i = 0; i < 2; ++i) {
        particleBindGroups[i] = device.createBindGroup({
          layout: computeBindGroupLayout,
          bindings: [{
            binding: 0,
            resource: {
              buffer: simParamBuffer,
              offset: 0,
              size: simParamData.byteLength
            },
          }, {
            binding: 1,
            resource: {
              buffer: particleBuffers[i],
              offset: 0,
              size: initialParticleData.byteLength,
            },
          }, {
            binding: 2,
            resource: {
              buffer: particleBuffers[(i + 1) % 2],
              offset: 0,
              size: initialParticleData.byteLength,
            },
          }],
        });
      }

这一段比较有意思,设置了2个bindGroup,而且这两个group的设置是相互反一反的,bindGroup[0]中一个是src,一个是dst,而bindGroup[1]则反过来了。这是为了打一个ping-pong的操作。↑↑↑↑↑↑↑

      function frame() {
        renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView();

        const commandEncoder = device.createCommandEncoder({});
        {
          const passEncoder = commandEncoder.beginComputePass();
          passEncoder.setPipeline(computePipeline);
          passEncoder.setBindGroup(0, particleBindGroups[t % 2]);
          passEncoder.dispatch(numParticles);
          passEncoder.endPass();
        }
        {
          const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
          passEncoder.setPipeline(renderPipeline);
          passEncoder.setVertexBuffers(0, [particleBuffers[(t + 1) % 2], verticesBuffer], [0, 0]);
          passEncoder.draw(3, numParticles, 0, 0);
          passEncoder.endPass();
        }
        device.getQueue().submit([commandEncoder.finish()]);

        ++t;
        requestAnimationFrame(frame);
      }
      requestAnimationFrame(frame);
    }

最后看一下frame函数,这里有三个地方需要在意:
首先是compute的中

passEncoder.setBindGroup(0, particleBindGroups[t % 2]);

然后是render中

passEncoder.setVertexBuffers(0, [particleBuffers[(t + 1) % 2], verticesBuffer], [0, 0]);

这里能看到 compute用了A,render就用B;反过来compute用B,render就用A,这就是我说的ping-pong的一个流程。
一块buffer作为src,另一块做dst。

最后一个要注意的是

passEncoder.draw(3, numParticles, 0, 0);

numParticles赋值了1000,就是1个三角形,绘制1000个instance,这和最开始gl_GlobalInvocationID中对应上。

最后再补充一点,之前说renderPipeline没有什么新鲜东西,说错了。

		vertexInput: {
          indexFormat: "uint32",
          vertexBuffers: [{
            // instanced particles buffer
            stride: 4 * 4,
            stepMode: "instance",
            attributes: [{
              // instance position
              shaderLocation: 0,
              offset: 0,
              format: "float2"
            }, {
              // instance velocity
              shaderLocation: 1,
              offset: 2 * 4,
              format: "float2"
            }],
          }, {
            // vertex buffer
            stride: 2 * 4,
            stepMode: "vertex",
            attributes: [{
              // vertex positions
              shaderLocation: 2,
              offset: 0,
              format: "float2"
            }],
          }],
        }

看一下vertexInput的设置,其中有个mode被设置成了instance,然后设置了位置,步长,格式等。而shaderLocation是2的时候是普通的vertex。

结合刚才我说的 draw函数中的1000个instance来理解。
即:
绘制一个三角形,绘制1000个instance,在shader中,location是0和1的情况,是instance模式,是将buffer按照offset,type拆分之后的一一对应的送到shader里的。而location是2的时候,则是普通的vertex数据,是统一的,就那么一份数据。

cool~以上就是这个sample的解析。
下一个sample将是官方提供的sample中最后的一个了。

统一结尾:以上均为个人理解和一家之言,有任何错漏之处欢迎留言讨论,共同进步,一经发现错漏,必立刻更新,且会在修改处指明reporter。谢谢

<think>我们正在处理一个TypeError,错误信息是“Cannotreadpropertiesofnull(reading'limits')”,发生在webgpu-device.ts文件中。这通常表示在尝试访问一个null对象的'limits'属性时发生了错误。根据错误信息,我们可以推断:1.代码中有一个对象,我们期望它是一个有效的WebGPU设备对象(非null),但实际上它是null。2.当我们尝试访问这个null对象的'limits'属性时,就抛出了这个错误。可能的原因:-在请求WebGPU设备(`adapter.requestDevice()`)时失败,返回的设备为null,但代码没有检查设备是否为null就直接使用。-设备请求是异步的,可能在设备还未成功返回时就尝试使用它(即没有正确处理异步流程)。-浏览器或运行环境不支持WebGPU,导致无法获取设备。解决方案步骤:1.确保运行环境支持WebGPU:检查浏览器或运行环境是否支持WebGPU。可以通过检查`navigator.gpu`是否存在来判断。2.在请求设备后,检查设备是否为null:在调用`requestDevice()`后,应该检查返回的设备对象是否为null,如果是,则处理错误(例如,回退到其他渲染方式或提示用户)。3.正确处理异步:确保在设备请求完成后再使用设备(使用`async/await`或`.then()`)。示例代码:```javascriptasyncfunctioninitWebGPU(){//检查浏览器是否支持WebGPUif(!navigator.gpu){console.error('WebGPUisnotsupportedinthisbrowser.');return;}//请求适配器constadapter=awaitnavigator.gpu.requestAdapter();if(!adapter){console.error('FailedtogetGPUadapter.');return;}//请求设备constdevice=awaitadapter.requestDevice();if(!device){console.error('FailedtogetGPUdevice.');return;}//现在可以安全地使用device.limitsconsole.log(device.limits);}initWebGPU();```如果按照上述步骤检查,就可以避免这个错误。另外,注意`requestDevice()`方法可能会因为各种原因失败(例如权限问题、资源不足等),所以一定要检查返回的设备对象。如果问题仍然存在,请检查以下几点:-浏览器版本是否支持WebGPU(如Chrome113+,并确保在chrome://flags中启用了WebGPU)。-如果是扩展环境(如Electron),确保启用了WebGPU支持。相关问题:1.WebGPU的适配器(adapter)和设备(device)有什么区别?2.如何检测浏览器是否支持WebGPU?3.在WebGPU中,设备(device)的limits属性包含哪些信息?4.如何处理WebGPU中异步操作可能出现的错误?</think>### TypeError: Cannot read properties of null (reading 'limits') in WebGPUDevice.ts 分析 这个错误发生在尝试访问 `null` 对象的 `limits` 属性时,具体涉及 WebGPU 设备初始化流程。以下是根本原因和解决方案: #### 错误原因 1. **设备请求失败** `adapter.requestDevice()` 返回了 `null`,但代码未检查返回值直接访问其属性: ```javascript const device = await adapter.requestDevice(); // 可能返回 null console.log(device.limits); // 访问 null.limits 导致错误 ``` 2. **浏览器兼容性问题** - 浏览器未启用 WebGPU 支持(Chrome 需在 `chrome://flags` 启用 **"Unsafe WebGPU"**) - 浏览器版本过低(WebGPU 要求 Chrome 113+ 或 Edge 113+) - 硬件/驱动不支持(如旧 GPU 或移动设备) 3. **异步初始化问题** 在设备初始化完成前提前访问 `device` 对象: ```javascript initWebGPU(); // 异步操作未完成 render(); // 立即访问未初始化的 device ``` #### 解决方案 ##### 1. 添加空值检查 在访问 `device` 前验证其有效性: ```javascript const device = await adapter.requestDevice(); if (!device) { throw new Error("WebGPU device request failed"); } console.log(device.limits); // 安全访问 ``` ##### 2. 启用浏览器支持 - **Chrome/Edge**:访问 `chrome://flags` → 搜索 **"Unsafe WebGPU"** → 设置为 **Enabled** → 重启浏览器 - **Firefox**:在 `about:config` 中启用 `dom.webgpu.enabled` ##### 3. 验证环境支持 添加运行时检测: ```javascript if (!navigator.gpu) { alert("WebGPU not supported in this browser!"); return; } const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { alert("No compatible GPU adapter found!"); return; } ``` ##### 4. 调试设备请求失败 捕获设备请求错误: ```javascript try { const device = await adapter.requestDevice({ requiredLimits: { maxStorageBufferBindingSize: 1024 } // 示例需求 }); } catch (error) { console.error("Device request failed:", error); } ``` #### 关键检查点 1. 浏览器是否启用 WebGPU 实验性功能 2. 设备请求是否包含不支持的 `requiredLimits` 配置 3. 是否存在安全限制(如非 HTTPS 环境或跨域资源) 4. 硬件是否满足 WebGPU 最低要求 > **扩展参考**:WebGPU 设备初始化流程遵循 [W3C WebGPU 规范](https://www.w3.org/TR/webgpu/#gpudevice),设备创建失败时规范要求返回 `null` 而非抛出异常[^1]。 --- ### 相关问题 1. 如何在 Chrome 中启用 WebGPU 支持? 2. WebGPU 的 `requiredLimits` 参数如何配置? 3. 哪些浏览器版本完全支持 WebGPU? 4. 如何检测用户设备的 WebGPU 兼容性? 5. WebGPUWebGL 在错误处理机制上有何区别? [^1]: W3C WebGPU Device Interface Specification, Section 3.3 Device Creation.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值