【WebGPU】WebGPU 中的反应扩散计算着色器

在本教程中,我们将使用 WebGPU 技术中的计算着色器实现图像效果。更多精彩内容尽在数字孪生平台。

image.png

程序结构

主要构建两个 WebGPU 管道:

  • 运行反应扩散算法多次迭代的计算管道(js/rd-compute.jsjs/shader/rd-compute-shader.js
  • 渲染管道,它获取计算管道的结果并通过渲染全屏三角形(js/composite.jsjs/shader/composite-shader.js)来创建最终合成图像。

WebGPU 是一个非常繁琐的 API,为了使其更容易使用,我使用了 webgpu-utils 库。此外,还包含 float16 库,用于创建和更新计算管道的存储纹理。

计算管道流程

在 GPU 上运行反应扩散模拟的一种常见方法是使用纹理交替。就是创建两个纹理,一个纹理保存要读取的模拟的当前状态,另一个纹理存储当前迭代的结果。每次迭代后,纹理都会交换。

此方法也可以使用片段着色器和帧缓冲在 WebGL 中实现。但是在 WebGPU 中,我们可以使用计算着色器和存储纹理作为缓冲区来实现相同的效果。这样做的优点是我们可以直接写入我们想要的纹理内的任何像素,还获得了计算着色器带来的性能优势。

初始化

首先是使用所有必要的布局描述符初始化管道。此外,还必须设置所有的缓冲区、纹理和绑定组。webgpu-utils 库就可以在这里节省大量工作。

WebGPU 不允许在创建缓冲区或纹理后更改其大小。因此,我们必须区分大小不变的缓冲区(例如uniform)和在某些情况下发生变化的缓冲区(例如调整画布大小时的纹理)。对于后者,我们需要一种方法来重新创建它们并在必要时销毁旧的缓冲区。

用于反应扩散模拟的所有纹理都是画布大小的一小部分(例如画布大小的四分之一)。要处理的像素数量较少,可以释放计算资源以进行更多迭代。因此,可以以相对较小的视觉损失进行更快的模拟。

除了“纹理交换”中涉及的两个纹理之外,示例中还有第三个纹理,我将其称为种子纹理。此纹理包含在其上绘制时钟字母的 HTML 画布的图像数据。种子纹理用作反应扩散模拟的一种影响图,以可视化时钟字母。当 WebGPU 画布调整大小时,必须重新创建该纹理以及相应的 HTML 画布大小调整。

运行模拟

完成所有必要的初始化后,我们可以使用计算着色器实际运行反应扩散模拟。我们先回顾一下计算着色器的一些特性。

计算着色器的每次调用都会并行处理多个线程。线程数由计算着色器的工作组(workgroup)大小定义。着色器的调用次数由调度(dispatch)大小定义(线程总数 = 工作组大小 * 调度大小)。

这些值以三个维度指定。因此,并行处理 64 个线程的计算着色器可能如下所示:

@compute @workgroup_size(8, 8, 1) fn compute() {}

运行此着色器 256 次(即 16,384 个线程)需要如下的调度大小:

pass.dispatchWorkgroups(16, 16, 1);

反应扩散模拟要求我们处理纹理的每个像素。实现此目的的一种方法是使用 workgroup 大小为 1 和 dispatch大小等于像素总数(像是模仿片段着色器)。但是这样不会提高性能,因为 workgroup 中的多个线程比单独的调度更快。

另一方面,我们可能想到使用等于像素数的 workgroup 大小,并且仅调用一次(dispatch 大小为 1)。然而这是不可能的,因为最大 workgroup 大小是有限的。对于 WebGPU 的一般建议是选择 workgroup 大小为 64。这要求我们将纹理内的像素数量划分为 workgroup 大小(= 64 像素)的块,并经常调度工作组以覆盖整个纹理。

因此,现在我们有了 workgroup 大小的恒定值,并且能够找到适当的 dispatch 大小来运行我们的模拟。但是其实我们还有更多可以优化的地方。

每线程像素数

为了使每个workgroup覆盖更大的区域(更多像素),我们引入了图块大小。图块大小定义每个单独线程处理的像素数,这就需要我们在着色器中使用嵌套 for 循环,所以我们需要保持图块大小非常小(例如 2×2)。

像素缓存

运行反应扩散模拟的一个重要步骤是与拉普拉斯核(3×3 矩阵)进行卷积。因此,对于我们处理的每个像素,我们必须读取内核覆盖的所有 9 个像素才能执行计算。由于像素与像素之间的内核重叠,因此会出现大量冗余纹理读取。

幸运的是,计算着色器允许我们跨线程共享内存。所以我们可以创建像素缓存。这个方式(来自图像模糊示例)是每个线程读取其图块的像素并将它们写入缓存。一旦workgroup的每个线程都将其像素存储在缓存中(我们通过工作组屏障确保这一点),实际处理只需要使用从缓存中预取的像素。因此它不需要任何进一步的纹理读取。计算函数的结构可能如下所示:

// workgroup所有线程共享的像素缓存
var<workgroup> cache: array<array<vec4f, 128>, 128>;

@compute @workgroup_size(8, 8, 1)
fn compute_main(/* ...builtin variables */ ) {

  // 将此线程的图块的像素添加到缓存中
  for (var c=0u; c<2; c++) {
    for (var r=0u; r<2; r++) {
      // ... 从内置变量计算像素坐标
      // 将像素值存储在缓存中
      cache[y][x] = value;
    }
  }

  // 在所有线程都到达此点之前不要继续
  workgroupBarrier();

  // 处理该线程图块的每个像素
  for (var c=0u; c<2; c++) {
    for (var r=0u; r<2; r++) {
        // ... 执行反应扩散算法
        textureStore(/* ... */);
      }
    }
  }
}

