蓝紫™
DDZEB 完全自主知识产权图形图像开发平台
开放源码 免费使用 在线运行
基于迭代函数系统(IFS)渲染 Flame 分形。
文件语言大小版本开发者最后更新
lands/flame productionJavaScript7.9KBv1.0.0YB2024-11-07查看
lands/flame source codeJavaScript19.5KBv1.0.0YB2024-11-07查看
一、简要介绍 二、初始化三、JS 接口四、Shader 程序五、VS 接口六、FS 接口一、简要介绍

Flame 算法是一种非常重要的分形艺术渲染算法,著名的分形艺术软件 Apophysis 的核心算法就是 Fractal Flame Algorithm,它实际上是属于迭代函数系统(IFS)分形算法。全球有大量的 Flame 分形艺术作品,可以说已经形成了一个独特的门派。

WebGL 渲染 Flame 分形
flame 引擎示例:谢宾斯基三角形

如此重要的数字视觉渲染算法,蓝紫平台当然要提供支持。基于 WebGPU 的渲染引擎 compute 因为支持随机写操作,可以非常快速的渲染 Flame 分形。但还是有几个问题,一是 WebGPU 还未被广泛支持,二是 WGSL 语法实在让人受不了。没有 ?: 选择符,缺少函数重载机制,在调用 Variation 的时候非常麻烦。另外,毕竟 compute 引擎是一个通用的渲染引擎,不是专门为 flame 渲染而准备的。所以我们决定使用 WebGL 开发一个专门针对 Flame 渲染的引擎。

WebGL 渲染 Flame 分形
flame 引擎示例:三维变换

蓝紫 flame 引擎设计得非常简洁漂亮,我们将最繁重的工作都放到了 GPU 中进行处理,只有极少的计算是在 CPU 上完成的,这样的目的是充分发挥 GPU 的性能优势。

WebGL 渲染 Flame 分形
flame 引擎示例:景深模糊
二、初始化

在 $JS 代码段里,采用标准的蓝紫渲染引擎初始化方法进行 flame 渲染初始化:

const canvas = await Lan.flame(width, height, {options...});
Lan.loop(canvas.render);

通过全局变量 Lan 调取 flame 引擎进行初始化,参数包括画布宽高以及一些渲染选项。以下重点介绍 options 参数:

2.1、palette

配置调色板的选项,如果没有配置,我们缺省调取 P123 调色班数据。最简单的配置方法是传入调色板编号:

{palette : 'P123'}

当我们想对调色板进行一些调整的时候,就需要稍微复杂一点的配置方式:

{
  palette : {source:'P123', freq:2, tran:0.5},
}

其中,source 不言而喻,freq 指定调色板的重复频率,相当于对调色板进行拉伸或压缩,tran 参数对调色板进行偏移。

2.2、threads

Flame 渲染里最繁重的工作就是迭代,一个点经过不断迭代变换位置,从而在图像上留下印记。threads 参数控制同时有多少个点进行迭代,缺省为 200000 个。对于性能好一点的电脑,同时上百万的点进行迭代也没有问题,这样渲染的速度就会更快。

2.3、skitter

在 Flame 算法中,一个随机生成的点在迭代最初阶段不进行打点着色,skitter 参数指定需要跳过的迭代次数,缺省为 20 次。

2.4、iterate

该参数控制一个点最多进行多少次迭代,缺省为 500 次。超过迭代次数之后,重新初始化数据进行迭代。

2.5、itertry

当迭代次数大于 skitter 之后,进入真正的渲染。每次迭代产生的点可能不在视口范围内。在每次迭代过程中,我们使用 itertry 进行多次尝试,希望能得到一个视口内的点。itertry 缺省为 200 次。

2.6、produce

渲染参数,主要用于生产级输出。

三、JS 接口

初始化后,canvas 附加一些接口用于 flame 渲染。

3.1、render()

该接口用于渲染图像,我们需要循环往复的调用该接口进行迭代渲染。

const canvas = await Lan.flame(900, 600, {
  palette : 'P123',
});
Lan.loop(canvas.render);
3.2、destroy()

用于销毁我们申请的资源,在蓝紫程序里无需调用,我们会自动销毁资源。

四、Shader 程序

编写 flame 程序主要的工作就是编写 Shader 程序。一个 flame 渲染程序可以有一个 $VS 段以及一个 $FS 段,$VS 段对应 Vertex Shader,而 $FS 段则对应 Fragment Shader。Vertex Shader 负责迭代,而 Fragment Shader 负责颜色处理。在 Shader 程序中,我们可以通过 #include 指令调取蓝紫的 GLSL 标准库,比如:

$vs
#include flame, complex

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

4.1、全局接口

以下是所有 Shader 程序都具有的一些常量以及函数,相当于 Shader 基础设施。

五、VS 接口

在 $VS 代码中,我们需要实现一个 iterate 函数进行迭代。另外,我们可以选择性的实现 project 函数,它用于坐标投影。

5.1、void iterate(inout vec3 z, inout float c)

该函数有两个参数,一个是迭代变量 z,另一个是颜色变量 c,它们均为 inout 方式传参。

5.2、vec2 project(vec3 z)

作为可选项,我们可以实现一个投影函数。因为我们的迭代全部是在三维空间中进行,最后需要投影到二维屏幕上,所以需要一个投影函数,缺省的投影函数直接取 xy 坐标:

vec2 project(vec3 z) {
  return z.xy;
}

实际上,我们可以在 project 函数里做更多的工作,比如进行视口(viewport)变换或者景深模糊(DoF Blur)等。

vec2 project(vec3 z) {
  return z.xy * 2.0;
}
六、FS 接口

通常情况下,我们不需要编写 $FS 代码,除非我们想对颜色进行一些调整和控制,比如调整色调或进行 Gamma 校正。

6.1、变量及辅助函数6.2、vec4 display(vec3 rgb, float w)

这是 $FS 代码的核心函数,display 函数输出的颜色即是画布上最终呈现的颜色。我们可以根据需要实现自己的 display 函数,引擎缺省的 display 函数如下:

vec4 display(vec3 c, float w) {
  float a = logDensity(w); // calculate alpha based on pixel density
  return vec4(c * a, 1); // blend with vec4(0,0,0,1)
}

缺省情况下,我们使用 log density 算法计算 alpha 值,然后与纯黑色不透明背景进行颜色混合。