Ⅰ、基于 WebGPU 从 0 到 1 渲染 GLTF:第一个三角形

news2024/9/22 13:43:52

Ⅰ、基于 WebGPU 从 0 到 1 渲染 GLTF:第一个三角形

WebGPU 是一种面相网页的现代图形 API,由主要浏览器供应商开发。与 WebGL 相比,WebGPU 对 GPU 提供了更直接的控制,使应用程序能更有效地利用硬件,类似于 Vulkan 和 DirectX 12。WebGPU 还提供了更多 WebGL 所不具备的 GPU 功能,如计算着色器(compute shaders)和存储缓冲区(storage buffers),使强大的 GPU 计算应用程序能够在网页上运行。从 OpenGL 到 Vulkan,WebGPU 向用户暴露了比 WebGL 更多的复杂性,不过该 API 在复杂性和可用性之间取得了很好的平衡,总体来说非常好用。在本系列中,我们将从头开始学习 WebGPU 的关键方面,目标是实现从零到基本的 glTF 模型渲染器。本篇文章标志着我们在这条道路上迈出了第一步,我们将设置一个 WebGPU 上下文,并在屏幕上显示一个三角形。

1、项目搭建:Vite + TypeScript + HTML

1、控制台执行以下两个命令

①全局安装 vite:npm install -g create-vite

在这里插入图片描述

②指定文件夹下创建工程:create-vite 1-first-triangle-ts --template vanilla-ts

这里的 1-first-triangle-ts 为 工程名/文件夹名

在这里插入图片描述

2、打开工程,安装所需依赖包

① 安装基础依赖

通过 vscode 打开 1-first-triangle-ts 文件夹

打开后在控制台,执行 npm i 和 npm run dev

在这里插入图片描述

npm i 安装依赖; npm run dev 是安装好基础依赖后,运行代码,查看网页是否正常启动、显示。

首次运行显示的界面:

在这里插入图片描述

② 安装 webgpu 所需依赖:npm install @webgpu/types

在这里插入图片描述

③删除、修改多余文件

生成的工程中,src 文件夹下会有多余的文件:counter.ts、style.css、typescript.svg、vite-env.d.ts,这里将它们删除,只保留 main.ts。

在这里插入图片描述

main.ts 中的代码修改,这里用 console.log(“正常启动了”); 来查看代码是否修改生效。

在这里插入图片描述

再次在控制台 npm run dev 运行项目,可以看到启动的网页没有了内容,ctrl + shift + i 打开调试控制台,可以看到控制台打印了我们在 main.ts 中写的代码,运行成功。

在这里插入图片描述

后续我们就在 main.ts 中编写 webgpu 的代码。

至此基础的前端工程就已经搭建完毕,可以进行 webgpu 的相关开发了。

这里 main.ts 的代码之所以生效,是因为 index.html 中引入了 main.ts,index.html 就是一个网页的入口。

在这里插入图片描述

3、基础配置

① 在 tsconfig.json 中的 compilerOptions 中添加两个配置:

启用实验性的装饰器特性:“experimentalDecorators”: true

指定需要包含在编译中的类型声明文件: “types”: [“vite/client”, “@webgpu/types”],

在这里插入图片描述

这样在 ts 文件编写 navigator.gpu 时就不会提示以下错误了。

在这里插入图片描述

4、基础概念

1、 GPUAdapter、GPUDevice、浏览器 之间的关系

在这里插入图片描述

他们的逻辑关系为:

假设浏览器支持 WebGPU,那么浏览器中的 JS 主线程 或 Web Worker 可以通过 GPUAdapter(显卡适配器) 来获取当前系统中的 GPUDevice(显卡设备)。

GPUAdapter(显卡适配器)的作用:

WebGPU 不光要考虑不同的操作系统(Windows、Linux、Mac OS、Android等),还要考虑系统上不同的显卡设备硬件(集成显卡、独立显卡、多个显卡等),因此 WebGPU 需要创建一个 GPUAdapter(显卡适配器) 来统一负责获取系统中的显卡设备(GPUDevice)。

GPUDevice(显卡设备)的作用:

而这里的 GPUDevice(显卡设备) 并不是真的电脑硬件上的显卡。

  1. 电脑硬件上的显卡是由不同厂商生产的。

  2. 即使同一显卡在不同的操作系统上又是由不同的底层图形 API 驱动的,WebGPU 所支持的 3 个底层图形架构分别是:DirectX 12(即 D3D12)、Metal、Vulkan。

    DirectX 12 属于 微软

    Metal 属于 苹果

    Vulkan 属于 Khronos Group

结论:为了适配不同的显卡设备以及系统底层图形架构,因此 WebGPU 需要创建 GPUDevice(显卡设备) 来统一负责 “系统中的显卡设备”。

