【WebGPU Unleashed】1.1 绘制三角形

2024-09-09 16:20

本文主要是介绍【WebGPU Unleashed】1.1 绘制三角形,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一部2024新的WebGPU教程,作者Shi Yan。内容很好,翻译过来与大家共享,内容上会有改动,加上自己的理解。更多精彩内容尽在
dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123

在 3D 渲染领域,三角形是最基本的绘制元素。在这里,我们将学习如何绘制单个三角形。接下来我们将制作一个简单的着色器来定义三角形内的像素颜色,以及了解如何建立一个图形管线,然后利用该三角形并使用着色器将其渲染在屏幕上。就像传统编程中的“Hello World”程序一样,绘制三角形基本算是任何图形API的初始介绍。

着色器介绍

在前面的示例中,我们没有创建任何着色器。着色器程序是在GPU上执行的程序,一般来说,着色器程序主要分为三种类型:顶点着色器、片段着色器和计算着色器。计算着色器用于通用计算,而顶点和片段着色器与渲染相关。顶点着色器处理几何体的每个顶点,确定其在屏幕上的最终位置。然后,片段着色器确定由这些顶点定义的形状内每个像素的颜色。这些着色器共同将几何图元(例如点或三角形)转换为我们在屏幕上看到的像素。

<script id="shader" type="wgsl">...
</script>

现在我们了解了着色器的作用,接下来我们将它们添加到我们的项目中。首先,我们将在 HTML 中创建另一个脚本标签来保存着色器代码,我们将其类型设置为 wgsl,代表 WebGPU 着色器语言。除了类型之外,我们还需要给它一个着色器的 id,因为我们稍后需要读取它的内容。此处注意我们不需要将着色器代码放入脚本标记中,我们可以选择将着色器代码分配给 JavaScript 字符串,也可以将它们写入外部文件并将其提取到代码中。

