蓝紫™
DDZEB 完全自主知识产权图形图像开发平台
开放源码 免费使用 在线运行
基于 WebGL 2.0 的 Fragment Shader 渲染引擎。
文件语言大小版本开发者最后更新
lands/frag productionJavaScript11.8KBv1.0.0YB2024-10-16查看
lands/frag source codeJavaScript25.3KBv1.0.0YB2024-10-16查看
一、简要介绍

frag 是蓝紫非常重要的一种渲染引擎,它满足如下渲染范式:

color(p) = f(px, py)

也就是说,我们的渲染程序需要根据画布上每一个点的坐标给出颜色。这样的渲染模型看起来就是一个二维渲染范式,但实际上,我们可以很容易的实现三维渲染,前提是我们手动放置相机以及处理投射关系。

随机散列
frag 引擎示例:随机散列

frag 背后,使用的是 WebGL 2.0 Fragment Shader 进行渲染,

二、JS 接口

使用 frag 渲染引擎有两种方式,一种是使用蓝紫调度器进行调用,在蓝紫集成开发环境编写程序都是采用这种方式:

const canvas = await Lan.frag(width, height, {options});

另一种是手动调用 frag 引擎,这时候我们需要手动将 fragment shader 代码通过 optins.progcode 传进去:

const {frag} = await import("https://ddzeb.com/lands/frag");
await frag(canvas, {progcode:'shader code', ...});

无论采用哪种方式,在初始化画布之后,除了常规的画布接口,我们得到的 canvas 对象还具有如下几个额外接口:

2.1、canvas.render(loop = {})

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

当我们没有传递 loop 参数的时候,frag 会自动生成一个属于自己的 loop 参数并在每次渲染的时候更新参数内容。参见:Lands frag Rendering Engine,在这个程序中,我们调用 canvas.render 的时候没有传任何参数进去。

2.2、canvas.run[x]

属性。我们可以通过 run[x] 访问 @run 函数,比如:canvas.run0 访问第 0@run 函数。返回的对象有如下属性或方法:

使用示例参见 Gaussian Kernel

2.3、canvas.texture(num, data, w, h)

更新纹理数据,其中 w 以及 h 为可选参数。

2.4、canvas.outcome(num, data)

更新 @run 函数输出纹理,data 为 null 时相当于执行清除操作。

2.5、canvas.destroy()

进行销毁和清理工作,在蓝紫集成开发环境中不需要调用此函数,我们会自动调用。

三、Fragment Shader

在蓝紫集成开发环境中,编写 $FS 程序,即 Fragment Shader,实现渲染。或者在第三方集成的时候通过 options.progcode 将 Fragment Shader 代码传入。

3.1、@run 函数

$FS 程序里可以编写一个或多个按照顺序执行的 @run 函数,编号从 0 开始,它们是 Fragment Shader 的渲染函数。每一个 @run 函数渲染结束后都会将数据写入到对应的输出纹理(outcome),比如,第 0@run 函数输出到 outcome0。屏幕(画布)上显示的结果是最后一个被执行的 @run 函数的输出。

@run 函数基本形式如下:

@run {
  vec3 color = vec3(0);
  // do some calculations to get a correct color
  FLUSH(color);
}

大概意思是,在 Fragment Shader 的 @run 函数里做一些计算,然后输出颜色。我们还可以使用 @active 来决定其是否活跃,以及给 @run 提供一个注释性质的名字或描述:

@run @active(false) post process {
  // code ...
}

默认的 @active 值为 true,表示正常运行该函数。此外,当我们需要极致渲染性能的时候,可以使用 @target 告诉渲染引擎此 @run 函数的渲染目标:

@run @target(screen) this would be the LAST rendering action {
  // code ...
}

渲染目标的可能取值如下:

当没有渲染到屏幕的 @run 函数时,我们会将最后一个 @run 函数的输出渲染到屏幕。

3.2、uniform 参数3.3、全局常量变量3.4、全局函数四、扩展库

在 $FS 代码中,可通过 #include 指令调取任意 GLSL 代码库。

