PHASE 1 · 用数学作画

1.3变换与重复

旋转、fract 网格与极坐标

你已经会画一个形状了,这一章解决「把它放到想要的位置、转到想要的角度、复制成想要的数量」。Shader 里做这些事的方式和常规图形 API 相反——理解了这个「反」字,变换就再也不会写错方向。

1动的是坐标,不是形状

在 Canvas / Compose 里,你移动的是画笔:canvas.translate(100, 0) 之后画的东西整体右移。Fragment Shader 里没有画笔——形状是坐标的函数,你唯一能动的是喂给函数的坐标。要让形状右移 0.3,就把每个像素的坐标左移 0.3 再去查函数:

GLSL · 变换都是"反着做"
// 想让形状右移 0.3 → 坐标减 0.3
float d = sdCircle(p - vec2(0.3, 0.0), 0.2);

// 想让形状放大 2 倍 → 坐标除以 2(查表前先"缩小世界")
float d2 = sdCircle(p / 2.0, 0.2) * 2.0;   // 注意距离也要乘回去,场才不失真

// 想让形状顺时针转 → 坐标逆时针转
float d3 = sdBox(rotate(-angle) * p, vec2(0.2, 0.1));

直觉解释:每个像素在问「我这里有没有形状?」。把所有人的提问坐标统一往左挪 0.3,那么原本在形状右侧 0.3 处的像素就会得到「有」的答案——形状看起来右移了。世界动的方向,永远和你对坐标做的相反。记住这句,以后所有「怎么动反了」的 bug 都能秒定位。

2平移、缩放与旋转

平移是减法,缩放是除法,旋转要请出本书第一个矩阵——2D 旋转矩阵。不用怕,它只有四个元素,而且一辈子就这一个写法:

GLSL · 旋转矩阵(建议直接背下来)
mat2 rot(float a) {
    float s = sin(a), c = cos(a);
    return mat2(c, -s, s, c);
}

// 用法:先转坐标,再查 SDF(角度为正 = 形状看起来逆时针转)
vec2 q = rot(uTime) * p;
float d = sdBox(q, vec2(0.25, 0.15));

为什么它能旋转?mat2 * vec2 把向量分解到两个新的坐标轴上,而 (c, s)(-s, c) 恰好是原坐标轴转过角度 a 之后的样子。感受一下三种变换叠加的效果:

3网格重复与格子编号

1.1 章埋的伏笔现在收割:fractfloor 这对兄弟,一个给出格子内坐标,一个给出格子编号。把它们套在居中坐标上,任何形状立刻变成无限阵列,而且每个格子可以借编号获得自己的「个性」:

GLSL · 网格重复的标准模板
float n = 6.0;                        // 每单位长度 6 个格子
vec2 cell = fract(p * n) - 0.5;       // 格子内坐标,居中到 (-0.5 ~ 0.5)
vec2 id   = floor(p * n);             // 格子编号,如 (2, -1)

// 在 cell 上画东西 = 每个格子都有;用 id 做参数 = 每个格子不一样
float d = sdCircle(cell, 0.15 + 0.1 * sin(id.x + id.y + uTime));
格子边缘的裁剪问题

格子内坐标只有 -0.5~0.5,如果你画的形状(算上发光、阴影)超出这个范围,就会被硬生生切断在格子边界上——这是网格重复最常见的穿帮。解法:形状留足边距,或者一个像素同时检查邻近 9 个格子(2.3 章水珠玻璃会用到这招)。

4极坐标:旋转对称的世界

花瓣、齿轮、雷达、万花筒——一切「绕着中心重复」的图案,在直角坐标系里都难写,换到极坐标就变成了普通条纹。转换只要两行:

GLSL · 直角坐标 → 极坐标
float r = length(p);            // 到中心的距离
float a = atan(p.y, p.x);       // 角度,范围 -π ~ π

// 核心思想:在 (a, r) 空间里,"绕一圈重复 k 次"就是普通的 fract/cos 条纹
float petals = cos(a * 5.0);    // 绕中心震荡 5 次 → 五瓣花的骨架
atan 的两个参数别写反

GLSL 的 atan(y, x) 是双参数版(等价于 C 的 atan2),y 在前。写反了图案会沿对角线镜像,而且只在某些象限出错——极难肉眼排查,遇到「花瓣位置不对」先查这里。

5镜像:abs 的魔法

最后一个小而美的技巧:abs(p.x) 把左半平面折叠到右半平面——于是你只需要在右边画一半,左边自动镜像。心形、蝴蝶、脸,一切左右对称的图形都只用画一半。嵌套折叠可以做出万花筒:

GLSL · 折叠对称
p.x = abs(p.x);                 // 左右镜像:画一半得整体
p = abs(p);                     // 四象限镜像:画 1/4 得整体

// 万花筒:把角度折叠进一个扇区,再转回直角坐标
float sector = 3.14159 * 2.0 / 6.0;             // 6 重对称
float a = mod(atan(p.y, p.x), sector);          // 折叠角度
a = abs(a - sector * 0.5);                      // 扇区内再对折
p = length(p) * vec2(cos(a), sin(a));           // 回到直角坐标

这五节的工具(反向变换、旋转矩阵、fract 网格、极坐标、折叠)是 Phase 2 效果工坊的全部坐标基础设施——到时候你会发现,所谓「高级效果」不过是这些原语的排列组合。

本章小结

  • Shader 变换的铁律:动坐标而非动形状,方向永远相反——右移形状 = 坐标减;放大形状 = 坐标除。
  • 旋转就一个 mat2(c, -s, s, c),先平移后旋转 = 自转,先旋转后平移 = 公转。
  • 网格模板:cell = fract(p*n) - 0.5 管格子内,id = floor(p*n) 管个性;小心形状越界被切。
  • 极坐标 (length, atan) 把"绕圈重复"变成普通条纹;atan 是 (y, x) 顺序。
  • abs 折叠空间:画一半得对称,折角度得万花筒。

动手练习

  1. 画一个钟表盘:12 个刻度(极坐标 + fract 重复角度),一根随 uTime 旋转的指针(sdSegment + rot)。
  2. 把圆点阵 Demo 改造成「棋盘格」:用 mod(id.x + id.y, 2.0) 让相邻格子交替显示圆和方块。
  3. 用折叠对称画一个心形:p.x = abs(p.x) 之后,右半边用一个斜着的圆+三角形拼(不满意就搜「heart SDF」对照,iq 有精确解)。