小总结:

  1. GPUAdapter 显卡适配器:用于抹平和获取不同类型的 GPUDevice。
  2. GPUDevice 显卡设备:用于抹平不同底层图形框架下的显卡设备,提供发送执行 GPU 渲染或计算命令的能力。

参考:puxiao/webgpu-tutorial: WebGPU 系列教程,学习和探索 WebGPU 世界。 (github.com)

2、获取 WebGPU 上下文

使用 WebGPU 的前提是设浏览器要支持WebGPU。

我们将在本篇文章中实现的三角形渲染器。

WebGPU 渲染上下文的初始设置与 WebGL 类似。我们的网页将有一个画布(canvas)来显示我们渲染的图像,并从 main.ts 加载我们的渲染代码。

因此,我们将把渲染代码放在一个异步函数中,该函数将在加载脚本时执行。我们的第一步是从 WebGPU API 获取一个 GPUAdapter。每个适配器代表一台机器上的 GPU 以及浏览器在该 GPU 上的 WebGPU 实现。然后,我们可以向适配器请求一个 GPUDevice,它为我们提供了与硬件协同工作的上下文。GPUDevice 提供了创建 GPU 对象(如缓冲区和纹理)和在设备上执行命令的 API。GPUAdapter 和 GPUDevice 之间的区别类似于 Vulkan 中的 VkPhysicalDevice 和 VkDevice。与 WebGL 一样,我们需要一个用于显示渲染图像的画布(canvas)的上下文。要在画布上使用 WebGPU,我们需要一个 webgpu 上下文。设置完成后,我们就可以加载着色器和顶点数据、配置渲染目标并构建渲染管道,从而绘制三角形!

index.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>WebGPU-GLTF</title>
  <style>
    html,
    body {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      margin: 0;
      width: 100%;
      height: 100%;
      background: #000;
      color: #fff;
      display: flex;
    }

    #app {
      width: 100%;
      height: 100%;
      display: flex;
    }
  </style>
</head>

<body>
  <div id="app">
  </div>
  <script type="module" src="/src/main.ts"></script>
</body>

</html>

main.ts:

// 1、创建画布 用于 GPU 渲染 在此画布上绘制
const createCanvas = (containerID = "app") => {
  const canvas = document.createElement('canvas');
  const contianer = document.getElementById(containerID) as HTMLDivElement;

  canvas.style.width = '100%';
  canvas.style.height = '100%';

  if (!contianer) {
    document.body.appendChild(canvas);
  } else {
    contianer.appendChild(canvas);
  }

  const devicePixelRatio = window.devicePixelRatio || 1;
  const w = Math.floor(canvas.clientWidth * devicePixelRatio);
  const h = Math.floor(canvas.clientHeight * devicePixelRatio);
  canvas.width = w;
  canvas.height = h;

  return canvas;
}

// 2、初始化 WebGPU、获取上下文
const setupGPU = async () => { // 异步函数 异步方法
  if (!navigator.gpu) { // 判断是否支持 WebGPU
    throw new Error("不支持 WebGPU");
  }
  // 获取 GPU device
  const adapter = await navigator.gpu.requestAdapter({
    powerPreference: 'high-performance'
  }); // 显卡适配器
  const device = await adapter?.requestDevice(); // 显卡设备

  const canvas = createCanvas(); // 创建画布
  const context = canvas.getContext("webgpu"); // 获取 WebGPU 上下文

  return {
    adapter,
    device,
    canvas,
    context
  }
}

const render = () => {
  setupGPU(); // 调用 initGPU
}

render(); // 执行 render 

编写代码逻辑如下:

1、判断是否支持 WebGPU

2、获取显卡适配器(GPUAdapter)、显卡设备(GPUDevice)

3、创建画布(canvas),并将画布添加至页面中(appendChild(canvas))

4、获取画布(canvas)的 WebGPU 上下文(context)

此时控制台执行 npm run dev,并 ctrl + shift + i 打开网页浏览器的控制台,在 id=“app” 的 div 里面,就有一个创建成功的 canvas,由于 canvas(画布)中尚未绘制任何东西,所以网页页面是空白的。

在这里插入图片描述

3、WebGPU 渲染管线

WebGPU 渲染管道包括两个可编程阶段:顶点着色器和片段着色器,与 WebGL 类似。WebGPU 还增加了对计算着色器的支持,计算着色器存在于渲染管道之外。

在这里插入图片描述

WebGPU 的渲染流水线由两个可编程着色器阶段组成:顶点着色器负责将输入顶点转换到裁剪空间,片段着色器负责为每个三角形覆盖的像素着色。

