Taichi.js 实战 WebGPU 编程

news2024/11/15 11:07:43

作为一名计算机图形学和编程语言极客,我很高兴在过去两年中参与了多个 GPU 编译器的开发。

这始于 2021 年,当时我开始为 taichi 做贡献,这是一个 Python 库,可将 Python 函数编译为 CUDA、Metal 或 Vulkan 中的 GPU 内核。后来,我加入了 Meta,开始研究 SparkSL,这是一种着色器语言,为 Instagram 和 Facebook 上的 AR 效果的跨平台 GPU 编程提供支持。

除了个人乐趣之外,我一直相信,或者至少希望,这些框架非常有用。它们使非专家更容易进行 GPU 编程,使人们无需掌握复杂的 GPU 概念即可创建迷人的图形内容。

在我最新的编译器文章中,我将目光转向了 WebGPU——下一代 Web 图形 API。WebGPU 承诺通过低 CPU 开销和显式 GPU 控制带来高性能图形,这与七年前 Vulkan 和 D3D12 开创的趋势保持一致。

就像 Vulkan 一样,WebGPU 的性能优势是以陡峭的学习曲线为代价的。虽然我相信这不会阻止世界各地的优秀程序员使用 WebGPU 构建精彩的内容,但我想为人们提供一种使用 WebGPU 的方法,而不必面对它的复杂性。这就是 taichi.js 的由来。

在 taichi.js 编程模型下,程序员不必考虑设备、命令队列、绑定组等 WebGPU 概念。相反,他们编写普通的 JavaScript 函数,编译器将这些函数转换为 WebGPU 计算或渲染管道。这意味着任何人都可以通过 taichi.js 编写 WebGPU 代码,只要他们熟悉基本的 JavaScript 语法。

本文的其余部分将通过“生命游戏”程序演示 taichi.js 的编程模型。如你所见,使用不到 100 行代码,我们将创建一个完全并行的 WebGPU 程序,其中包含三个 GPU 计算管道和一个渲染管道。该演示的完整源代码可在此处找到,如果你想使用代码而不必设置任何本地环境,请转到此页面。

NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 

1、游戏

生命游戏是元胞机或细胞自动机(cellular automaton)的一个经典例子,细胞自动机是一种根据简单规则随时间演化的细胞系统。它由数学家约翰·康威于 1970 年发明,自此成为计算机科学家和数学家的最爱。游戏在二维网格上进行,每个细胞都可以是活的或死的。游戏规则很简单:

  • 如果活细胞的活细胞邻居少于两个或多于三个,它就会死亡
  • 如果死细胞的活细胞邻居恰好有三个,它就会复活。

尽管生命游戏很简单,但它可能会表现出令人惊讶的行为。从任何随机的初始状态开始,游戏通常会收敛到一种状态,其中一些模式占主导地位,就好像这些模式是通过进化而幸存下来的“物种”。

2、模拟

让我们深入研究使用 taichi.js 的生命游戏实现。首先,我们在简写 ti 下导入 taichi.js 库,并定义一个包含所有逻辑的异步 main() 函数。在 main() 中,我们首先调用 ti.init(),它初始化库及其 WebGPU 上下文。

import * as ti from "path/to/taichi.js"

let main = async () => {
    await ti.init();
    ...
};

main()

在 ti.init() 之后,让我们定义“生命游戏”模拟所需的数据结构:

    let N = 128;

    let liveness = ti.field(ti.i32, [N, N])
    let numNeighbors = ti.field(ti.i32, [N, N])

    ti.addToKernelScope({ N, liveness, numNeighbors });

这里,我们定义了两个变量, liveness 和 numNeighbors,它们都是 ti.fields。在 taichi.js 中,“fields”本质上是一个 n 维数组,其维数由 ti.field() 的第二个参数提供。数组的元素类型在第一个参数中定义。在本例中,我们有 ti.i32,表示 32 位整数。但是,字段元素也可能是更复杂的类型,包括向量、矩阵和结构。