但我们还必须注意另一个棘手的问题:内核卷积要求我们读取比最终处理的像素更多的像素。我们可以扩展像素缓存大小,但是workgroup线程共享的内存大小限制为 16,384 字节。因此,我们必须将每一侧的dispatch大小减少 (kernelSize - 1)/2。下面的插图可以让这些步骤更加清晰:
image.png

UV扰动

与片段着色器解决方案相比,使用计算着色器的一个缺点是无法在计算着色器中使用采样器来存储纹理(只能加载整数像素坐标)。如果想通过移动纹理空间(即以小数增量扰动 UV 坐标)来对模拟进行动画处理,则必须自己进行采样。

解决这个问题的一种方法是使用手动双线性采样函数。示例中使用的采样函数基于此处所示的采样函数,并进行了一些调整以供在计算着色器中使用。这允许我们对浮点像素值进行采样:

fn texture2D_bilinear(t: texture_2d<f32>, coord: vec2f, dims: vec2u) -> vec4f {
    let f: vec2f = fract(coord);
    let sample: vec2u = vec2u(coord + (0.5 - f));
    let tl: vec4f = textureLoad(t, clamp(sample, vec2u(1, 1), dims), 0);
    let tr: vec4f = textureLoad(t, clamp(sample + vec2u(1, 0), vec2u(1, 1), dims), 0);
    let bl: vec4f = textureLoad(t, clamp(sample + vec2u(0, 1), vec2u(1, 1), dims), 0);
    let br: vec4f = textureLoad(t, clamp(sample + vec2u(1, 1), vec2u(1, 1), dims), 0);
    let tA: vec4f = mix(tl, tr, f.x);
    let tB: vec4f = mix(bl, br, f.x);
    return mix(tA, tB, f.y);
}

这就是示例中所示的从中心开始的模拟脉动运动的创建方式。

合成渲染

反应扩散模拟完成后,唯一剩下的就是将结果绘制到屏幕上。这是合成渲染管道的工作。

我这里简要概述示例程序中涉及的步骤:

  1. 凸出变形:在对反应扩散结果纹理进行采样之前,将凸出变形应用于 UV 坐标(基于此 Shadertoy 代码),可以增加场景的深度感。
  2. 颜色:应用调色板(来自 Inigo Quilez)
  3. 浮雕滤镜:简单的浮雕效果赋予“纹理”一定的体积。
  4. 假彩虹色:这种微妙的效果基于不同的调色板,但应用于压花结果的负空间。假虹彩使场景看起来更加充满活力。
  5. 晕影:晕影叠加用于使边缘变暗。

