你已经会画一个形状了,这一章解决「把它放到想要的位置、转到想要的角度、复制成想要的数量」。Shader 里做这些事的方式和常规图形 API 相反——理解了这个「反」字,变换就再也不会写错方向。
1动的是坐标,不是形状
在 Canvas / Compose 里,你移动的是画笔:canvas.translate(100, 0) 之后画的东西整体右移。Fragment Shader 里没有画笔——形状是坐标的函数,你唯一能动的是喂给函数的坐标。要让形状右移 0.3,就把每个像素的坐标左移 0.3 再去查函数:
// 想让形状右移 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 旋转矩阵。不用怕,它只有四个元素,而且一辈子就这一个写法:
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 章埋的伏笔现在收割:fract 与 floor 这对兄弟,一个给出格子内坐标,一个给出格子编号。把它们套在居中坐标上,任何形状立刻变成无限阵列,而且每个格子可以借编号获得自己的「个性」:
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极坐标:旋转对称的世界
花瓣、齿轮、雷达、万花筒——一切「绕着中心重复」的图案,在直角坐标系里都难写,换到极坐标就变成了普通条纹。转换只要两行:
float r = length(p); // 到中心的距离
float a = atan(p.y, p.x); // 角度,范围 -π ~ π
// 核心思想:在 (a, r) 空间里,"绕一圈重复 k 次"就是普通的 fract/cos 条纹
float petals = cos(a * 5.0); // 绕中心震荡 5 次 → 五瓣花的骨架
GLSL 的 atan(y, x) 是双参数版(等价于 C 的 atan2),y 在前。写反了图案会沿对角线镜像,而且只在某些象限出错——极难肉眼排查,遇到「花瓣位置不对」先查这里。
5镜像:abs 的魔法
最后一个小而美的技巧:abs(p.x) 把左半平面折叠到右半平面——于是你只需要在右边画一半,左边自动镜像。心形、蝴蝶、脸,一切左右对称的图形都只用画一半。嵌套折叠可以做出万花筒:
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折叠空间:画一半得对称,折角度得万花筒。
动手练习
- 画一个钟表盘:12 个刻度(极坐标 + fract 重复角度),一根随 uTime 旋转的指针(sdSegment + rot)。
- 把圆点阵 Demo 改造成「棋盘格」:用
mod(id.x + id.y, 2.0)让相邻格子交替显示圆和方块。 - 用折叠对称画一个心形:
p.x = abs(p.x)之后,右半边用一个斜着的圆+三角形拼(不满意就搜「heart SDF」对照,iq 有精确解)。