Ⅰ、基于 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(显卡设备) 并不是真的电脑硬件上的显卡。
-
电脑硬件上的显卡是由不同厂商生产的。
-
即使同一显卡在不同的操作系统上又是由不同的底层图形 API 驱动的,WebGPU 所支持的 3 个底层图形架构分别是:DirectX 12(即 D3D12)、Metal、Vulkan。
DirectX 12 属于 微软
Metal 属于 苹果
Vulkan 属于 Khronos Group
结论:为了适配不同的显卡设备以及系统底层图形架构,因此 WebGPU 需要创建 GPUDevice(显卡设备) 来统一负责 “系统中的显卡设备”。
小总结:
- GPUAdapter 显卡适配器:用于抹平和获取不同类型的 GPUDevice。
- 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