要渲染三角形,我们需要配置这样一个管道,指定着色器、顶点属性配置等。在 WebGPU 中,该流水线采用具体对象 GPURenderPipeline 的形式,它指定了流水线的不同部分。该流水线组件(如着色器、顶点状态、渲染输出状态等)的配置是固定的,这样 GPU 就能更好地优化流水线的渲染。绑定到相应输入或输出的缓冲区或纹理可以更改,但输入和输出的数量及其类型等不能更改。这与 WebGL 形成了鲜明对比,WebGL 通过修改全局状态机隐式地指定了绘制的流水线状态,而且着色器、顶点状态等可以在绘制调用之间随时交换,这给优化流水线带来了挑战。

4、着色器模块

创建流水线的第一步是创建顶点和片段着色器模块,这些模块将在流水线中执行。WebGPU 着色器是用 WGSL 编写的。本应用的着色器相对简单。我们为顶点数据的输入和输出格式定义了一个结构,并将位置和颜色从输入传递到输出。

在 src 文件夹下新建triangle.wgsl.ts,并编码如下:

triangle.wgsl.ts:

const triangleShader = `// This type definition is just to make typing a bit easier
alias float4 = vec4<f32>;

struct VertexInput {
    @location(0) position: float4,
    @location(1) color: float4,
};

struct VertexOutput {
    // This is the equivalent of gl_Position in GLSL
    @builtin(position) position: float4,
    @location(0) color: float4,
};

@vertex
fn vertex_main(vert: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    out.color = vert.color;
    out.position = vert.position;
    return out;
};

@fragment
fn fragment_main(in: VertexOutput) -> @location(0) float4 {
    return float4(in.color);
}
`

export default triangleShader;

我们将包含 WGSL 代码的字符串传递给 createShaderModule 方法来编译它们。我们可以检查生成的着色器模块的编译信息,查看是否有任何错误导致着色器编译失败。

代码逻辑如下:

1、定义 wgsl 着色器

2、maint.ts 中引入

3、创建画布 canvas、初始化 GPU

4、载入 shader 着色器

main.ts:

import shaderCode from "./triangle.wgsl.ts";

// 3、加载 初始化  shader 着色器
const setupShaderModules = async (device: GPUDevice) => {
  // Setup shader modules
  const shaderModule = device.createShaderModule({ code: shaderCode }); // 传入 shaderCode
  const compilationInfo = await shaderModule.getCompilationInfo(); // 编译信息
  if (compilationInfo.messages.length > 0) {
    let hadError = false;
    console.log("Shader compilation log:");
    for (let i = 0; i < compilationInfo.messages.length; ++i) {
      const msg = compilationInfo.messages[i];
      console.log(`${msg.lineNum}:${msg.linePos} - ${msg.message}`);
      hadError = hadError || msg.type == "error";
    }
    if (hadError) {
      console.log("Shader failed to compile"); // 编译失败
      return null;
    }
  }
  return shaderModule;
}

5、指定顶点数据

接下来,我们将指定三角形的顶点数据。我们将在单个缓冲区中指定顶点位置和颜色,位置和颜色相互交错。每个位置和颜色都将存储为 float4。首先,我们使用 createBuffer 在设备上分配和映射一个有足够空间存储顶点数据的缓冲区。该方法获取我们要创建的缓冲区的大小(以字节为单位),以及一组指定缓冲区所需的使用模式的标志。通过设置 mappedAtCreation 参数,我们可以指定在创建缓冲区时对其进行映射(mapped)。

createBuffer 返回 GPUBuffer 和 ArrayBuffer,我们可以使用 ArrayBuffer 将数据上传到缓冲区。要写入顶点数据,我们要为数组缓冲区创建一个 Float32Array 视图,并通过该视图设置数据。最后,我们必须先取消缓冲区的映射(unmap),然后再在渲染中使用。

为了告诉 GPU 如何将顶点数据传递给我们的着色器,我们将指定一个 GPUVertexState 作为创建时渲染管道的一部分。该对象既指定了我们要运行的着色器,也指定了该着色器的属性输入应如何填充顶点缓冲区的数据。我们通过传递 GPUVertexBufferLayout 对象数组来指定顶点缓冲区的解释方式。每个条目描述了渲染时从绑定到相应槽的顶点缓冲区中读取的顶点属性。

在本示例中,我们有一个单一的缓冲区,其中包含每个顶点的交错属性。因此,元素之间的间隔为 32 字节(2 个 float4),缓冲区指定了两个 float4 属性。第一个属性是位置,发送到着色器输入位置 0;第二个属性是颜色,发送到着色器输入位置 1。

WebGPU 指定顶点缓冲区和属性的模型与 D3D12 和 Vulkan 相同,其中顶点缓冲区绑定到输入插槽,并提供一组顶点属性,如下图所示。从 D3D12 的角度来看,顶点缓冲区(vertexBuffers)成员映射到创建图形流水线时通过 D3D12_INPUT_LAYOUT_DESC 传递的 D3D12_INPUT_ELEMENT_DESC 结构数组。在 Vulkan 视图中,vertexBuffers 成员直接映射到创建图形流水线(graphics pipeline)时传递的 VkPipelineVertexInputStateCreateInfo 结构。

