蓝紫™
DDZEB 完全自主知识产权图形图像开发平台
开放源码 免费使用 在线运行
开发文档 compute
基于 WebGPU 的 Compute Shader 渲染引擎。
文件语言大小版本开发者最后更新
lands/compute productionJavaScript12.9KBv1.0.0YB2024-10-16查看
lands/compute source codeJavaScript27.0KBv1.0.0YB2024-10-16查看
compute 引擎 WGSL 开发库
utilWGSL4.3KBv1.0.0YB2024-10-16查看
cmpxWGSL16.1KBv1.0.0YB2024-10-16查看
quatWGSL3.1KBv1.0.0YB2024-10-16查看
hashWGSL9.9KBv1.0.0YB2024-10-16查看
sdfWGSL23.6KBv1.0.0YB2024-10-16查看
colorWGSL11.0KBv1.0.0YB2024-10-16查看
rayWGSL2.8KBv1.0.0YB2024-10-16查看
flameWGSL6.5KBv1.0.0YB2024-10-16查看
textureWGSL8.0KBv1.0.0YB2024-10-16查看

WebGPU 是 WebGL 的继任技术,主要的优势在于其计算管线、缓冲的控制以及可以对输出纹理的随机写入。但另一方面,WGSL 真的很糟糕,以我们的经验,如果可以使用 GLSL,就尽量别碰 WGSL。

蓝紫的 compute 渲染引擎与 frag 引擎同样属于二维渲染范式,也就是对平面上的点进行着色。compute 整体上是采用双缓冲架构,一个是当前屏幕纹理(只读),一个是渲染输出纹理(只写)。当然,我们进行渲染的时候也可以不用考虑当前屏幕数据,直接计算出颜色写入输出缓冲即可。

WebGPU 光线追踪
compute 引擎示例:光线追踪
一、JS 接口
const canvas = await Lan.compute(1800, 1200, {
  replaces : {from:'to'},
  initfill : new Float32Array(...),
  interval : 0,
  storages : [100 * 200 * 4, new Uint32Array([10, 20]), ...],
  textures : ['url', {source:'url', flipy:false}, ...],
  painters : 'P1,P2'
});
await Lan.loop(canvas.render);

initfill 用于初始化画布数据,指定一个 Float32Array,每 4 个元素代表一个像素的 RGBA 分量。其它参数请参考本文档后续部分。

1.1、async canvas.render(loop = {})

该接口为渲染接口,通常我们会调用 Lan.loop(canvas.render) 来进行持续的动画渲染,当然,我们也可以直接调用 canvas.render 来渲染某一帧画面。loop 参数与 Lan.loop 需要的参数一致,同样是一个 JSON 带有如下属性:

当我们没有传递 loop 参数的时候,frag 会自动生成一个属于自己的 loop 参数并在每次渲染的时候更新参数内容。

1.2、async canvas.storage

使用该接口可以读取或更新存储缓冲,读取的时候调用方式为:

const index = 0;
canvas.storage(index, (data) => {
  // data is of type Uint32Array
  // do anything with data ...
  // return true if you want to write data back to GPU
});

当回调函数返回 true 的时候,data 会被写回 GPU 中从而更新缓冲数据。如果我们想直接更新数据,也可以通过以下更加高效的方式更新 storage 的内容:

const index = 0;
const storageOffset = 10;
canvas.storage(index, new Uint32Array([123]), storageOffset);
canvas.storage(index, new Uint32Array([456]));
二、计算管线

我们的所有渲染工作都是通过计算管线完成的,编写计算管线程序就是我们的开发重点。为此,蓝紫提供了强有力的基础,包括全局变量以及尽量简化的计算函数语法等。

2.1、输入输出纹理
// input and output textures
@binding var TI : texture_2d<f32>;
@binding var TO : texture_storage_2d<rgba32float, write>;

其中,TO 为只写的输出纹理,我们计算出颜色数据之后,都要输出到这里。而 TI 则是只读的输入纹理,即:上一帧渲染得到的数据。通常我们不会直接跟它们打交道,而是通过专用的输入输出函数(参考本文第 2.5 节)。

2.2、uniform 参数2.3、全局常量变量2.4、全局函数2.5、类型别名2.6、@run 函数

我们可以编写多个 @run 计算函数,它们会按照声明顺序执行。一个 @run 函数实际上就是执行一个二维计算

@run(RUN_WIDTH, RUN_HEIGHT) {
  // do something ...
}

其中,(RUN_WIDTH, RUN_HEIGHT) 规定二维平面区域大小,如果只提供了 RUN_WIDTHRUN_HEIGHT 默认为 1,如果两者都忽略则采用画布宽高。比如:

@run {
  // do something ...
}

这样就可以在整个画布范围内进行计算,我们可以通过全局变量 COORDUV 等获取当前计算坐标,然后进行计算后将结果写入到输出纹理。WebGPU 允许我们写入到输出纹理的任意位置,而不一定必须写入当前计算点,这是与 frag 引擎巨大的区别所在。计算函数的调度通常是每一帧都按顺序调度一次,除非声明了 @interval