struct VertexOutput {@builtin(position) clip_position: vec4<f32>,
};@vertexfn vs_main(@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {var out: VertexOutput;let x = f32(1 - i32(in_vertex_index)) * 0.5;let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5;out.clip_position = vec4<f32>(x, y, 0.0, 1.0);return out;
}@fragmentfn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {return vec4<f32>(0.3, 0.2, 0.1, 1.0);
}

我们的第一个着色器将以纯色渲染三角形。尽管听起来很简单,但代码可能看起来很复杂。接下来我们剖析代码以更好地理解它的组成部分。

着色器代码剖析

着色器程序定义 GPU 管线的行为。 GPU 管线的工作方式就像一个小工厂,包含一系列阶段或车间。典型的 GPU 管线由两个主要阶段组成:

  1. 顶点阶段:处理几何数据并生成画布对齐的几何图形。
  2. 片段阶段:GPU 将顶点阶段的输出转换为片段后,片段着色器为它们分配颜色。

在我们的着色器代码中,有两个入口函数:

  • vs_main:代表顶点阶段,用@vertex注解
  • fs_main:代表fragment阶段,用@fragment注解

虽然 vs_main 函数 @builtin(vertex_index) in_vertex_index: u32 的输入看起来与 C 类型语言中的函数参数类似,但它是不同的。这里,in_vertex_index是变量名,u32是类型(32位无符号整数)。 @builtin(vertex_index) 是一个特殊的装饰器。

在 WGSL 中,着色器输入并不是真正的函数参数。我们可以想象一个包含多个字段的预定义表单,每个字段都有一个标签,@builtin(vertex_index) 就是这样的标签之一。对于管线阶段的输入,我们不能随意地提供数据,而是必须从这个预定义的集中选择字段。在这种情况下,@builtin(vertex_index)是实际的参数名称,而in_vertex_index只是我们给它的别名。

@builtin 装饰器表示一组预定义字段。我们还会遇到其他装饰器,例如@location,稍后我们将讨论它们以了解它们的差异。

着色器阶段输出遵循类似的原则。我们不能输出任意数据,只能填充一些预定义的字段。在我们的示例中,我们输出了一个 struct VertexOutput。虽然它看起来像是自定义的,但是它包含一个预定义字段 @builtin(position),我们将在其中写入结果。

屏幕空间坐标系

顶点着色器

顶点着色器的内容乍一看可能令人费解。在我们深入研究之前,我先解释一下顶点着色器的主要目标。顶点着色器接收几何图形作为单独的顶点,在这个阶段,我们缺乏几何之间的连接信息,也就是不知道哪些顶点连接形成三角形,所以我们处理各个顶点的目的就只是转换它们的位置以与画布对齐。

如果没有顶点转换,那么我们也就无法正确看见顶点。由顶点着色器接收的顶点位置通常在其自己的坐标系中定义,为了将它们显示在画布上,我们必须将输入顶点使用的坐标系统一到画布的坐标系中。此外,顶点可以存在于 3D 空间中,而画布始终是 2D,将 3D 坐标转换为 2D 的过程称为投影。

现在,让我们了解一下画布的坐标系,该坐标系通常称为屏幕空间或裁剪空间(注:按照常理来说其实是NDC空间,裁剪空间一般是没有进行透视除法,而屏幕空间是画布像素坐标,按NDC理解就好)。尽管在 WebGPU 中我们通常渲染到画布而不是直接渲染到屏幕,但术语“屏幕空间坐标系”是从其他本机 3D API 继承的。屏幕空间坐标系的原点位于中心,x 和 y 坐标都限制在 [-1, 1] 范围内。无论屏幕或画布大小如何,该坐标系都保持不变。

回想一下之前的教程,我们可以定义视口大小,但不会影响坐标系。无论视口定义如何,屏幕空间坐标系都保持不变。只要顶点的坐标落在 [-1, 1] 范围内,顶点就是可见的。渲染管道会自动拉伸屏幕空间坐标系以匹配我们定义的视口。例如,如果我们的视口为 640x480,即使纵横比为 4:3,画布坐标系的 x 和 y 区间仍为 [-1, 1]。如果在位置 (1, 1) 处绘制顶点,它将出现在右上角。当呈现在画布上时,位置 (1, 1) 将被拉伸到 (640, 0)。

在上面的代码中,我们的输入是顶点索引而不是顶点位置。三角形有三个顶点,我们可以把索引设为 0、1 和 2。在没有顶点位置作为输入的情况下,我们可以根据这些索引来生成它们的位置。我们的目标是为每个索引生成唯一的位置,同时确保该位置落在 [-1, 1] 范围内,使整个三角形可见。如果我们用 0, 1, 2 代替 vertex_index,我们将分别得到位置 (0.5, -0.5)、(0, 0.5) 和 (-0.5, -0.5)。

let x = f32(1 - i32(in_vertex_index)) * 0.5;
let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5;

剪辑位置(剪辑空间中的位置)由 4 位浮点向量表示。对于屏幕空间中的 2D 三角形,第三个分量始终为零。最后一个值设置为 1.0。当我们稍后探索相机和矩阵转换时,我们将深入研究最后两个值的细节。

如前所述,顶点阶段的输出经过光栅化,生成带有插值顶点值的片段。在我们的简单示例中,唯一的插值就是顶点位置。

片段着色器

片段着色器的输出由另一个名为@location(0) 的预定义字段定义。每个位置最多可以存储 16 个字节的数据,相当于四个 32 位浮点数,可用位置的总数由特定的 WebGPU 实现决定。

对于locationbuiltin之间的区别,我们可以将location视为非结构化自定义数据,除了索引之外,它们没有任何标签。这个概念与 HTTP 协议类似,其中我们有一个结构化消息头(类似于builtin),后面跟着可以包含任意数据的正文或有效负载(类似于location)。如果你熟悉解码二进制文件,它相当于具有带有元数据的结构化标头,后面跟着一块数据作为有效负载。在我们的上下文中,builtin函数和location共享这个概念结构。

本例中的片段着色器非常简单:它只是将纯色输出到@location(0)

let code = document.getElementById('shader').innerText;
const shaderDesc = { code: code };
let shaderModule = device.createShaderModule(shaderDesc);

编写着色器代码只是渲染简单三角形的一部分。现在让我们研究一下如何修改管线以合并此着色器代码。该过程涉及几个步骤:

  1. 我们从第一个脚本标签中检索着色器代码字符串,这就是为什么要给标签加 id='shader' 属性。
  2. 我们构建一个包含源代码的着色器描述对象。
  3. 我们通过向 WebGPU API 提供着色器描述来创建着色器模块。

需要注意的是,在此示例中我们没有实现错误处理。如果发生编译错误,我们最终会得到一个无效的着色器模块。在这种情况下,浏览器的控制台消息对于调试就会非常有帮助。通常,着色器代码是由开发人员在开发阶段定义的,并且所有着色器问题很可能都会在部署代码之前得到解决。因此,我们在这个基本示例中省略了错误处理。但是,如果是在生产环境中,那么还是建议实施严密的错误检查。

const pipelineLayoutDesc = { bindGroupLayouts: [] };
const layout = device.createPipelineLayout(pipelineLayoutDesc);

接下来,我们开始定义管线布局。那么管线布局到底是什么呢?它指的是我们打算提供给管线的常量结构。每个布局layout代表一组我们想要输入管线的常量。

一个管线可以有多组常量,这就是为什么bindGroupLayouts被定义为一个列表。这些常量在管线的整个执行过程中都保持其数值。

在我们当前的示例中,不需要提供任何常量。因此,我们的管线布局是空的。

const colorState = {format: 'bgra8unorm'
};

我们管线配置的下一步是指定输出像素格式。在本例中,我们使用 bgra8unorm。这种格式定义了我们如何填充渲染目标。详细来说,bgra8unorm 各分量的意思:

  • ‘b’、‘g’、‘r’、‘a’:蓝色、绿色、红色、Alpha 通道
  • ‘8’:每个通道使用8位
  • ‘unorm’:值是无符号且标准化的(范围从 0 到 1)
const pipelineDesc = {layout,vertex: {module: shaderModule,entryPoint: 'vs_main',buffers: []},fragment: {module: shaderModule,entryPoint: 'fs_main',targets: [colorState]},primitive: {topology: 'triangle-list',frontFace: 'ccw',cullMode: 'back'}
};pipeline = device.createRenderPipeline(pipelineDesc);

定义管线

组装完所有必要的组件后,现在终于可以创建管线了。 GPU 管线类似于真实的工厂管线,由输入、一系列处理阶段和最终输出组成。其中,layoutprimitive描述了输入数据格式,layout指的是常量,而primitive指定应如何提供几何基元。

基本上实际输入数据是通过缓冲区提供的,一般都包含顶点数据,包括顶点位置和其他属性,例如顶点颜色和纹理坐标。但是在我们当前的示例中,我们不使用任何缓冲区。我们不是直接输入顶点位置,而是在顶点着色器阶段从顶点索引中导出它们,这些索引由 GPU 管线自动提供给顶点着色器。

通常我们提供的都是没有显式连接信息的顶点列表,而不是作为完整的 3D 图形元素(例如三角形),管线会根据拓扑场从这些顶点重建出三角形。例如,如果topology字段设置为triangle-list,则表示顶点列表以逆时针或顺时针顺序表示三角形顶点。每个三角形都有一个正面和一个背面,frontFace:'ccw' 定义了三角形顶点顺序为正面。

cullMode 参数决定我们是否要消除三角形特定面的渲染。将其设置为back就表示我们选择不渲染三角形的背面。在大多数情况下不应渲染三角形的背面,省略背面的绘制可以节省计算资源。

使用triangle-list拓扑是表示三角形的最直接的方法,但它并不总是最有效的方法。如下图所示,当我们想要渲染一条由连接的三角形组成的条带时,它的许多顶点会被多个三角形共享。

两个三角形形成一个三角形带。在右手系统中,正顶点顺序围绕三角形法线逆时针方向

在这种情况下,我们希望重用多个三角形的顶点位置,而不是为不同的三角形多次发送相同的位置。这就是triangle-strip拓扑成为更好选择的地方。它使我们能够更有效地定义一系列连接的三角形,减少数据冗余并有可能提高渲染性能。我们将在以后的章节中探讨其他拓扑类型。

commandEncoder = device.createCommandEncoder();passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1);
passEncoder.end();device.queue.submit([commandEncoder.finish()]);