在这里插入图片描述

通过缓冲区向输入装配器提供三角形的顶点属性。我们从绑定到输入插槽 0 的缓冲区中读取属性,并使用为输入插槽和属性指定的跨距和偏移量传递到指定的着色器输入位置。

///4、指定顶点数据
const specifyVertexData = (device: GPUDevice, shaderModule: GPUShaderModule) => {
  // Specify vertex data
  // Allocate room for the vertex data: 3 vertices, each with 2 float4's
  const dataBuf = device.createBuffer({
    size: 3 * 2 * 4 * 4,
    usage: GPUBufferUsage.VERTEX,
    mappedAtCreation: true
  });

  // Interleaved positions and colors
  new Float32Array(dataBuf.getMappedRange()).set([
    1, -1, 0, 1,  // position
    1, 0, 0, 1,   // color
    -1, -1, 0, 1, // position
    0, 1, 0, 1,   // color
    0, 1, 0, 1,   // position
    0, 0, 1, 1,   // color
  ]);
  dataBuf.unmap();

  // Vertex attribute state and shader stage
  const vertexState: GPUVertexState = {
    // Shader stage info
    module: shaderModule,
    entryPoint: "vertex_main",
    // Vertex buffer info
    buffers: [{
      arrayStride: 2 * 4 * 4,
      attributes: [
        { format: "float32x4", offset: 0, shaderLocation: 0 },
        { format: "float32x4", offset: 4 * 4, shaderLocation: 1 }
      ]
    }]
  };

  return { dataBuf, vertexState };
}

6、编写渲染输出(Writing Rendering Outputs)

接下来,我们将创建一个交换链,并指定片段着色器输出结果的写入位置。要在画布上显示图像,我们需要一个与其上下文相关联的交换链。交换链可让我们旋转画布上显示的图像,在显示另一个缓冲区时渲染到不可见的缓冲区(即双重缓冲)。我们通过指定所需的图像格式和纹理用途来创建交换链。交换链将为我们创建一个或多个纹理,大小与要显示的画布相匹配。由于我们将直接对交换链纹理进行渲染,因此我们指定将它们用作输出附件。交换链在 WebGPU 中不再是一个显式对象,而是由上下文进行内部管理。要设置交换链,我们需要调用 configure 并传递交换链参数。

虽然在本例中我们只绘制了一个三角形,但我们仍将创建并使用深度纹理,因为我们稍后会用到它。深度纹理的创建与普通纹理一样,需要指定大小、格式和用途。和之前一样,我们将直接对该纹理进行渲染,因此指定它将用作输出附件。

与片段着色器阶段的 GPUVertexState 类似的是 GPUFragmentState。该对象指定了要运行的片段着色器,以及它将以何种格式写入输出纹理。指定渲染目标是为了与着色器中的输出位置相匹配,这与之前指定顶点输入和缓冲槽的方式类似。我们将直接渲染到交换链,因此我们只有一个渲染目标,其格式就是交换链格式。

// 5、渲染输出
const setupRenderOutputs = (canvas: HTMLCanvasElement, context: GPUCanvasContext, device: GPUDevice, shaderModule: GPUShaderModule) => {
  // Setup render outputs
  const swapChainFormat = "bgra8unorm";
  context.configure({
    device: device,
    format: swapChainFormat,
    usage: GPUTextureUsage.RENDER_ATTACHMENT
  });
  const depthFormat: GPUTextureFormat = "depth24plus-stencil8";
  const depthTexture = device.createTexture({
    size: {
      width: canvas.width,
      height: canvas.height,
      depthOrArrayLayers: 1.0
    },
    format: depthFormat,
    usage: GPUTextureUsage.RENDER_ATTACHMENT
  });

  const fragmentState: GPUFragmentState = {
    // Shader info
    module: shaderModule,
    entryPoint: "fragment_main",
    // Output render target info
    targets: [{ format: swapChainFormat }]
  };

  return {
    depthTexture,
    fragmentState,
    depthFormat
  }
}

7、创建渲染管线

最后,我们可以创建渲染管道,将着色器、顶点属性和输出配置结合起来(shaders、vertex attributes、output configuration),用于渲染三角形。渲染管道描述通过 GPURenderPipelineDescriptor 对象传递给 createRenderPipeline。我们的渲染管道需要的最后部分是管道布局(pipeline layout,指定管道使用的绑定组布局)和深度/模版状态(depth/stencil)。图元拓扑结构(primitive topology)也可以在此结构中指定;不过,默认值是三角形列表,这已经是我们所需要的了。