下一行代码 ti.addToKernelScope({...}) 确保变量 N、 liveness 和 numNeighbors 在 taichi.js“内核”中可见,这些“内核”是以 JavaScript 函数形式定义的 GPU 计算和/或渲染管道。例如,以下初始化内核用于用初始活跃度值填充我们的网格单元,其中每个单元最初有 20% 的存活几率:

    let init = ti.kernel(() => {
        for (let I of ti.ndrange(N, N)) {
            liveness[I] = 0
            let f = ti.random()
            if (f < 0.2) {
                liveness[I] = 1
            }
        }
    })
    init()

init() 内核是通过调用 ti.kernel() 并以 JavaScript lambda 作为参数创建的。在底层,taichi.js 将查看此 lambda 的 JavaScript 字符串表示并将其逻辑编译为 WebGPU 代码。在这里,lambda 包含一个 for 循环,其循环索引 I 通过 ti.ndrange(N, N) 进行迭代。这意味着我将取 NxN 个不同的值,范围从 [0, 0] 到 [N-1, N-1]

神奇的部分来了 — 在 taichi.js 中,内核中的所有顶层 for 循环都将并行化。更具体地说,对于循环索引的每个可能值,taichi.js 将分配一个 WebGPU 计算着色器线程来执行它。在这种情况下,我们为“生命游戏”模拟中的每个单元专门分配一个 GPU 线程,将其初始化为随机活动状态。随机性来自 ti.random() 函数,这是 taichi.js 库中提供的供内核使用的众多函数之一。这些内置实用程序的完整列表可在 taichi.js 文档中找到。

创建游戏的初始状态后,让我们继续定义游戏如何演变。以下是定义此演变的两个 taichi.js 内核:

    let countNeighbors = ti.kernel(() => {
        for (let I of ti.ndrange(N, N)) {
            let neighbors = 0
            for (let delta of ti.ndrange(3, 3)) {
                let J = (I + delta - 1) % N
                if ((J.x != I.x || J.y != I.y) && liveness[J] == 1) {
                    neighbors = neighbors + 1;
                }
            }
            numNeighbors[I] = neighbors
        }
    });
    let updateLiveness = ti.kernel(() => {
        for (let I of ti.ndrange(N, N)) {
            let neighbors = numNeighbors[I]
            if (liveness[I] == 1) {
                if (neighbors < 2 || neighbors > 3) {
                    liveness[I] = 0;
                }
            }
            else {
                if (neighbors == 3) {
                    liveness[I] = 1;
                }
            }
        }
    })

与我们之前看到的 init() 内核一样,这两个内核也具有顶层 for 循环,可遍历每个网格单元,这些循环由编译器并行化。在 countNeighbors() 中,对于每个单元,我们查看八个相邻单元并计算这些邻居中有多少个“活着”。

活着的邻居的数量存储在 numNeighbors 字段中。请注意,在遍历邻居时, for (let delta of ti.ndrange(3, 3)) {...} 循环未并行化,因为它不是顶层循环。循环索引 delta 范围从  [0, 0] 到 [2, 2],用于偏移原始单元索引 I。我们通过对 N 取模来避免越界访问。(对于拓扑倾向的读者来说,这基本上意味着游戏具有环形边界条件)。

计算每个单元的邻居数量后,我们在 updateLiveness() 内核中更新它们的活跃状态。这很简单,只需读取每个单元的活跃状态及其当前活跃邻居的数量,并根据游戏规则写回新的活跃值即可。与往常一样,此过程并行应用于所有单元。

这基本上结束了游戏模拟逻辑的实​​现。接下来,我们将了解如何定义 WebGPU 渲染管道以将游戏的演变绘制到网页上。

3、渲染

在 taichi.js 中编写渲染代码比编写通用计算内核稍微复杂一些,并且确实需要对顶点着色器、片段着色器和光栅化管道有一定的了解。但是,你会发现 taichi.js 的简单编程模型使这些概念非常容易使用和推理。

在绘制任何内容之前,我们需要访问要在其上绘制的一块画布。假设 HTML 中存在一个名为 result_canvas 的画布,则以下代码行将创建一个 ti.CanvasTexture 对象,该对象表示可以通过 taichi.js 渲染管道在其上渲染的一块纹理。

    let htmlCanvas = document.getElementById('result_canvas');
    htmlCanvas.width = 512;
    htmlCanvas.height = 512;
    let renderTarget = ti.canvasTexture(htmlCanvas);

