从零玩转Shadertoy:用GLSL和SDF打造你的专属2D视觉特效
在创意编程的世界里,Shadertoy就像一座永不熄灭的霓虹灯塔,吸引着无数图形学爱好者和数字艺术家。当你第一次看到inigo quilez等大神用短短几行代码创造的惊人视觉效果时,那种震撼感难以言表——但紧接着,面对那些充满数学符号的"天书"代码,兴奋很快变成了困惑。本文将带你跨越这道认知鸿沟,从看懂大神代码到亲手创作属于自己的SDF特效。
1. 初识Shadertoy与SDF的魔力
Shadertoy本质上是一个基于WebGL的实时着色器编辑器,但它革命性地降低了图形编程的门槛。不需要复杂的开发环境配置,打开浏览器就能编写GLSL代码并即时看到渲染结果。这种即时反馈机制特别适合学习SDF(符号距离函数)这类抽象概念。
SDF的核心思想其实非常直观:
- 空间中的每个点都存储着到目标图形边界的最短距离
- 距离为正表示点在图形外部
- 距离为负表示点在图形内部
- 距离为零的点构成了图形边界
// 最基础的圆形SDF示例 float sdCircle(vec2 p, float r) { return length(p) - r; }这个简单的函数已经包含了SDF的所有精髓。在Shadertoy中,我们可以通过以下步骤可视化它:
- 创建新着色器("New Shadertoy"按钮)
- 在
void mainImage()函数中添加距离计算 - 用距离值控制像素颜色
void mainImage(out vec4 fragColor, in vec2 fragCoord) { // 标准化坐标到[-1,1]区间 vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y; // 计算SDF float d = sdCircle(uv, 0.5); // 可视化:白色表示边界,红色内部,蓝色外部 vec3 col = vec3(0.0); col = mix(col, vec3(1,0,0), smoothstep(0.0,0.01,-d)); // 内部 col = mix(col, vec3(0,0,1), smoothstep(0.0,0.01,d)); // 外部 col = mix(col, vec3(1), 1.0-smoothstep(0.0,0.01,abs(d))); // 边界 fragColor = vec4(col,1.0); }提示:在Shadertoy中按Alt+Enter可以全屏预览,Ctrl+Enter编译运行
2. 解构大神代码:SDF的常见模式与技巧
分析inigo quilez等大神的SDF实现,会发现几个反复出现的模式:
2.1 对称性优化
大多数基础图形都具有对称性,利用这一点可以大幅简化计算:
// 长方形SDF中的对称处理 float sdBox(vec2 p, vec2 b) { vec2 d = abs(p) - b; // 利用绝对值处理四个象限 return length(max(d,0.0)) + min(max(d.x,d.y),0.0); }对称性优化的关键步骤:
- 使用
abs()函数将坐标映射到第一象限 - 只计算基准区域的SDF
- 结果自动适用于所有对称区域
2.2 区域划分与投影
复杂图形的SDF通常需要划分不同区域分别处理:
// 线段SDF的区域划分逻辑 float sdSegment(vec2 p, vec2 a, vec2 b) { vec2 pa = p-a, ba = b-a; float h = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0); return length(pa - ba*h); }这个经典实现展示了如何用投影(clamp)来处理三种不同情况:
- 点在起点a之前(h=0)
- 点在终点b之后(h=1)
- 点在线段中间(0<h<1)
2.3 距离场的组合运算
SDF的强大之处在于可以进行各种布尔运算:
| 运算类型 | GLSL实现 | 可视化效果 |
|---|---|---|
| 并集 | min(d1,d2) | 两个图形的外轮廓合并 |
| 交集 | max(d1,d2) | 只保留重叠区域 |
| 差集 | max(d1,-d2) | 从第一个图形中减去第二个 |
| 圆角 | d-r | 为图形边缘添加平滑过渡 |
// 创建一个月牙形:圆形与长方形的差集 float moonSDF(vec2 p) { float circle = sdCircle(p, 0.6); float box = sdBox(p+vec2(0.3,0), vec2(0.4,0.2)); return max(circle, -box); }3. Shadertoy实战:从调试到创作的完整流程
3.1 设置调试环境
在Shadertoy中调试SDF需要一些可视化技巧:
// SDF调试显示函数 vec3 debugSDF(float d) { // 距离场等高线 float contour = fract(d*10.0); contour = smoothstep(0.1,0.15,abs(contour-0.5)); // 边界高亮 float edge = 1.0-smoothstep(0.0,0.01,abs(d)); // 组合显示 return mix( vec3(0.5), // 底色 vec3(0,1,0), // 等高线颜色 contour ) + vec3(edge); // 边界高亮 }3.2 动态交互实现
Shadertoy支持鼠标/键盘交互,让SDF动起来:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y; // 鼠标交互:控制图形位置 vec2 mouse = (2.0*iMouse.xy-iResolution.xy)/iResolution.y; if(iMouse.z<=0.0) mouse = vec2(0); // 动态SDF:随时间旋转的长方形 float angle = iTime; vec2 dir = vec2(sin(angle), cos(angle)); float d = sdBox(uv-dir*0.3, vec2(0.2,0.1)); fragColor = vec4(debugSDF(d),1.0); }3.3 进阶效果:SDF变形与动画
通过参数化控制,可以实现各种炫酷效果:
// 脉动星形效果 float pulsatingStar(vec2 p, float t) { // 基础圆形 float d = length(p) - 0.5; // 添加星形突起 for(int i=0; i<5; i++) { float angle = float(i)/5.0*6.28 + t; vec2 spikePos = vec2(cos(angle),sin(angle))*0.7; d = min(d, length(p-spikePos)-0.1); } // 脉动效果 d += sin(t*2.0)*0.05; return d; }4. 从模仿到创新:构建你的SDF工具箱
4.1 常用SDF函数库
建立自己的SDF函数库是进阶的关键:
// 2D基本图形SDF集合 float sdRing(vec2 p, float r, float thickness) { return abs(length(p)-r)-thickness/2.0; } float sdHexagon(vec2 p, float r) { vec2 q = abs(p); return max(q.x*0.866025 + q.y*0.5, q.y) - r; } float sdRoundedCross(vec2 p, float r) { p = abs(p); float d1 = length(p-vec2(r,r))-r; float d2 = min(p.x,p.y)-r; return max(d1,d2); }4.2 SDF组合技法进阶
复杂图形通过基本SDF的组合实现:
// 齿轮创建函数 float sdGear(vec2 p, float teeth, float outerR, float innerR) { float angle = atan(p.y,p.x); float sector = 6.283/teeth; // 将角度映射到一个齿距内 float modAngle = mod(angle, sector) - sector*0.5; vec2 dir = vec2(cos(modAngle),sin(modAngle)); // 创建齿形 float tooth = dot(p, dir) - outerR; float valley = length(p) - innerR; return max(valley, -tooth); }4.3 性能优化技巧
高质量SDF也需要考虑性能:
- 距离场近似:复杂图形可以用简单SDF近似
- 早期终止:当距离足够大时可提前返回
- 层次细节:根据缩放级别调整计算精度
// 优化版多圆SDF float sdMultiCircle(vec2 p, vec2[4] centers, float r) { float minD = 1000.0; // 初始大值 for(int i=0; i<4; i++) { float d = length(p-centers[i])-r; if(d < 0.0) return d; // 内部点可立即返回 minD = min(minD, d); if(minD > 10.0*r) break; // 足够远的点提前终止 } return minD; }在Shadertoy创作过程中,最令人兴奋的时刻往往是当你调整某个参数后,屏幕上突然涌现出意想不到的美丽图案。这种即时反馈的魔力,正是图形编程最大的魅力所在。记住,每个大神作品背后都是无数次的尝试和调整——现在轮到你开始这段创意之旅了。