// 6、创建渲染管线
const createRenderPipeline = (device: GPUDevice, vertexState: GPUVertexState, fragmentState: GPUFragmentState, depthFormat: GPUTextureFormat) => {
  // Create render pipeline
  const layout = device.createPipelineLayout({ bindGroupLayouts: [] });
  const renderPipeline = device.createRenderPipeline({
    layout: layout,
    vertex: vertexState,
    fragment: fragmentState,
    depthStencil: { format: depthFormat, depthWriteEnabled: true, depthCompare: "less" }
  });
  return renderPipeline;
}

8、渲染

WebGPU 中的渲染是在渲染通道(Render Pass)中进行的,渲染通道通过 GPURenderPassDescriptor 进行描述。渲染通道描述符指定了要绑定到片段着色器中要写入的目标的图像,以及可选的深度缓冲区和遮挡查询集。(occlusion query,遮挡查询在现代图形API上面很常见,基于遮挡查询可以做一些遮挡剔除,减少drawCall,优化性能。)指定的颜色和深度附件必须与渲染通道中使用的渲染管道所指定的颜色和深度状态相匹配。我们的片段着色器会写入一个输出槽,即对象颜色,并将其写入当前交换链图像。由于每帧图像都会更改为当前的交换链图像,因此我们暂时不对其进行设置。

剩下要做的就是编写我们的渲染循环,并将其传递给 requestAnimationFrame,以便每帧都调用它来更新图像。为了记录和提交 GPU 命令,我们使用了 GPUCommandEncoder。命令编码器可用于预先录制可多次提交给 GPU 的命令缓冲区,或重新录制并提交每一帧。由于我们将在每一帧更改渲染通道颜色附件,因此我们将在每一帧重新录制并提交命令缓冲区。

对于每一帧,我们都要获取最新的交换链图像,将渲染输出写入其中,并将其设置为输出色彩附件图像。然后,我们创建一个命令编码器来记录我们的渲染命令。我们通过调用 beginRenderPass 开始渲染过程,并传递渲染过程描述符来获取 GPURenderPassEncoder,这样我们就可以记录渲染命令了。然后,我们可以设置要使用的渲染流水线,将顶点缓冲区绑定到相应的输入插槽,绘制三角形,并结束渲染过程。要获得可提交给 GPU 执行的命令缓冲区,我们需要在命令编码器上调用 finish。然后,返回的命令缓冲区将传递给设备执行。命令缓冲区运行后,我们的三角形将被写入交换链图像并显示在画布上,如下图所示!