在我们的画布上,我们将渲染一个正方形,并将游戏的 2D 网格绘制到这个正方形上。在 GPU 中,要渲染的几何图形表示为三角形。在这种情况下,我们尝试渲染的正方形将表示为两个三角形。这两个三角形在 ti.field 中定义,它存储了两个三角形的六个顶点的坐标:

    let vertices = ti.field(ti.types.vector(ti.f32, 2), [6]);
    await vertices.fromArray([
        [-1, -1],
        [1, -1],
        [-1, 1],
        [1, -1],
        [1, 1],
        [-1, 1],
    ]);

正如我们对 liveness 和 numNeighbors 字段所做的那样,我们需要在 taichi.js 中明确声明 renderTarget 和 vertices 变量在 GPU 内核中可见:

    ti.addToKernelScope({ vertices, renderTarget });

现在我们已经有了实现渲染管道所需的所有数据。以下是管道本身的实现:

    let render = ti.kernel(() => {
        ti.clearColor(renderTarget, [0.0, 0.0, 0.0, 1.0]);
        for (let v of ti.inputVertices(vertices)) {
            ti.outputPosition([v.x, v.y, 0.0, 1.0]);
            ti.outputVertex(v);
        }
        for (let f of ti.inputFragments()) {
            let coord = (f + 1) / 2.0;
            let texelIndex = ti.i32(coord * (liveness.dimensions - 1));
            let live = ti.f32(liveness[texelIndex]);
            ti.outputColor(renderTarget, [live, live, live, 1.0]);
        }
    });

接下来,我们定义两个顶层 for 循环,正如您所知,它们是在 WebGPU 中并行化的循环。但是,与之前迭代 ti.ndrange 对象的循环不同,这些循环分别迭代 ti.inputVertices(vertices) 和 ti.inputFragments()。这表明这些循环将被编译成 WebGPU“顶点着色器”和“片段着色器”,它们一起作为渲染管道工作。

顶点着色器有两个职责。

对于每个三角形顶点,计算其在屏幕上的最终位置(或者更准确地说,其“剪辑空间”坐标)。在 3D 渲染管道中,这通常涉及一堆矩阵乘法,将顶点的模型坐标转换为世界空间,然后转换为相机空间,最后转换为“剪辑空间”。但是,对于我们简单的 2D 正方形,顶点的输入坐标在剪辑空间中已经是其正确值,因此我们可以避免所有这些。我们要做的就是附加一个固定的 z 值 0.0 和一个固定的 w 值 1.0(如果您不知道这些是什么,请不要担心 - 这在这里并不重要!)。

    ti.outputPosition([v.x, v.y, 0.0, 1.0]);

对于每个顶点,生成要插值的数据,然后将其传递到片段着色器中。在渲染管道中,执行顶点着色器后,将对所有三角形执行一个称为“光栅化”的内置过程。这是一个硬件加速的过程,它计算每个三角形覆盖的像素。这些像素也称为“片段”。

对于每个三角形,程序员可以在三个顶点中的每一个顶点生成附加数据,这些数据将在光栅化阶段进行插值。对于像素中的每个片段,其对应的片段着色器线程将根据其在三角形内的位置接收插值。在我们的例子中,片段着色器只需要知道片段在 2D 方块中的位置,这样它就可以获取游戏的相应活跃度值。

为此,只需将 2D 顶点坐标传递到光栅化器中即可,这意味着片段着色器将接收像素本身的插值 2D 位置:

          ti.outputVertex(v);

片段着色器的代码如下:

        for (let f of ti.inputFragments()) {
            let coord = (f + 1) / 2.0;
            let cellIndex = ti.i32(coord * (liveness.dimensions - 1));
            let live = ti.f32(liveness[cellIndex]);
            ti.outputColor(renderTarget, [live, live, live, 1.0]);
        }

值 f 是从顶点着色器传递过来的插值像素位置。使用此值,片段着色器将查找游戏中覆盖此像素的单元格的活跃状态。首先将像素坐标 f 转换为 [0, 0] ~ [1, 1] 范围,并将此坐标存储到 coord 变量中。然后将其乘以活跃字段的尺寸,得出覆盖单元格的索引。

