| 文件 | 语言 | 大小 | 版本 | 开发者 | 最后更新 | |
| lands/compute production | JavaScript | 13.0KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| lands/compute source code | JavaScript | 27.0KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| compute 引擎 WGSL 开发库 | ||||||
| util | WGSL | 4.3KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| cmpx | WGSL | 16.1KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| quat | WGSL | 3.1KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| hash | WGSL | 9.9KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| sdf | WGSL | 23.6KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| color | WGSL | 11.0KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| ray | WGSL | 2.8KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| flame | WGSL | 6.5KB | v1.0.0 | YB | 2024-10-16 | 查看 |
| texture | WGSL | 8.0KB | v1.0.0 | YB | 2024-10-16 | 查看 |
const canvas = await Lan.compute(900, 600, {options...}); // canvas width, height and rendering options
const canvas = await Lan.compute(900, {options...}); // auto set canvas height with aspect ratio 3/2
const canvas = await Lan.compute({options...}); // canvas dimensions default to 900 x 600因为 compute 是蓝紫的核心渲染引擎之一,可以直接通过全局变量 Lan 进行访问,请参考示例程序:Mandelbrot。
import {compute} from "https://ddzeb.com/lands/compute";
await compute(canvas, {progcode:'shader source', ...});
canvas.render();const {compute} = await import("https://ddzeb.com/lands/compute");
await compute(canvas, {progcode:'shader source', ...});
canvas.render();WebGPU 是 WebGL 的继任技术,主要的优势在于其计算管线、缓冲的控制以及可以对输出纹理的随机写入。但另一方面,WGSL 真的很糟糕,以我们的经验,如果可以使用 GLSL,就尽量别碰 WGSL。
蓝紫的 compute 渲染引擎与 frag 引擎同样属于二维渲染范式,也就是对平面上的点进行着色。compute 整体上是采用双缓冲架构,一个是当前屏幕纹理(只读),一个是渲染输出纹理(只写)。当然,我们进行渲染的时候也可以不用考虑当前屏幕数据,直接计算出颜色写入输出缓冲即可。

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 分量。其它参数请参考本文档后续部分。
该接口为渲染接口,通常我们会调用 Lan.loop(canvas.render) 来进行持续的动画渲染,当然,我们也可以直接调用 canvas.render 来渲染某一帧画面。loop 参数与 Lan.loop 需要的参数一致,同样是一个 JSON 带有如下属性:
loop 开始到现在经过的秒数(float),去掉了程序暂停时间;loop 开始到现在的帧数,从 0 开始计数。当我们没有传递 loop 参数的时候,frag 会自动生成一个属于自己的 loop 参数并在每次渲染的时候更新参数内容。
使用该接口可以读取或更新存储缓冲,读取的时候调用方式为:
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]));我们的所有渲染工作都是通过计算管线完成的,编写计算管线程序就是我们的开发重点。为此,蓝紫提供了强有力的基础,包括全局变量以及尽量简化的计算函数语法等。
// input and output textures
@binding var TI : texture_2d<f32>;
@binding var TO : texture_storage_2d<rgba32float, write>;
其中,TO 为只写的输出纹理,我们计算出颜色数据之后,都要输出到这里。而 TI 则是只读的输入纹理,即:上一帧渲染得到的数据。通常我们不会直接跟它们打交道,而是通过专用的输入输出函数(参考本文第 2.5 节)。
f32,从程序运行起经过的秒数;i32,帧编号(计数),从 0 开始;const f32,3.14159265358979323846const f32,PI / 2;const f32,PI / 4;const f32,PI * 2;const u32,0xffffffffconst f32,图像宽高比(DX / DY);const i32,图像宽度;const i32,图像高度;const vec2f,图像宽度及高度;const i32,图像宽度的一半,即 X 中心位置;const i32,图像高度的一半,即 Y 中心位置;const vec2,图像宽度及高度的一半,即 XY 中心位置;vec2i,调度尺寸;vec2i,当前点像素坐标;vec2f,当前点的坐标(像素坐标),其值与 COORD 相等;[0, 1) 之间的均匀分布随机数;COORD.y * GROUP.x + COORD.x;[0, 1] 范围内,不考虑 ASPECT,图像中心点为 [0.5, 0.5];X 方向归一化到 [-0.5, +0.5],即宽度为 1,Y 方向按 ASPECT 适配,图像中心点为 [0, 0];Y 方向归一化到 [-0.5, +0.5],即高度为 1,X 方向按 ASPECT 适配,图像中心点为 [0, 0];X 方向归一化到 [-1.0, +1.0],即宽度为 2,Y 方向按 ASPECT 适配,图像中心点为 [0, 0];Y 方向归一化到 [-1.0, +1.0],即高度为 2,X 方向按 ASPECT 适配,图像中心点为 [0, 0];p 点的数据;c 写入到输出纹理(即下一帧)的 p 点;p 点,将输入纹理颜色加上颜色 c 后写入到输出纹理;c 写入到输出纹理;c 后写入到输出纹理,alpha 加 1,代表累加次数,在显示的时候实际渲染的颜色为 vec4f(color.rgb / color.w, 1),也就是用 color.w 对 rgb 颜色进行归一化处理。我们可以编写多个 @run 计算函数,它们会按照声明顺序执行。一个 @run 函数实际上就是执行一个二维计算:
@run(RUN_WIDTH, RUN_HEIGHT) {
// do something ...
}其中,(RUN_WIDTH, RUN_HEIGHT) 规定二维平面区域大小,如果只提供了 RUN_WIDTH 则 RUN_HEIGHT 默认为 1,如果两者都忽略则采用画布宽高。比如:
@run {
// do something ...
}这样就可以在整个画布范围内进行计算,我们可以通过全局变量 COORD、UV 等获取当前计算坐标,然后进行计算后将结果写入到输出纹理。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 函数转换为 i32 或 f32 等类型。所有的缓冲都会被声明为 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 的函数。
除了加载常规的图片、视频等纹理外,当 source 设置为 'camera' 时,我们会试着打开设备的照相机:
textures : 'camera'
textures : {source:'camera'}我们还可以将一个 HTMLCanvasElement 作为纹理源,当 live 为 true 时,会实时更新。
textures : {source:canvas_element, live:true}蓝紫内置资源中有颜色桶支持,我们可以在 options 中指定颜色桶(painters),然后通过 color 附加库中的 painter 函数获取颜色。
有些时候我们希望程序可以尽可能快的进行计算和渲染,间隔一段时间后输出到屏幕。我们可以指定 options.interval 来设置输出到屏幕的帧周期。比如,设置为 100,那么就会每隔 100 帧输出到屏幕。示例参见 Flame 渲染。
在 options 里指定 replaces 项,可在 WGSL 代码中进行字符串替换操作。
{
replaces : {NUMBER1:1000},
}通过这样的设定,WGSL 代码中所有的 NUMBER1 都将被替换为 1000。使用例子请参考示例程序:二维 Mandelbrot 集合。
通过字符替换,我们可以将 JS 中的值“代入”到 Shader 代码中,这样的好处是快速,Shader 会将那些代入的值作为常量。但这样就无法在程序运行过程中将 JS 中计算得到的数据传入 Shader 程序中。我们提供的 uniforms 就可以解决这类问题。通过在 options 声明:
{
uniforms : { UniformName : InitialValue, ... },
}这样我们就可以在 Shader 程序中使用 UniformName 了。要在 JS 中更新,只需要这样:
canvas.uniforms.UniformName = new_value;需要注意的是,UniformName 必须以大写字母开头,并且目前只提供 f32 类型的 uniform 变量支持。详细使用例子,请参考示例程序:光线追踪。