// 7、渲染
const setupRender = (context: GPUCanvasContext, device: GPUDevice, depthTexture: GPUTexture, renderPipeline: GPURenderPipeline, dataBuf: GPUBuffer) => {

  const renderPassDesc: GPURenderPassDescriptor = {
    colorAttachments: [{
      // view will be set to the current render target each frame
      view: context.getCurrentTexture().createView(),
      loadOp: "clear",
      clearValue: [0.3, 0.3, 0.3, 1],
      storeOp: "store"
    }],
    depthStencilAttachment: {
      view: depthTexture.createView(),
      depthLoadOp: "clear",
      depthClearValue: 1.0,
      depthStoreOp: "store",
      stencilLoadOp: "clear",
      stencilClearValue: 0,
      stencilStoreOp: "store"
    }
  };
  // Render!
  const frame = function () {
    const colorAttachments = renderPassDesc.colorAttachments as Iterable<GPURenderPassColorAttachment>;
    Array.from(colorAttachments)[0].view = context.getCurrentTexture().createView();

    const commandEncoder = device.createCommandEncoder();

    const renderPass = commandEncoder.beginRenderPass(renderPassDesc);

    renderPass.setPipeline(renderPipeline);
    renderPass.setVertexBuffer(0, dataBuf);
    renderPass.draw(3, 1, 0, 0);

    renderPass.end();
    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
}

9、总结

有了屏幕上的第一个三角形,我们就可以制作一个基本的 glTF 模型查看器了。在下一篇文章中,我们将介绍如何使用绑定组(bind groups)向着色器传递额外数据(例如均匀缓冲区)。

main.ts 完整代码:

import shaderCode from "./triangle.wgsl.ts";

// 1、创建画布 用于 GPU 渲染 在此画布上绘制
const createCanvas = (containerID = "app") => {
  const canvas = document.createElement('canvas');
  const contianer = document.getElementById(containerID) as HTMLDivElement;

  canvas.style.width = '100%';
  canvas.style.height = '100%';

  if (!contianer) {
    document.body.appendChild(canvas);
  } else {
    contianer.appendChild(canvas);
  }

  const devicePixelRatio = window.devicePixelRatio || 1;
  const w = Math.floor(canvas.clientWidth * devicePixelRatio);
  const h = Math.floor(canvas.clientHeight * devicePixelRatio);
  canvas.width = w;
  canvas.height = h;

  return canvas;
}

// 2、初始化 WebGPU、获取上下文
const setupGPU = async () => { // 异步函数 异步方法
  if (!navigator.gpu) { // 判断是否支持 WebGPU
    throw new Error("不支持 WebGPU");
  }
  // 获取 GPU device
  const adapter = await navigator.gpu.requestAdapter({
    powerPreference: 'high-performance'
  }); // 显卡适配器
  const device = await adapter?.requestDevice(); // 显卡设备

  const canvas = createCanvas(); // 创建画布
  const context = canvas.getContext("webgpu"); // 获取 WebGPU 上下文

  return {
    adapter,
    device,
    canvas,
    context
  }
}

// 3、加载 初始化  shader 着色器
const setupShaderModules = async (device: GPUDevice) => {
  // Setup shader modules
  const shaderModule = device.createShaderModule({ code: shaderCode }); // 传入 shaderCode
  const compilationInfo = await shaderModule.getCompilationInfo(); // 编译信息
  if (compilationInfo.messages.length > 0) {
    let hadError = false;
    console.log("Shader compilation log:");
    for (let i = 0; i < compilationInfo.messages.length; ++i) {
      const msg = compilationInfo.messages[i];
      console.log(`${msg.lineNum}:${msg.linePos} - ${msg.message}`);
      hadError = hadError || msg.type == "error";
    }
    if (hadError) {
      console.log("Shader failed to compile"); // 编译失败
      return null;
    }
  }
  return shaderModule;
}
///4、指定顶点数据
const specifyVertexData = (device: GPUDevice, shaderModule: GPUShaderModule) => {
  // Specify vertex data
  // Allocate room for the vertex data: 3 vertices, each with 2 float4's
  const dataBuf = device.createBuffer({
    size: 3 * 2 * 4 * 4,
    usage: GPUBufferUsage.VERTEX,
    mappedAtCreation: true
  });

  // Interleaved positions and colors
  new Float32Array(dataBuf.getMappedRange()).set([
    1, -1, 0, 1,  // position
    1, 0, 0, 1,   // color
    -1, -1, 0, 1, // position
    0, 1, 0, 1,   // color
    0, 1, 0, 1,   // position
    0, 0, 1, 1,   // color
  ]);
  dataBuf.unmap();

  // Vertex attribute state and shader stage
  const vertexState: GPUVertexState = {
    // Shader stage info
    module: shaderModule,
    entryPoint: "vertex_main",
    // Vertex buffer info
    buffers: [{
      arrayStride: 2 * 4 * 4,
      attributes: [
        { format: "float32x4", offset: 0, shaderLocation: 0 },
        { format: "float32x4", offset: 4 * 4, shaderLocation: 1 }
      ]
    }]
  };

  return { dataBuf, vertexState };
}
// 5、渲染输出
const setupRenderOutputs = (canvas: HTMLCanvasElement, context: GPUCanvasContext, device: GPUDevice, shaderModule: GPUShaderModule) => {
  // Setup render outputs
  const swapChainFormat = "bgra8unorm";
  context.configure({
    device: device,
    format: swapChainFormat,
    usage: GPUTextureUsage.RENDER_ATTACHMENT
  });
  const depthFormat: GPUTextureFormat = "depth24plus-stencil8";
  const depthTexture = device.createTexture({
    size: {
      width: canvas.width,
      height: canvas.height,
      depthOrArrayLayers: 1.0
    },
    format: depthFormat,
    usage: GPUTextureUsage.RENDER_ATTACHMENT
  });

  const fragmentState: GPUFragmentState = {
    // Shader info
    module: shaderModule,
    entryPoint: "fragment_main",
    // Output render target info
    targets: [{ format: swapChainFormat }]
  };

  return {
    depthTexture,
    fragmentState,
    depthFormat
  }
}

// 6、创建渲染管线
const createRenderPipeline = (device: GPUDevice, vertexState: GPUVertexState, fragmentState: GPUFragmentState, depthFormat: GPUTextureFormat) => {
  // Create render pipeline
  const layout = device.createPipelineLayout({ bindGroupLayouts: [] });
  const renderPipeline = device.createRenderPipeline({
    layout: layout,
    vertex: vertexState,
    fragment: fragmentState,
    depthStencil: { format: depthFormat, depthWriteEnabled: true, depthCompare: "less" }
  });
  return renderPipeline;
}
// 7、渲染
const setupRender = (context: GPUCanvasContext, device: GPUDevice, depthTexture: GPUTexture, renderPipeline: GPURenderPipeline, dataBuf: GPUBuffer) => {

  const renderPassDesc: GPURenderPassDescriptor = {
    colorAttachments: [{
      // view will be set to the current render target each frame
      view: context.getCurrentTexture().createView(),
      loadOp: "clear",
      clearValue: [0.3, 0.3, 0.3, 1],
      storeOp: "store"
    }],
    depthStencilAttachment: {
      view: depthTexture.createView(),
      depthLoadOp: "clear",
      depthClearValue: 1.0,
      depthStoreOp: "store",
      stencilLoadOp: "clear",
      stencilClearValue: 0,
      stencilStoreOp: "store"
    }
  };
  // Render!
  const frame = function () {
    const colorAttachments = renderPassDesc.colorAttachments as Iterable<GPURenderPassColorAttachment>;
    Array.from(colorAttachments)[0].view = context.getCurrentTexture().createView();

    const commandEncoder = device.createCommandEncoder();

    const renderPass = commandEncoder.beginRenderPass(renderPassDesc);

    renderPass.setPipeline(renderPipeline);
    renderPass.setVertexBuffer(0, dataBuf);
    renderPass.draw(3, 1, 0, 0);

    renderPass.end();
    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
}
// 8、调用执行
const render = async () => {
  const { adapter, device, canvas, context } = await setupGPU(); // 调用 initGPU
  if (!adapter || !device || !canvas || !context) return; // 初始化异常 如果以上值不对 直接返回
  const shaderModule = await setupShaderModules(device); // 初始化 shader
  if (!shaderModule) return; // shader 异常
  const { dataBuf, vertexState } = specifyVertexData(device, shaderModule);
  const {
    depthTexture,
    fragmentState,
    depthFormat
  } = setupRenderOutputs(canvas, context, device, shaderModule);
  const renderPipeline = createRenderPipeline(device, vertexState, fragmentState, depthFormat);
  setupRender(context, device, depthTexture, renderPipeline, dataBuf);

}


render(); // 执行 render 

渲染结果:

在这里插入图片描述

参考资料:

From 0 to glTF with WebGPU 1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2054353.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习设计模式之外观模式

文章目录 前言一、介绍二、特点三、详细分析1.核心组成2.代码示例3.优缺点优点缺点 4.使用场景 总结 前言 外观模式是结构型设计模式&#xff0c;定义一个高层接口&#xff0c;用来访问子系统中的众多接口&#xff0c;使系统更加容易使用。 一、介绍 外观设计模式&#xff08…

低代码与AI:赋能企业数字化转型

引言 随着全球经济的快速发展和科技的飞速进步&#xff0c;数字化转型已成为各个行业和企业发展的重要趋势。数字化转型的背景不仅是提升效率和竞争力的手段&#xff0c;更是适应市场变化、满足客户需求的必由之路。 在当今信息化时代&#xff0c;技术的变革推动了企业运营方式…

【Python机器学习】MapReduce(分布式计算的框架)

MapReduce的优缺点&#xff1a; 优点&#xff1a;可在短时间内完成大量工作&#xff1b; 缺点&#xff1a;算法必须经过重写&#xff0c;需要对系统工程有一定的理解&#xff1b; 适用数据类型&#xff1a;数值型和标称型数据。 MapReduce是一个软件框架&#xff0c;可以将单个…

SQL UA注入 (injection 第十八关)

简介 SQL注入&#xff08;SQL Injection&#xff09;是一种常见的网络攻击方式&#xff0c;通过向SQL查询中插入恶意的SQL代码&#xff0c;攻击者可以操控数据库&#xff0c;SQL注入是一种代码注入攻击&#xff0c;其中攻击者将恶意的SQL代码插入到应用程序的输入字段中&a…

[Python学习日记-10] Python中的流程控制(if...else...)

简介 假如把写程序比做走路&#xff0c;那我们到现在为止&#xff0c;一直走的都是直路&#xff0c;还没遇到过分叉口&#xff0c;想象现实中&#xff0c;你遇到了分叉口&#xff0c;然后你决定往哪拐必然是有所动作的。你要判断那条岔路是你真正要走的路&#xff0c;如果我们想…

合宙LuatOS AIR700 IPV6 TCP 客户端向NodeRed发送数据

为了验证 AIR700 IPV6 &#xff0c;特别新建向NodeRed Tcp发送的工程。 Air700发送TCP数据源码如下&#xff1a; --[[ IPv6客户端演示, 仅EC618系列支持, 例如Air780E/Air600E/Air780UG/Air700E ]]-- LuaTools需要PROJECT和VERSION这两个信息 PROJECT "IPV6_SendDate_N…

Leetcode面试经典150题-155.最小栈

解法都在代码里&#xff0c;不懂就留言或者私信 我写了两种解法&#xff0c;建议选择双栈的&#xff0c;感觉这才是考察点 /**一般解法&#xff1a;过个笔试没问题&#xff0c;建议用双栈的方法 */ class MinStack2 {/**至少应该有一个栈用于保存数据 对于push和pop以及top的话…

STM32之SPI读写W25Q128芯片

SPI简介 STM32的SPI是一个串行外设接口。它允许STM32微控制器与其他设备&#xff08;如传感器、存储器等&#xff09;进行高速、全双工、同步的串行通信。通常包含SCLK&#xff08;串行时钟&#xff09;、MOSI&#xff08;主设备输出/从设备输入Master Output Slave Input&…

【React Hooks - useState状态批量更新原理】

概述 所谓批量处理就是当在同时更新多个状态下&#xff0c;能够统一批量处理更新&#xff0c;避免了重复渲染。在React17及之前版本&#xff0c;React只会在合成事件以及生命周期内部进行批量处理&#xff0c;在setTimeout、Promise、Fetch等异步请求中&#xff0c;则不会自动…

【GH】【EXCEL】P1: Write DATA SET from GH into EXCEL

文章目录 WriteFast WriteGH data material :GH process and components instructionFast Write DataFast Write Data & Clear DataFast Write to Cell EXCEL written results Write by ColumnGH data material :Compile ColumnGH process and components instructionWrite…

三、Kafka副本

2、创建2个分区两个副本 /usr/local/kafka/bin# ./kafka-topics.sh --bootstrap-server 192.168.58.130:9092 --create --topic atguigu2 --partitions 2 --replication-factor 23、查看topic详细信息 /usr/local/kafka/bin# ./kafka-topics.sh --bootstrap-server 192.168.5…

如何理解CAPL—Test编程中的测试对象

前言&#xff1a;CAPL—Test编程中的对象&#xff0c;是一个比较复杂的概念&#xff0c;对象的作用是作为Test特定函数的参数。来执行特定的功能&#xff08;这是比较复杂的一个概念&#xff0c;下文会慢慢讲解&#xff09;。 注意&#xff1a;因为翻译的问题&#xff0c;有些…

arm:ADC模数转换器

比较器 AD&#xff1a; 精度&#xff1a;10位 转换速率&#xff1a;500 KSPS 量程&#xff1a;0~3.3v void adc_init(void) {ADCCON (1 << 14) | (49 << 6) | (1 << 1); }unsigned short adc_read(void) {unsigned short value ADCDAT0;while(~(ADCCON &am…

华为M60首次降价,消费回暖能延续?

导语 8月15日&#xff0c;华为Mate 60系列首次官宣降价&#xff01;能否带动消费电子进一步回暖&#xff1f; 在当前全球经济形势复杂多变的背景下&#xff0c;各行各业都在寻求新的增长点和突破口。 消费电子市场作为科技与日常生活紧密结合的重要领域&#xff0c;其发展态势一…

基于HarmonyOS的宠物收养系统的设计与实现(一)

基于HarmonyOS的宠物收养系统的设计与实现&#xff08;一&#xff09; 本系统是简易的宠物收养系统&#xff0c;为了更加熟练地掌握HarmonyOS相关技术的使用。 项目创建 创建一个空项目取名为PetApp 首页实现&#xff08;组件导航使用&#xff09; 官方文档&#xff1a;组…

微服务中的Sidecar模式

微服务中的Sidecar模式 什么是sidecarsidecar如何工作Sidecar 代理服务注册发现Sidecar 代理异构服务发起服务调用异构服务如何被调用 常见应用以MOSN流量接管为例使用 sidecar 模式的优势sidecar和面向切片编程AOP的关系参考 什么是sidecar sidecar是服务网络架构的产物。 S…

【网络】 arp 命令 得到网段内所有物理设备ip

我的笔记本和 NVIDIA Jetson Orin 都位于同一个 192.168.1.x 的网段内&#xff0c;我想远程访问 Orin&#xff0c;但我不知道orin的ip 方法 1: 使用 arp 命令 打开命令提示符&#xff1a; 按下 Win R 键&#xff0c;打开“运行”对话框。输入 cmd 并按 Enter 键打开命令提示符…

JAVA类加载过程/类装载的执行过程/java类加载机制/JVM加载Class文件的原理机制?

JAVA类加载过程/类装载的执行过程/java类加载机制/JVM加载Class文件的原理机制&#xff1f; 类加载的过程主要分为三个部分&#xff1a;&#xff08;加链初&#xff0c;验准解&#xff09; 加载链接初始化 而链接又可以细分为三个小部分&#xff1a; 验证准备解析 骚戴理解…

散点图适用于什么数据 thinkcell散点图设置不同颜色

在数据可视化的众多工具和技巧中&#xff0c;散点图是一种极为有效的方式&#xff0c;能够揭示变量之间的关系&#xff0c;尤其是在探索数据集的相关性、分布趋势、集群现象时。而在众多助力于制作高质量散点图的工具中&#xff0c;think-cell插件以其高效的操作和丰富的功能&a…

重定向与追加

1、>和>> > 【重定向】 如果文件不存在&#xff0c;则创建&#xff0c;并将内容输入到文件&#xff1b; 如果文件存在&#xff0c;则先清空文件&#xff0c;然后将内容输入到文件&#xff1b;>> 【追加】 如果文件不存在&#xff0c;则创建&…