最后,我们获取此单元格的活跃值,如果单元格已死亡,则为 0,如果单元格还活着,则为 1。它将此像素的 RGBA 值输出到 renderTarget 上,其中 R、G、B 分量都等于活跃,A 分量等于 1,表示完全不透明。

定义渲染管道后,剩下的就是通过每帧调用模拟内核和渲染管道将所有内容放在一起:

    async function frame() {
        countNeighbors()
        updateLiveness()
        await render();
        requestAnimationFrame(frame);
    }
    await frame();

就这样!我们在 taichi.js 中完成了基于 WebGPU 的“生命游戏”实现。

如果运行该程序,您应该会看到以下动画,其中 128x128 个细胞进化了大约 1,400 代,然后融合为几种稳定的生物。


原文链接:Taichi.js 实战 WebGPU - BimAnt

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

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

相关文章

Linux驱动开发—创建总线,创建属性文件

文章目录 1.什么是BUS&#xff1f;1.1总线的主要概念1.2总线的操作1.3总线的实现 2.创建总线关键结构体解析2.1注册总线到系统2.2 struct bus_type *bus 解析 3.实验结果分析1. devices 目录2. drivers 目录3. drivers_autoprobe 文件4. drivers_probe 文件5. uevent 文件 4.在…

相机掉帧采集速度慢怎么办巨型帧9014

巨型帧需要改到9014 巨型帧 的英文 Jumbo Frame

【leetcode】位运算专题

文章目录 1. 位1的个数2. 汉明距离3. 判定字符是否唯一4. 丢失的数字5. 两整数之和6. 只出现一次的数字Ⅱ7. 只出现一次的数字Ⅲ8. 消失的两个数字 首先我们先来复习一下几种位运算 & 与&#xff1a;都为1&#xff0c;才为1| 或&#xff1a; 有1则1^ 异或&#xff1a;相同…

神经网络——优化器

1.优化器介绍&#xff1a; 优化器集中在torch.optim中。 Constructing it optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9) optimizer optim.Adam([var1, var2], lr0.0001)Taking an optimization step for input, target in dataset:optimizer.zero_grad(…

Linux 基础技术介绍

Linux 是最著名和最常用的开源操作系统。作为一种操作系统&#xff0c;Linux 是位于计算机上所有其他软件之下的软件&#xff0c;接收来自这些程序的请求并将这些请求转发到计算机的硬件。 图1 Linux的发行版之一 Ubuntu 23.04 在许多方面&#xff0c;Linux 与您之前可能使用过…

超详细Git的基本命令使用(三)

&#x1f600;前言 本篇博文是关于Git的基本命令使用&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是我的动力&#x1f60…

【第51课】前后台功能点文件下载文件读取文件删除目录遍历目录穿越

免责声明 本文发布的工具和脚本&#xff0c;仅用作测试和学习研究&#xff0c;禁止用于商业用途&#xff0c;不能保证其合法性&#xff0c;准确性&#xff0c;完整性和有效性&#xff0c;请根据情况自行判断。 如果任何单位或个人认为该项目的脚本可能涉嫌侵犯其权利&#xff0…

(C语言) stdlib 程序终止

文章目录 &#x1f4a3;前言&#x1f4a3;程序终止&#x1f9e8;EXIT_SUCCESS & EXIT_FAILURE&#x1f9e8;_Exit (C99)&#x1f9e8;exit & atexit&#x1f9e8;&#x1f9e8;exit&#x1f9e8;&#x1f9e8;atexit &#x1f9e8;quick_exit & at_quick_exit (C11…

config.h-config.cpp详解

config.h定义四种组合方式切换“ET LT” listenfd触发模式 ET LT connfd触发模式 ET LT LT是电平触发、ET是边缘触发。 level-triggered VS edge-triggered 电平触发&#xff1a;只要有就能触发。 边缘触发&#xff1a;从无到有才能触发。 以socket为例 可读&#xff1a;有数据…

Node 缓存、安全与鉴权