image.png

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习之路 -- 文件系统 -- 缓冲区

前面介绍了文件描述符的相关知识&#xff0c;下面我们将介绍缓冲区的相关知识。 本质上来说&#xff0c;缓冲区就是一块内存区域&#xff0c;因为内核上的缓冲区较复杂&#xff0c;所以本文主要介绍C语言的缓冲区。 目录 1.为什么要有缓冲区 2.应用层缓冲区的默认刷新策略 …

【C++】STL — map和set的使用详细介绍

前言 本章将继续学习STL中的两个很重要的容器map和set&#xff0c;其底层实现是封装了一个红黑树&#xff0c;我们通过本节来学习和深入了解一下这两大容器。。。 序列式容器&#xff1a; string 、Vector、List 、dequeue 关联式容器&#xff1a;MAP 、SET、nordered_map、uno…

成员函数构造函数析构函数

文章目录 类的6个默认成员函数构造函数概述定义特性 析构函数概述特性 类的6个默认成员函数 空类&#xff1a; 如果一个类里面什么都没有写&#xff0c;我们称之为空类 class Date {};空类真的什么都没有吗&#xff1f; 实际上并非如此&#xff0c;编译器会自动生成6个默认成…

【大数据】HDFS

文章目录 [toc]HDFS 1.0NameNode维护文件系统命名空间存储元数据解决NameNode单点问题 SecondaryNameNode机架感知数据完整性校验校验和数据块检测程序DataBlockScanner HDFS写流程HDFS读流程HDFS与MapReduce本地模式Block大小 HDFS 2.0NameNode HANameNode FederationHDFS Sna…

C++笔试强训day19

目录 1.小易的升级之路 2.礼物的最大价值 3.对称之美 1.小易的升级之路 链接 模拟就行&#xff0c;唯一可能是难点得就是gcd&#xff08;最大公约数&#xff09; #include <iostream> using namespace std; #define int long long const int N 1e5 10; int arr[N];…

【DIY小记】深圳万象天地餐馆探店点评

第一次在技术博客里面写生活日记&#xff0c;也算是破了个小天荒。个人以为&#xff0c;博客是个人生活思考的载体&#xff0c;而技术只占生活的一部分&#xff0c;那么博客里为什么一定要限制只能够写技术内容&#xff0c;不能写点其它生活上的东西呢&#xff1f;思来想去&…

科研诚信与学术规范 2024年春 期末考试答案

章节答案&#xff1a;https://www.bilibili.com/video/BV1JZ42177F8/ 是这个课&#xff0c;网上的大多数答案都是以前的&#xff0c;跟这门课没啥关系. 期末考试的答案长这样&#xff0c;题库有80个题&#xff0c;考试一般是50个题。 期末考试答案&#xff1a;&#xff08;不…

C++动态内存区域划分、new、delete关键字

目录 一、C/C中程序的内存区域划分 为什么会存在内存区域划分&#xff1f; 二、new关键字 1、内置类型的new/delete使用方法&#xff1a; 2、new和delete的本质 一、C/C中程序的内存区域划分 为什么会存在内存区域划分&#xff1f; 因为不同数据有不同的存储需求&#xff0…

6818Linux内核--Bootloader应用分析

Bootloader应用分析 一个嵌入式 Linux 系统从软件的角度看通常可以分为四个层次&#xff1a; 引导加载程序。包括固化在固件( firmware )中的 boot 代码(可选)&#xff0c;和 Boot Loader 两大部分。 Linux 内核。特定于嵌入式板子的定制内核以及内核的启动参数。 文件系统…

Polygon市值机器人

随着区块链技术的蓬勃发展和数字货币市场的日益繁荣&#xff0c;投资者们对于如何精准把握市场动态、实现资产稳健增长的需求愈发迫切。在这个背景下&#xff08;市值管理飞//机//aishutuyu&#xff09;&#xff0c;Polygon市值机器人应运而生&#xff0c;作为一款基于Polygon公…

TriCore:Interrupt