定义管线后,我们需要创建 colorAttachment,这与我们在第一个教程中介绍的类似,因此我将在此处省略详细信息。最后一步是命令创建和提交。这个过程与我们之前所做的几乎相同,主要区别在于新创建的管线的使用和draw()函数的调用。

draw() 函数触发渲染过程。第一个参数指定我们要渲染的顶点数,第二个参数表示实例数。由于我们渲染的是单个三角形,因此顶点总数为 3。顶点索引是为顶点着色器自动生成的。实例数决定了我们要复制三角形的次数。当我们需要渲染大量相同的几何图形(例如视频游戏中的草或树叶)时,该技术可以加快渲染速度。在此示例中,我们指定单个实例,因为我们只需要绘制一个三角形。

这篇关于【WebGPU Unleashed】1.1 绘制三角形的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1151665

相关文章

usaco 1.1 Broken Necklace(DP)

直接上代码 接触的第一道dp ps.大概的思路就是 先从左往右用一个数组在每个点记下蓝或黑的个数 再从右到左算一遍 最后取出最大的即可 核心语句在于: 如果 str[i] = 'r'  ,   rl[i]=rl[i-1]+1, bl[i]=0 如果 str[i] = 'b' ,  bl[i]=bl[i-1]+1, rl[i]=0 如果 str[i] = 'w',  bl[i]=b

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