Node 缓存、安全与鉴权 1、Cookie1.1 Set-Cookie1.2 Cookie 的生命周期1.3 如何保证Cookie安全性1.4 Cookie 的作用域Domain 属性Path 属性 1.5 SameSite attribute1.6 JS操作Cookie1.7 安全性 2、 Node缓存2.1 缓存作用2.2 缓存类型强制缓存对比缓存&#xff08;协商缓存&…

ET6框架(三)前后端通讯分析

文章目录 一、信息的通讯二、网络通讯协议的“理像模型”三、网络通讯协议的“四层模型”四、什么是 Socket&#xff1f;五、Socket通讯流程 一、信息的通讯 网络消息的发送类似于邮寄信件的流程&#xff0c;需要一个地址及收件人。 在网络通讯中通常我们需要一个IP地址及端口…

P2709 小B的询问

*原题链接* 非常简单的莫队板子题&#xff0c;让我们求出区间[l,r]中每个数出现次数的平方和&#xff0c;设枚举到,原来答案是res&#xff0c;如果加上后&#xff0c;则原来的变为&#xff0c;即res相比原来加上&#xff0c;删除同理。知道如何维护一个数的添加和删除后&#…

录屏软件合集【收藏版】

嘎嘎好用 为了提高办公效率&#xff0c;满足办公需求&#xff0c;我已经整理到下面了↓↓↓想要的可以自拿喔&#xff01;自行领取吧

大模型本地化部署2-Docker部署MaxKB

大模型本地化部署2-Docker部署MaxKB 0、MaxKB简介1、安装docker2、在docker中拉取MaxKB镜像3、运行镜像4、访问MaxKB5、创建应用6、使用应用进行对话 0、MaxKB简介 MaxKB是一款基于LLM大预言模型的知识库问答系统。具有以下特点&#xff1a; 多模型支持&#xff1a;支持对接主…

Qt 调用执行 Python 函数

一.环境 Qt 5.15.2 python-3.12.5 二.安装 1.安装python-3.12.5.exe 三.配置 1.设置环境变量 2.设置Qt 编译环境 3.新建Python文件 4.运行 四.源码 1.修改pro文件 2.testPy.py 注意: .py文件需要拷贝到build目录下 def myPrint(string):print(string)def ad…

抖音ip会莫名其妙变成北京吗

‌‌抖音IP会莫名其妙变成北京吗&#xff1f;抖音的IP地址可能会莫名其妙变成‌北京‌&#xff0c;这通常是由于多种原因导致的&#xff0c;包括但不限于网络连接、用户使用的网络服务提供商等问题。以下是一些可能导致这种情况发生的原因和解决方法。 原因分析&#xff1a; 网…

mysql学习下

1&#xff1a;添加数据 1.1为表中所有字段添加数据 1.1.1NSERT 语句中指定所有字段名 语法&#xff1a;INSERT INTO 表名(字段名1&#xff0c;字段名2&#xff0c;…)VALUES(值1&#xff0c;值2&#xff0c;…); 例题&#xff1a;向student表中插⼊&#xff08;id为1&#…

src-登陆框的常见测试思路

常见的登陆形式 第三方平台 OAuth 认证 用户名 密码 手机号 短信验证码 邮箱 邮件验证码 登陆框的常见测试思路 弱口令 弱口令指的是人为设定、复杂度较低的密码口令 为系统账户&#xff08;尤其是管理员账户&#xff09;设置弱口令会使得整个系统的身份认证模块…

graalvm jenkins maven 配置

1. maven 使用指定jdk编译 设置 JAVA_HOME环境变量: linux: linux: export JAVA_HOME/data/java/graalvm-jdk-22.0.29.1window: set JAVA_HOMED:\develop\Java\graalvm-jdk-22.0.29.1 2.mvn编译报错 问题 : Unable to make field private final java.util.Comparator java.…

波束搜索算法图解【Beam Search】

许多 NLP 应用程序&#xff08;例如机器翻译、聊天机器人、文本摘要和语言模型&#xff09;都会生成一些文本作为其输出。此外&#xff0c;图像字幕或自动语音识别&#xff08;即语音转文本&#xff09;等应用程序也会输出文本&#xff0c;即使它们可能不被视为纯 NLP 应用程序…