本系列介绍 WebGPU 和一般计算机图形学。
首先让我们看看我们要构建什么,
生命游戏
3D 渲染
3D 渲染,但有灯光
渲染 3D 模型
除了JS基础知识外,不需要任何基础知识。
教程已经在我的 github 上完成,附有源代码。
WebGPU 是一个相对较新的 GPU API。尽管名为 WebGPU,但它实际上可以被视为 Vulkan、DirectX 12、Metal、OpenGL 和 WebGL 之上的一层。它被设计为低级 API,旨在用于高性能应用程序,例如游戏和模拟。
在本章中,我们将在屏幕上绘制一些东西。第一部分将参考 Google Codelabs 教程。我们将在屏幕上创建一个生活游戏。
我们将在启用 typescript 的 vite 中创建一个空的普通 JS 项目。然后清除所有多余的代码,只留下main.ts。
const main = async () => { console.log('Hello, world!') } main()
在实际编码之前,请检查您的浏览器是否启用了 WebGPU。您可以在 WebGPU Samples 上查看它。
Chrome 现在默认处于启用状态。在 Safari 上,您应该转到开发者设置、标记设置并启用 WebGPU。
我们还需要为 WebGPU 启用这些类型,安装 @webgpu/types,并在 tsc 编译器选项中添加 "types": ["@webgpu/types"]。
此外,我们替换了
WebGPU 有很多样板代码,如下所示。
首先我们需要访问 GPU。在WebGPU中,是通过适配器的概念来完成的,适配器是GPU和浏览器之间的桥梁。
const adapter = await navigator.gpu.requestAdapter();
然后我们需要向适配器请求一个设备。
const device = await adapter.requestDevice(); console.log(device);
我们在画布上绘制三角形。我们需要获取canvas元素并配置它。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
这里,我们使用 getContext 来获取画布的相关信息。通过指定 webgpu,我们将获得一个负责使用 WebGPU 进行渲染的上下文。
CanvasFormat其实就是颜色模式,例如srgb。我们通常只使用首选格式。
最后,我们使用设备和格式配置上下文。
在深入研究工程细节之前,我们首先必须了解 GPU 如何处理渲染。
GPU 渲染管道是 GPU 渲染图像所采取的一系列步骤。
在 GPU 上运行的应用程序称为着色器。着色器是运行在GPU上的程序。着色器有一种特殊的编程语言,我们稍后会讨论。
渲染管道有以下步骤,
根据图元(GPU 可以渲染的最小单位)的不同,管道可能有不同的步骤。通常,我们使用三角形,它通知 GPU 将每 3 组顶点视为一个三角形。
Render Pass 是完整 GPU 渲染的一个步骤。创建渲染通道后,GPU 将开始渲染场景,完成后反之亦然。
要创建渲染通道,我们需要创建一个编码器,负责将渲染通道编译为 GPU 代码。
const main = async () => { console.log('Hello, world!') } main()
然后我们创建一个渲染通道。
const adapter = await navigator.gpu.requestAdapter();
在这里,我们创建一个带有颜色附件的渲染通道。附件是 GPU 中的一个概念,表示将要渲染的图像。一张图像可能有很多个方面需要 GPU 处理,每个方面都是一个附件。
这里我们只有一个附件,就是颜色附件。视图是 GPU 将在其上渲染的面板,这里我们将其设置为画布的纹理。
loadOp是GPU在渲染通道之前执行的操作,clear表示GPU将首先清除最后一帧之前的所有数据,storeOp是GPU在渲染通道之后执行的操作,store表示GPU将把数据存储到纹理中。
loadOp可以是load,它保留最后一帧的数据,也可以是clear,它清除最后一帧的数据。 storeOp可以是store,将数据存储到纹理,也可以是discard,丢弃数据。
现在,只需调用 pass.end() 即可结束渲染通道。现在,该命令已保存在 GPU 的命令缓冲区中。
要获取编译后的命令,请使用以下代码,
const device = await adapter.requestDevice(); console.log(device);
最后,将命令提交到 GPU 的渲染队列。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
现在,您应该看到一个丑陋的黑色画布。
根据我们对 3D 的刻板印象,我们期望空白空间是蓝色的。我们可以通过设置透明颜色来做到这一点。
const encoder = device.createCommandEncoder();
现在,我们将在画布上绘制一个三角形。我们将使用着色器来做到这一点。着色器语言将是 wgsl,WebGPU 着色语言。
现在,假设我们要绘制一个具有以下坐标的三角形,
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store", }] });
正如我们之前所说,要完成渲染管道,我们需要一个顶点着色器和一个片段着色器。
使用以下代码创建着色器模块。
const commandBuffer = encoder.finish();
这里的label只是一个名称,用于调试。 code 是实际的着色器代码。
顶点着色器是一个接受任意参数并返回顶点位置的函数。然而,与我们的预期相反,顶点着色器返回一个四维向量,而不是一个三维向量。第四个维度是w维度,用于透视划分。我们稍后再讨论。
现在,您可以简单地将四维向量 (x, y, z, w) 视为三维向量 (x / w, y / w, z / w)。
但是,还有一个问题——如何将数据传递给着色器,以及如何从着色器中取出数据。
为了将数据传递给着色器,我们使用 vertexBuffer,一个包含顶点数据的缓冲区。我们可以使用以下代码创建一个缓冲区,
const main = async () => { console.log('Hello, world!') } main()
这里我们创建了一个缓冲区,大小为24字节,6个浮点数,这是顶点的大小。
usage是缓冲区的使用情况,对于顶点数据来说就是VERTEX。 GPUBufferUsage.COPY_DST 表示该缓冲区可作为复制目标。对于所有由CPU写入数据的缓冲区,我们需要设置这个标志。
这里的map是指将buffer映射到CPU,也就是说CPU可以对buffer进行读写操作。 unmap的意思是取消缓冲区的映射,这意味着CPU不能再读写缓冲区,因此内容可供GPU使用。
现在,我们可以将数据写入缓冲区。
const adapter = await navigator.gpu.requestAdapter();
这里,我们将缓冲区映射到CPU,并将数据写入缓冲区。然后我们取消映射缓冲区。
vertexBuffer.getMappedRange() 将返回映射到 CPU 的缓冲区范围。我们可以用它来将数据写入缓冲区。
但是,这些只是原始数据,GPU 不知道如何解释它们。我们需要定义缓冲区的布局。
const device = await adapter.requestDevice(); console.log(device);
这里,arrayStride是GPU在寻找下一个输入时需要在缓冲区中向前跳过的字节数。例如,如果 arrayStride 为 8,GPU 将跳过 8 个字节来获取下一个输入。
由于这里我们使用float32x2,步幅是8个字节,每个float 4个字节,每个顶点2个float。
现在我们可以编写顶点着色器了。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
这里,@vertex 表示这是一个顶点着色器。 @location(0) 表示属性的位置,如前面定义的那样,为 0。请注意,在着色器语言中,您正在处理缓冲区的布局,因此每当您传递一个值时,您需要传递一个结构体,其字段已定义@location,或者仅传递一个带有@location的值。
vec2f 是二维浮点向量,vec4f 是四维浮点向量。由于顶点着色器需要返回 vec4f 位置,因此我们需要使用 @builtin(position) 对其进行注释。
片段着色器,类似地,是获取插值顶点输出并输出附件(在本例中为颜色)的东西。插值意味着虽然只有顶点上的某些像素具有确定的值,但对于每隔一个像素,这些值都会被插值,可以是线性的、平均的或其他方式。 fragment的颜色是一个四维向量,即fragment的颜色,分别是红、绿、蓝、alpha。
请注意,颜色的范围是0到1,而不是0到255。此外,片段着色器定义的是每个顶点的颜色,而不是三角形的颜色。三角形的颜色由顶点的颜色通过插值确定。
由于我们目前不想控制片段的颜色,所以我们可以简单地返回一个常量颜色。
const main = async () => { console.log('Hello, world!') } main()
然后我们通过替换顶点和片段着色器来定义自定义渲染管道。
const adapter = await navigator.gpu.requestAdapter();
注意,在片段着色器中,我们需要指定目标的格式,也就是画布的格式。
在渲染过程结束之前,我们添加绘制调用。
const device = await adapter.requestDevice(); console.log(device);
这里,在setVertexBuffer中,第一个参数是缓冲区的索引,在管道定义字段buffers中,第二个参数是缓冲区本身。
调用draw时,参数是要绘制的顶点数。由于我们有 3 个顶点,因此我们绘制 3 个。
现在,您应该在画布上看到一个黄色三角形。
现在我们稍微调整一下代码 - 因为我们想要构建一个生活游戏,所以我们需要绘制正方形而不是三角形。
正方形实际上是两个三角形,所以我们需要画6个顶点。这里的改动很简单,不需要详细解释。
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
现在,您应该在画布上看到一个黄色方块。
我们没有讨论GPU的坐标系。嗯,这相当简单。 GPU实际的坐标系是右手坐标系,即x轴指向右侧,y轴指向上方,z轴指向屏幕外。
坐标系的范围是-1到1。原点位于屏幕中心。 z轴从0到1,0是近平面,1是远平面。然而,z 轴代表深度。当你做3D渲染时,你不能仅仅使用z轴来确定物体的位置,你需要使用透视划分。这称为 NDC,标准化设备坐标。
例如,要在屏幕左上角画一个正方形,顶点为 (-1, 1), (-1, 0), (0, 1), (0, 0) ,尽管你需要使用两个三角形来绘制它。
以上是网络上的三角形 抓取一些东西的详细内容。更多信息请关注PHP中文网其他相关文章!