@run @interval {
  // do something
}

对于某些周期性执行的函数可以添加上 @interval 标识,该函数就会按照 options.interval 指定的屏幕输出周期执行。当然,也可以重载这个数字:

@run @interval(100) {
  // do something
}

这时候,@run 函数就会按照每 100 帧执行一次的频率进行调度。另外还有一个特殊的 @interval

@run @interval(once) {
  // do something
}

我们使用 once 来告诉渲染引擎 @run 函数只调用一次。示例可以参考 图像处理计算 程序。

最后一个关于 @run 函数的事情是,您可以为每一段 @run 函数提供一个注释性质的名字或描述:

@run update particles {
  // do something
}

示例参见 粒子系统

三、存储缓冲

在 options 里通过 storages 指定存储缓冲,可以指定一个,也可以是多个:

{
  // 一个存储缓冲,大小为 4 个 u32,即 16 字节
  storages : 4,
}

{
  // 两个存储缓冲,一个 16 字节,第二个 40 字节
  storages : [4, 10],
}

{
  // 一个存储缓冲,两个 u32 数据,直接上传到 GPU
  storages : new Uint32Array([1e20, 0]),
}
  

整体来说,存储缓冲是一个 u32 数组,u32 可以通过 WGSL 的 bitcast 函数转换为 i32f32 等类型。所有的缓冲都会被声明为 array<atomic<u32>> 类型,并被命名为 storagex,其中 x 为序号。

let index = 32u;
atomicAdd(&storage0[index], 1);

let val:f32 = 1.41;
atomicStore(&storage0[index], bitcast<u32>(val));
  

此外,我们还可以通过 storagexLen 常量来取得存储缓冲的长度。

四、纹理资源

加载纹理与 frag 引擎类似,需要在 JS 接口的 textures 数组指定纹理 url 或者一个 HTMLCanvasElement 对象作为纹理源。我们可以将音频、视频以及 Cubemap 资源加载为纹理,所有纹理加载进来后都是 texture_2d_array<f32> 结构。音频 FFT 频率分布数据会被转换为一个高度为 1 的二维图像进入程序,Cubemap 是一个 6 层的 texture_2d_array 数组。纹理命名与缓冲命名类似,texturex,其中 x 为序号。

var c = textureLoad(texture0, vec2u(id.x, id.y), 0, 0).rgb;

因为 textureLoad 在采样的时候没有使用任何 filter,我们在 base 附加库中提供了几个采样函数,可以方便我们进行二维纹理采用以及 HDRI、Cubemap 采样。

var c0 = texture(texture0, uv);
var c1 = texHdri(texture1, dir);
var c2 = texCube(texture2, dir);

其中,缺省采用 cubic B-spline 采样,如果要修改采样算法,请使用带 Filter 的函数。它们的具体使用方法请参考示例程序:WebGPU HDRI 以及 WebGPU Cubemap

除了加载常规的图片、视频等纹理外,当 source 设置为 'camera' 时,我们会试着打开设备的照相机:

textures : 'camera'
textures : {source:'camera'}

我们还可以将一个 HTMLCanvasElement 作为纹理源,当 live 为 true 时,会实时更新,具体情况请参考示例程序:Canvas Texture

textures : {source:canvas_element, live:true}
五、颜色桶

蓝紫内置资源中有颜色桶支持,我们可以在 options 中指定颜色桶(painters),然后通过 color 附加库中的 painter 函数获取颜色。使用代码请参考官方 Demo:WebGPU Painters

六、屏幕输出周期

有些时候我们希望程序可以尽可能快的进行计算和渲染,间隔一段时间后输出到屏幕。我们可以指定 options.interval 来设置输出到屏幕的帧周期。比如,设置为 100,那么就会每隔 100 帧输出到屏幕。示例参见 Flame 渲染

七、字符替换

options 里指定 replaces 项,可在 WGSL 代码中进行字符串替换操作。

{
  replaces : {NUMBER1:1000},
}

通过这样的设定,WGSL 代码中所有的 NUMBER1 都将被替换为 1000。使用例子请参考向导程序:二维 Mandelbrot 集合

八、自定义 uniforms

通过字符替换,我们可以将 JS 中的值“代入”到 Shader 代码中,这样的好处是快速,Shader 会将那些代入的值作为常量。但这样就无法在程序运行过程中将 JS 中计算得到的数据传入 Shader 程序中。我们提供的 uniforms 就可以解决这类问题。通过在 options 声明:

{
  uniforms : { UniformName : InitialValue, ... },
}

这样我们就可以在 Shader 程序中使用 UniformName 了。要在 JS 中更新,只需要这样:

canvas.uniforms.UniformName = new_value;

需要注意的是,UniformName 必须以大写字母开头,并且目前只提供 f32 类型的 uniform 变量支持。详细使用例子,请参考向导程序:光线追踪。另外,关于鼠标交互,可以参考官方 Demo:WebGPU Mouse Interaction