$fs
#include util, ray, color

多个扩展库可使用半角逗号和或空格进行分隔,也可以采用多个 #include 指令来完成。除了导入蓝紫的 GLSL 标准库,也可以导入第三方的外部库,只需要指定绝对全局地址即可。引入 GLSL 开发库的 #include 指令不一定非要写在程序开头,但不能写在函数体内。我们还可以通过如下指令一次性导入全部蓝紫 GLSL 标准库:

$fs
#include all
五、纹理加载

通过在 options 参数里指定 textures 可以加载纹理,可以有如下几种方式:

textures : 'T01,T02'
textures : {source:'T01', width:50, flipy:false, wrap:'clamp', filter:'nearest'}
textures : ['T01', {source:video, mute:false}]

缺省情况下,图片纹理都会将 y 方向进行翻转,除非设置了 flipy:false 参数。frag 内置的纹理支持只允许图片(包括 HDRI 以及 Cubemap)、Float32Array 以及 Uint8Array 三种,需要加载音频、视频的时候可以引入 media 库进行处理。比如,音频加载为纹理,可以参考示例程序:Audio Visualization

const media = (await Lan.import('media')).media();
const audio = await media.audio('A13');
const canvas = await Lan.frag(900, 600, {
  textures : [{source:audio.fft_buffer()}, 'T1']
});

对于数据纹理,当我们可以使用 Float32Array 作为数据纹理源,通常需要设置好宽度(width)或高度(height),不设置尺寸的时候,认为宽度为整个数据长度:

const data = new Float32Array([1,2,3,4,5,6]);
textures : {source:data, width:2}

这样就得到了一个 2x3 大小的数据纹理。关于数据纹理的使用,请参考示例程序:Gaussian Kernel

六、颜色桶

蓝紫内置资源中有颜色桶支持,我们可以使用 media 库加载颜色桶,使用代码请参考官方 Demo:WebGL Painters

七、字符替换

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

{
  replaces : {NUMBER1:1000},
}

通过这样的设定,GLSL 代码中所有的 NUMBER1 都将被替换为 1000。使用例子请参考向导程序:二维域翘曲渲染

八、自定义 uniforms

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

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

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

canvas.uniforms.UniformName = new_value;

需要注意的是,UniformName 必须以大写字母开头,并且目前只提供 float 类型的 uniform 变量支持。详细使用例子,请参考向导程序:光线追踪

九、输出缓冲

我们在 $FS 程序中通过 @run 函数进行渲染并输出到自己的缓冲 outcome 中,比如,第 0@run 函数输出到 outcome0。在 @run 函数渲染过程中,有可能会用到之前渲染输出的数据,我们通过 PRIOR 函数可以很方便的取得。但在 @run 函数首次运行的时候,这些数据都是空的,即,RGBA 全部为 0。我们可以通过 canvas.outcome 函数直接写入输出缓冲数据。具体例子请参见示例程序:Paint Flow

另外,当输出缓冲的数据被渲染到屏幕的时候,如果发现 alpha 值大于 1,我们会将 rgb 值除以 alpha,同时将 alpha 设置为 1,即:

if (color.w > 1) {
  color.rgb /= color.w;
  color.w = 1.0;
}

也就是说,frag 天然支持渐近渲染。如果我们在运行过程中,因为 Uniform 值产生了改变,需要重新进行渐近渲染累积,这时候可以使用 canvas.outcome 函数清除之前的累积数据:

canvas.outcome(0, null);
十、分块渲染

在渐进渲染静态图像的时候,如果渲染流程特别耗时,我们可以采用分块渲染来提高帧率,不至于让 WebGL 崩溃而无法渲染。分块渲染参数为 tilesize,通过该参数指定分块大小:

tilesize : 300 // 300 x 300
tilesize : '300x200' // 300 x 200
tilesize : '400,1' // 400 x 1

当只有一个数字的时候,我们按照正方形处理。当同时指定宽高的时候,可以使用任意非数字符号分割宽高数字。详细使用例子,请参考向导程序:路径追踪