今天简单总结下 TriCore 的中断路由模块。 名词缩写 缩写全称说明IRInterrupt Router SRService Request 包括&#xff1a; 1. External Resource 2. Internal Resource 3.SW&#xff08;Software&#xff09; SPService Privoder 包括&#xff1a; 1. CPU 2. DMA SRNServic…

【5分钟学会一个知识点】01.Elasticsearch基本操作-增删改查

目录 【5分钟学会一个知识点-探索现代搜索与分析引擎的魅力】01.Elasticsearch基本操作-增删改查1.基本操作1.1索引操作1.2文档操作1.3查询1.4修改数据1.5查询1.5.1条件查询1.5.1.1遍历所有的索引1.5.1.2查询某个索引1.5.1.3条件查询1&#xff1a;使用GET url传参数1.5.1.4条件…

proteus数模转换器DAC0832的应用

proteus proteus&#xff0c;即EDA工具软件。Proteus软件是英国Lab Center Electronics公司出版的EDA工具软件。它不仅具有其它EDA工具软件的仿真功能&#xff0c;还能仿真单片机及外围器件。它是比较好的仿真单片机及外围器件的工具。虽然国内推广刚起步&#xff0c;但已受到…

油泼辣子在食品类别可以申请成商标不!

前阵韩国人在美国申请“chili crunch”油泼辣子作为商标&#xff0c;还准备禁止华人餐馆使用投诉侵权并索赔&#xff0c;普推知产老杨在USPTO上面检索发现&#xff0c;这个人申请的主要是30类方便食品的调味品&#xff0c;商标分类是全球通用的。 商标名称不能申请本类所属的通…

C++的数据结构(三):栈

栈&#xff08;Stack&#xff09;是一种后进先出&#xff08;LIFO, Last In First Out&#xff09;的数据结构&#xff0c;它只允许在一端&#xff08;称为栈顶&#xff09;进行插入和删除操作。栈的这种特性使得它在解决函数调用、括号匹配、表达式求值等问题时具有天然的优势…

Linux下多线程相关概念

thread 1.什么是线程1.1 线程优缺点1.2 线程异常1.3 线程用途 2. 进程和线程区别3. 线程控制3.1 POSIX线程库3.2 pthread_create()3.3 线程ID3.4 线程ID地址空间布局pthread_self() 3.5 线程终止pthread_exit函数pthread_cancle函数 3.6 线程等待3.7 分离线程__thread修饰全局变…

【安全每日一讲】加强数据安全保护 共享数字化时代便利

前言 数据安全是数据治理的核心内容之一&#xff0c;随着数据治理的深入&#xff0c;我不断的碰到数据安全中的金发姑娘问题&#xff08;指安全和效率的平衡&#xff09;。 DAMA说&#xff0c;降低风险和促进业务增长是数据安全活动的主要驱动因素&#xff0c;数据安全是一种资…

Django项目之电商购物商城 -- 修改/删除收货地址/设置默认地址

Django项目之电商购物商城 – 修改/删除收货地址/设置默认地址 修改和删除收货地址依旧实在user应用下进行 , 其思路和新增收货地址非常相似 依旧是更具前端的数据来写 在这里修改和删除地址的URL是相同的 , 所以我们只要设置一个模型类就可以实现这两个功能 一 . 修改地址…

apk反编译修改教程系列-----反编译apk 去除软件强制更新的八种方式步骤解析【十七】

安卓有的apk 软件会不断更新。但有些用户需要旧版的有些功能或者新版功能增减原因等等。需要不更新继续使用。这类问题有的可以简单修改版本号来跳过更新。或者有的软件可以忽略。但对于某些无法跳过更新界面等等的apk。就需要深度反编译来去除软件的强制更新。 通过课程可以了…

Obsidian/Typora设置图床

在obsidian中默认图片是保存在本地的&#xff0c;但是在要导出文档上传到网上时&#xff0c;由于图片保存在本地&#xff0c;会出现无法加载图片的问题。 这里引用的一段话&#xff1a; 这里使用picgo-core和gitee实现图床功能&#xff0c; 参考1&#xff1a; Ubuntu下PicGO配…