文件 | 语言 | 大小 | 版本 | 开发者 | 最后更新 | |
lands/compute production | JavaScript | 12.9KB | 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.14159265358979323846
const f32
,PI / 2;const f32
,PI / 4;const f32
,PI * 2;const u32
,0xffffffff
const 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 的函数。它们的具体使用方法请参考示例程序: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 集合。
通过字符替换,我们可以将 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。