利用matlab bar函数绘制较为复杂的柱状图,并在图中进行适当标注

示例代码和结果如下:小疑问:如何自动选择合适的坐标位置对柱状图的数值大小进行标注?😂 clear; close all;x = 1:3;aa=[28.6321521955954 26.2453660695847 21.69102348512086.93747104431360 6.25442246899816 3.342835958564245.51365061796319 4.87

CSS实现DIV三角形

本文内容收集来自网络 #triangle-up {width: 0;height: 0;border-left: 50px solid transparent;border-right: 50px solid transparent;border-bottom: 100px solid red;} #triangle-down {width: 0;height: 0;bor

YOLOv8/v10+DeepSORT多目标车辆跟踪(车辆检测/跟踪/车辆计数/测速/禁停区域/绘制进出线/绘制禁停区域/车道车辆统计)

01:YOLOv8 + DeepSort 车辆跟踪 该项目利用YOLOv8作为目标检测模型,DeepSort用于多目标跟踪。YOLOv8负责从视频帧中检测出车辆的位置,而DeepSort则负责关联这些检测结果,从而实现车辆的持续跟踪。这种组合使得系统能够在视频流中准确地识别并跟随特定车辆。 02:YOLOv8 + DeepSort 车辆跟踪 + 任意绘制进出线 在此基础上增加了用户

使用matplotlib绘制散点图、柱状图和饼状图-学习篇

一、散点图 Python代码如下: num_points = 100x = np.random.rand(num_points) #x点位随机y = np.random.rand(num_points) #y点位随机colors = np.random.rand(num_points) #颜色随机sizes = 1000 * np.random.rand(num_points) # 大

双指针(5)_单调性_有效三角形的个数

个人主页:C++忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创 双指针(5)_单调性_有效三角形的个数 收录于专栏【经典算法练习】 本专栏旨在分享学习C++的一点学习笔记,欢迎大家在评论区交流讨论💌 目录 1. 题目链接: 2.题目描述 : 3.解法 :     解法一(暴力枚举) :     算法思路 :     代码展示 : 暴力枚

黑神话:悟空》增加草地绘制距离MOD使游戏场景看起来更加广阔与自然,增强了游戏的沉浸式体验

《黑神话:悟空》增加草地绘制距离MOD为玩家提供了一种全新的视觉体验,通过扩展游戏中草地的绘制距离,增加了场景的深度和真实感。该MOD通过增加草地的绘制距离,使游戏场景看起来更加广阔与自然,增强了游戏的沉浸式体验。 增加草地绘制距离MOD安装 1、在%userprofile%AppDataLocalb1SavedConfigWindows目录下找到Engine.ini文件。 2、使用记事本编辑

利用向量积(叉积)计算三角形的面积和多边形的面积(hdu2036)

开始撸计算几何题目了。。。。。。。 预备知识:叉乘求多边形面积 参考证明资料: 公式证明: http://www.cnblogs.com/xiexinxinlove/p/3708147.html 高中知识: http://wenku.baidu.com/view/867e6edfad51f01dc281f11a.html #include<stdio.h>#inclu

1.1 Avtivity的生命周期全面分析

本文将Activity的生命周期分为两部分内容,一部分是典型情况下的生命周期,另一部分是异常情况下的生命周期。所谓典型情况下的生命周期,是指在有用户参与的情况下,Activity所经过的生命周期的改变;而异常情况下的生命周期是指在Activity被系统回收或者由于当前设备的Configuration发生改变从而导致Activity被销毁重建,异常情况下的生命周期的关注点和典型情况下略有不同。 1