# 自带示例 这个目录里的东西**随插件分发**,不依赖运行期工作区 —— 换机器、重装 profile、 或者把「存储位置」改到别处,示例都还在。 ## 怎么载入 面板 → 资源树 → **载入示例**(工作区还空着时,空状态里也有一个入口)。 已经存在同名项目时会问一句要不要覆盖;接口是 `{"action":"example.load","name":"demo"}`(`overwrite: true` 才覆盖)。 载入就是把这里的整个目录**拷进** `<工作区>/projects/<名字>/`, 形状与工作区完全一致,所以不会把示例里调好的参数、清单和预览图弄丢。 ## 示例里没有"黑盒节点" 这几个效果(傅里叶级数 / 频谱 / 本轮 / IFS 隧道)走过一段弯路,值得记下来: 1. 最早它们是**内置专用节点**(`ifsTunnel` / `fourierSeries` / `epicycles`)—— 循环写在节点的 `glslPrelude` 里。问题是:用不上的人也得背着这段源码, 而且在画布上**展不开、改不了**,就是一颗黑盒。 2. 后来改成**示例自带的用户节点**(`examples/<名>/library/nodes/*.json`)—— 不用改插件源码了,但**还是黑盒**,还是展不开。 3. 现在的做法:**把循环展开成节点链**,效果就是一张普通的图。 | 示例 | 画布节点 | 原来那个黑盒现在长什么样 | |---|---|---| | `fourier-series` | 10 | 每个奇次谐波一个 `custom` 节点,链式累加 —— **想加项就再加一个节点** | | `fourier-spectrum` | 12 | 每根柱一个节点(`max(累加, 本柱)`) | | `fourier-epicycles` | 32 + BufferA 17 | 每个旋转向量一个节点;**轨迹靠 BufferA 反馈**(笔尖走过的印子逐帧衰减) | | `demo` | 10 | 原版照搬:99 步累积封成一个**重复组**(循环体只写一遍),逐像素一致 | 两个代价说清楚: - **`terms` 滑块没了**:项数现在是图上节点的个数。想 20 项就复制节点 —— 比改一个数字麻烦,但它是看得见、改得动的,这正是换来的东西。 - **循环用「重复次数」,别复制节点**。IFS 隧道一度是 99 份循环体复制: 801 个节点 / 676 KB 着色器 / 2 MB 图 / codegen 352 ms,面板一打开就卡; 贝塞尔的 24 段复制也要 51 KB 着色器。现在两者都是**重复组**(组的「重复次数」> 1, 循环体只写一遍):10 个节点 / 9 KB / 34 KB 图。 这是"和原版逐像素一致"和"打开不卡"之间唯一能同时成立的做法 —— 中途试过"少几步 + 放大步长"省节点,采样噪声和亮度都对不上,一比就露馅。 > 一致性是**量出来的**而不是"看着像":探针把原版 `.frag` 和展开版并排渲, > 六个采样点在两个时间点上**逐像素相同**(`center=110,121,132` / `206,227,247` …)。 > 素材在 `scripts/.ifs-compare/`。 > 生成脚本:[`scripts/rebuild-primitive-demos.mjs`](../scripts/rebuild-primitive-demos.mjs)。 > 它只允许用 `uv` / `time` / `custom` / `output` / `bufferSample` 这几种基础件, > 测试里也有对应的护栏(`examples.test.js`:不许带用户节点、不许出现非内置类型、 > 不许有 `glslPrelude`)。 > > 顺带一提,**「组(Group)」是可以展开的**:右键那颗组节点 →「拆开组」, > 子图会还原成普通节点。`gaussian-blur` / `depth-of-field` / `bezier` 这三个示例 > 就是用组把几十个节点包成一颗的,展开就能改。 ## demo — Phantom Star(IFS 隧道) [Shadertoy ttKGDt](https://www.shadertoy.com/view/ttKGDt) 的节点图移植 (原作者 kasari39,2020;累积辉光思路来自 aiekick 的 Phantom Mode)。 ``` demo/ ├── project.json └── materials/phantom-star/ ├── graph.json 节点图(uv + time → ifsTunnel → output) ├── material.json 材质清单 ├── phantom-star.frag 这个图生成的 GLSL(对照用) └── preview.png 真机跑出来的样子 ``` 图里只有 4 个节点,重活全在 `ifsTunnel` 节点的 `glslPrelude` 里: | 参数 | 默认 | 说明 | |---|---|---| | 累积步数 | 99 | 沿光线累积多少次(原作就是 99) | | 辉光衰减 | 3 | `exp(-距离 × 这个值)`,越大越"薄" | | IFS 折叠次数 | 5 | `p = abs(p) - 1` 重复几次 | | 前进速度 | 3 | 相机每秒往前多少 | | 脉冲强度 | 1 | 那条沿路程脉动的亮带 | | 曝光 | 0.01 | 整体亮度 | | 色调 | 白 | 色调乘数 | 改这几个参数就能得到同族但不同的画面(折叠次数调到 2~3 会变成另一种分形结构)。 --- ## 傅里叶变换三件套 三个各自独立的示例,从三个角度看同一件事:**任何信号都能拆成一堆正弦 / 旋转向量。** ### fourier-series — 方波 = 正弦叠加 理想方波(灰色虚线)vs 前 N 项正弦之和(青色)。调「项数」能直接看到两件事: - 项数越多越贴近方波; - **吉布斯振铃**:跳变处永远会过冲,加多少项都消不掉(只是过冲越来越窄)。 参数面板里把「显示」切到 `FS_SHOW_STACK`,会变成每行一个谐波、最上面是它们的和 —— 能看清"每一项长什么样、它们是怎么凑出方波的"。 ### fourier-spectrum — 每根柱是一次谐波 上面是这些分量重建出来的波形,下面是频谱:柱高 = 该次谐波的幅度。 方波只有奇次谐波,幅度按 `1/n` 衰减(1、1/3、1/5、1/7…), 所以"越来越矮"——这就是为什么方波在频域里是无穷多项。 参数面板里换「波形」:`FS_WAVE_SAW`(锯齿,全次 `1/n`)、 `FS_WAVE_TRI`(三角,`1/n²` 掉得快 → 几项就很像)。 ### fourier-epicycles — DFT 逆变换画出一条曲线 一串长度、转速、初相各不相同的旋转向量首尾相接,笔尖就画出目标曲线 —— 这就是离散傅里叶变换的逆变换(IDFT)在画面上的样子。 - 选**心形**:只要 **8 个**旋转向量就能精确画出它 —— 因为心形的参数方程本身就是 8 个旋转向量之和。 - 换成**五角星**:尖角让谐波衰减很慢,要 **57 个**才收敛。 把「本轮个数」从 1 往上拖,能看着它从一团乱线慢慢变成星形。 系数是对参数曲线做 DFT 烘焙进 `shared/fourier-data.js` 的 (DFT 是 O(N²),每个像素现算不可能);重烤走 `node scripts/bake-fourier-epicycles.mjs`。 > 三个示例的节点图都只有 4 个节点(`uv` + `time` → 节点 → `output`), > 重活全在节点的 `glslPrelude` 里 —— 因为生成器给每个节点写的是 > **单条表达式** `vec4 x = <表达式>;`,循环装不进去,只能以 GLSL 函数的形式 > 注入到 `main()` 之前。要加带循环的节点都走这条路(见 `docs/架构说明.md` 7.1)。 --- ## 用现有组件搭的三个效果 这三个跟上面不一样:**一个新节点都没加**,全部用现有组件搭出来,重活打进 `group` 元节点。它们也是「想加效果别改插件源码」的示范 —— 效果应该是**图**,不是代码。 | 示例 | 画布上的节点 | 组里藏了多少 | |---|---|---| | `gaussian-blur` | 8 | 30(9 抽样高斯核) | | `depth-of-field` | 11 | 30(同一个核,半径按深度算) | | `bezier` | 9 | 30(24 段采样求最小距离) | ### gaussian-blur — 9 抽样可分离核 **BufferA** 画一张硬边棋盘(带个圈),**image** 用组做 9 抽样高斯模糊。 组里每个抽样 3 个节点:`custom` 算偏移 → `bufferSample` 采源 → `custom` 加权累加。 权重是 3×3 高斯核(和为 1),半径接的是 `uRadius` 滑块 —— 拖一下就能看到 "糊"是怎么发生的。 ### depth-of-field — 半径由深度决定 同一个模糊组,只是半径不接滑块,接一个算**弥散圆**的 `custom`: `|深度 − 对焦距离| × 光圈`。 关键一步:**深度写进 BufferA 的 alpha**。没有深度缓冲的话,image Pass 只知道 屏幕坐标、不知道这个像素有多远,弥散圆根本算不出来 —— 颜色进 RGB、深度进 A、 下一 Pass 读回来,是这类效果的标准做法。 场景是平地上几块不同距离的方块(不是透视棋盘:透视花纹在掠射角必然摩尔纹, 糊出来一片竖条)。拖「对焦距离」能看着焦点前后移动。 ### bezier — 24 段采样求到曲线的最小距离 三次贝塞尔的点 = 四个控制点的加权和,所以曲线上的点可以**烤成常量系数**。 每段一个 `custom`:`min(目前最小距离, 到这一段的距离)`。 两个坑写在这儿免得重踩: 1. **要量线段,不能只量端点**。只量端点到曲线的距离,画出来是一串**虚点** (端与端之间的空隙被高估了)。线段距离的两个端点都是烤进来的常量, 所以不需要多一个节点、也不用多接一根线。 2. 像素坐标不用接线 —— `gl_FragCoord` 在 `custom` 里可以直接用, 三个输入端口(`a`=P0P1、`b`=P2P3、`c`=累加器)刚好够。 四个控制点是 `uniform`(`uP0`~`uP3`),在面板「自定义参数」里直接拖,曲线实时变; 沿曲线跑的小球用的是 `iTime`,同样不用接线。 > 生成脚本是 [`scripts/build-effect-demos.mjs`](../scripts/build-effect-demos.mjs), > 改完重跑一遍会重新生成示例 + 校验用的 `.frag`。 > 真机校验走多 Pass 探针(`scripts/.effect-demos/probe.html`): > 把 BufferA 渲进离屏纹理再喂给 image,和面板里的渲染顺序一致。