编程语言
首页 > 编程语言> > 使用taichi.js进行WebGPU编程

使用taichi.js进行WebGPU编程

作者:互联网

模拟

让我们使用taichi.js深入了解生命游戏的实现。首先,我们在速记ti下导入taichi.js库,并定义一个async main()函数,该函数将包含我们所有的逻辑。在main()我们从调用ti.init()开始,它初始化库及其WebGPU上下文。

从“path/to/taichi.js”导入*作为ti

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

主()

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

    让 N = 128;

    让活力 = ti.field(ti.i32, [N, N])
    let numNeighbors = ti.field(ti.i32, [N, N])

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

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

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

    let init = ti.kernel(() => {
        对于(让I of ti.ndrange(N,N)){
活力[I] = 0
            让f = ti.random()
            如果 (f < 0.2) {
活力[I] = 1
}
}
})
    init()

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

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

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

    let countNeighbors = ti.kernel(() => {
        对于(让I of ti.ndrange(N,N)){
            让邻居 = 0
            对于(让ti.ndrange的三角洲(3,3)){
                let J = (I + 增量 - 1) % N
                如果((J.x!= I.x || J.y != I.y) && liveness[J] == 1) {
邻居 = 邻居 + 1;
}
}
numNeighbors[I] = 邻居
}
});
    let updateLiveness = ti.kernel(() => {
        对于(让I of ti.ndrange(N,N)){
            让邻居 = num邻居[I]
            如果(活力[I] == 1){
                如果(邻居<2 ||邻居>3){
活力[I] = 0;
}
}
            其他{
                如果(邻居== 3){
活力[I] = 1;
}
}
}
})

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

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

在计算了每个单元格的邻居数量后,我们在updateLiveness()内核中更新了它们的活力状态。这是一个简单的问题,即读取每个单元格的活力状态及其当前活邻居的数量,并根据游戏规则回写一个新的活力值。像往常一样,这个过程并行适用于所有细胞。

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

透视图

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],
]);

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

ti.addToKernelScope({ vertices, renderTarget });

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

    let render = ti.kernel(() => {
ti.clearColor(renderTarget,[0.0,0.0,0.0,1.0]);
        对于(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(活力[texelIndex]);
ti.outputColor(renderTarget,[直播,直播,直播,1.0]);
}
});

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

顶点着色器有两个责任:

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 cellIndex = ti.i32(coord *(liveness.dimensions - 1);
            let live = ti.f32(liveness[cellIndex]);
ti.outputColor(renderTarget,[直播,直播,直播,1.0]);
}

f是从顶点着色器传递的插值像素位置。使用此值,片段着色器将在覆盖此像素的游戏中查找细胞的活跃状态。这是通过首先将像素坐标f转换为[0, 0] ~ [1, 1]范围,并将此坐标存储到coord变量中来完成的。然后,这乘以liveness场的尺寸,产生覆盖单元格的指数。

最后,我们获取此单元格的live值,如果它死了,则为0如果它活着,则为1。它将此像素的RGBA值输出到renderTarget,其中R、G、B组件都等于live,A组件等于1,以获得完全不透明度。

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

    异步函数frame() {
        countNeighbors()
        更新活力()
        等待渲染();
        requestAnimationFrame(框架);
}
    等待框架();

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

如果您运行该程序,您应该会看到以下动画,其中128x128细胞进化了大约1400代,然后汇聚成几种稳定的生物体。

 

标签:WebGPU, JavaScript, GPU
来源: