Shadertoy实战:从零构建2D图形SDF渲染器
开启GLSL图形编程之旅
在Shadertoy这个神奇的实时着色器平台上,每个像素都能成为你创造力的画布。想象一下,只需几行代码就能让简单的数学公式转化为屏幕上跃动的视觉艺术——这正是符号距离函数(SDF)的魅力所在。对于刚接触GLSL的开发者来说,SDF就像图形编程的"乐高积木",通过基础形状的组合与变换,可以构建出令人惊叹的复杂效果。
不同于传统图形API需要处理顶点缓冲区和渲染管线,SDF让我们用纯数学函数定义图形。这种声明式的编程方式特别适合Shadertoy的即时反馈环境:每次代码修改都能在0.5秒内看到渲染更新。我们将从最基础的圆形开始,逐步构建线段、矩形等基本图形,最终组合出胶囊体等复杂形状。整个过程就像搭积木一样直观有趣,而你需要准备的只是一款现代浏览器和对图形学的热情。
1. 搭建Shadertoy开发环境
在开始编写SDF代码前,我们需要配置好Shadertoy的工作环境。访问Shadertoy官网并点击"New"按钮创建一个空白项目,你会看到默认生成的代码模板:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { // 规范化像素坐标到[0,1]范围 vec2 uv = fragCoord/iResolution.xy; // 输出纯色作为初始测试 fragColor = vec4(uv, 0.5, 1.0); }这个简单示例展示了Shadertoy的核心工作机制:mainImage函数会为屏幕上的每个像素(通过fragCoord传入)计算颜色值。我们的第一个任务是建立适合SDF工作的坐标系系统:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { // 将坐标中心移到屏幕中央,y轴向上,范围[-1,1] vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y; // 初始化距离值为最大值 float d = 1e5; // 这里将放置SDF计算代码 // 根据距离值渲染(白色表示图形内部,黑色外部) fragColor = vec4(vec3(1.0-smoothstep(0.0,0.02,d)),1.0); }这段代码建立了几个重要约定:
- 坐标系原点在屏幕中心
- y轴向上,x轴向右
- 垂直方向范围固定为[-1,1],水平方向按屏幕比例缩放
- 使用
smoothstep实现边缘抗锯齿
提示:在Shadertoy中按Alt+Enter可以全屏预览效果,Ctrl+Enter强制重新编译着色器。
2. 圆形SDF:图形学的"Hello World"
圆形是最简单的2D图形,它的SDF也直观体现了距离函数的本质:
float sdCircle(vec2 p, float r) { return length(p) - r; }这个简洁的函数完成了三件事:
- 计算点p到原点的距离
length(p) - 减去半径r得到符号距离
- 正值表示点在圆外,负值表示圆内,零值恰好在边界
让我们在Shadertoy中实现并可视化这个圆形:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y; float d = sdCircle(p, 0.5); // 彩色可视化:内部红色,外部渐变蓝 vec3 col = mix(vec3(0.8,0.2,0.2), vec3(0.2,0.4,0.8), smoothstep(-0.1,0.1,d)); // 添加白色边界线 col = mix(col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d))); fragColor = vec4(col,1.0); }通过这个例子,我们可以观察到SDF的几个关键特性:
- 等距线:修改代码将
abs(d)直接作为颜色输出,会看到一系列同心圆环 - 平滑过渡:
smoothstep函数创造了边缘的渐变效果 - 灵活着色:距离值d可以作为各种视觉效果的控制参数
尝试交互式修改半径值,实时观察圆形变化:
// 添加时间动态效果 float r = 0.3 + 0.2*sin(iTime); float d = sdCircle(p, r);3. 线段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); }这个函数的几何原理是:
- 计算点p到线段端点a的向量pa
- 计算线段方向向量ba
- 通过点积求出pa在ba上的投影比例h
- 用clamp限制h在[0,1]范围内
- 最终距离就是p点到投影点的长度
在Shadertoy中可视化线段:
// 定义线段端点 vec2 a = vec2(-0.5, -0.2); vec2 b = vec2(0.5, 0.3); float d = sdSegment(p, a, b); // 可视化线段和端点 vec3 col = vec3(1.0-smoothstep(0.0,0.01,abs(d))); col = mix(col, vec3(1,0,0), 1.0-smoothstep(0.0,0.02,length(p-a))); col = mix(col, vec3(0,1,0), 1.0-smoothstep(0.0,0.02,length(p-b)));线段SDF的一个神奇特性是:当我们将距离减去一个常数,就能得到胶囊体效果:
// 胶囊体效果 float capsule = sdSegment(p,a,b) - 0.1;4. 矩形SDF:对称性与区域划分
轴对称矩形的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); }这个看似简洁的函数实际上处理了三种情况:
- 点在矩形外部右侧/上方:
max(d,0.0)选择正分量 - 点在矩形外部但靠近角:
length(max(d,0.0))计算到角的距离 - 点在矩形内部:
min(max(d.x,d.y),0.0)处理内部符号
在Shadertoy中实现交互式矩形:
// 动态变化的矩形尺寸 vec2 size = vec2(0.4, 0.6) * (1.0 + 0.2*cos(iTime)); float d = sdBox(p, size); // 添加圆角效果 float rounded = d - 0.05;矩形SDF的扩展性很强,我们可以轻松实现:
- 圆角矩形:SDF结果减去圆角半径
- 边框效果:使用
abs(d)-thickness - 挖空效果:组合多个矩形SDF
5. 胶囊体SDF:组合艺术的典范
胶囊体可以看作线段加圆的组合,其SDF完美展示了SDF系统的组合特性:
float sdCapsule(vec2 p, vec2 a, vec2 b, float r) { return sdSegment(p,a,b) - r; }这种"减法"操作在SDF中称为"扩张"(offset),是构建复杂形状的基础技术之一。让我们创建一个动态胶囊体:
// 动态端点 vec2 a = vec2(-0.6, 0.1*sin(iTime)); vec2 b = vec2(0.6, -0.1*cos(iTime)); // 脉动半径 float radius = 0.1 * (1.0 + 0.3*sin(iTime*2.0)); float d = sdCapsule(p, a, b, radius);胶囊体的可视化可以做得非常生动:
// 渐变色根据距离变化 vec3 col = mix(vec3(0.2,0.5,0.8), vec3(0.8,0.3,0.2), smoothstep(-0.5,0.5, sin(30.0*d))); // 添加发光效果 col += vec3(0.3,0.5,0.8) * exp(-10.0*abs(d));6. SDF组合与变换
SDF真正的威力在于它们的组合性。GLSL提供了多种运算符来组合SDF:
// 并集 float unionSDF(float a, float b) { return min(a, b); } // 交集 float intersectSDF(float a, float b) { return max(a, b); } // 差集 float differenceSDF(float a, float b) { return max(a, -b); }几何变换也同样重要:
// 平移 float translatedSDF(vec2 p, vec2 offset) { return sdCircle(p - offset, 0.3); } // 旋转 float rotatedSDF(vec2 p, float angle) { float c = cos(angle), s = sin(angle); vec2 q = vec2(c*p.x - s*p.y, s*p.x + c*p.y); return sdBox(q, vec2(0.4,0.2)); } // 缩放(均匀) float scaledSDF(vec2 p, float scale) { return sdBox(p/scale, vec2(0.3,0.5)) * scale; }让我们创建一个组合示例:
// 基础圆形 float circle = sdCircle(p, 0.4); // 旋转矩形 float box = rotatedSDF(p, iTime*0.5); // 组合效果 float d = differenceSDF(circle, box); // 添加边框 float border = abs(d) - 0.02;7. 高级技巧与性能优化
随着场景复杂度增加,我们需要考虑SDF的性能优化:
距离场抗锯齿
// 传统硬边缘 float edge = step(0.0, d); // 平滑边缘(推荐) float smoothEdge = smoothstep(0.0, fwidth(d), d);空间划分加速
// 粗略距离测试 float boundingCircle = length(p) - 1.0; if(boundingCircle > 0.1) { // 快速跳过远距离区域 fragColor = vec4(0.0,0.0,0.0,1.0); return; }SDF缓存技巧
// 复用中间计算结果 vec2 q = p * 2.0; float d1 = sdCircle(q, 0.5); float d2 = sdBox(q, vec2(0.3));距离场着色技巧
// 法线估计(用于光照) vec3 estimateNormal(vec2 p) { float eps = 0.001; return normalize(vec3( sdCircle(p + vec2(eps,0.0)) - sdCircle(p - vec2(eps,0.0)), sdCircle(p + vec2(0.0,eps)) - sdCircle(p - vec2(0.0,eps)), eps )); } // 简单光照 vec3 n = estimateNormal(p); float diff = max(0.0, dot(n, normalize(vec3(0.8,0.6,0.0))));8. 实战项目:构建2D SDF渲染引擎
让我们整合所学知识,创建一个可交互的SDF渲染系统:
// SDF场景定义 float sceneSDF(vec2 p) { // 背景网格 float grid = min(mod(p.x,0.1), mod(p.y,0.1)) - 0.005; // 动态元素 float circle = sdCircle(p-vec2(0.3,0.2), 0.2); float box = sdBox(p-vec2(-0.3,0.1), vec2(0.2,0.3)); float capsule = sdCapsule(p, vec2(-0.4,-0.3), vec2(0.4,0.3), 0.1); // 组合场景 return min(grid, min(circle, min(box, capsule))); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y; // 计算场景SDF float d = sceneSDF(p); // 高级着色 vec3 col = vec3(0.0); // 距离场基础色 col = mix(vec3(0.2,0.5,0.8), vec3(0.8,0.3,0.2), smoothstep(-0.5,0.5,d)); // 添加等距线 col *= 0.7 + 0.3*cos(6.2831*d*5.0); // 边缘高光 col += vec3(1.0) * exp(-100.0*abs(d)); // 鼠标交互 vec2 mouse = (2.0*iMouse.xy-iResolution.xy)/iResolution.y; if(length(mouse) > 0.0) { float mouseDist = sceneSDF(mouse); float influence = exp(-50.0*length(p-mouse)); col = mix(col, vec3(1.0,1.0,0.0), influence); } fragColor = vec4(col,1.0); }这个完整示例展示了:
- 模块化的SDF场景构建
- 多层次着色技术
- 交互式元素
- 视觉效果增强技巧
9. 创意延伸:SDF的无限可能
掌握了SDF基础后,你可以探索更多创意方向:
动态变形效果
// 波纹变形 float ripple = 0.02 * sin(p.x * 20.0 + iTime * 3.0); d += ripple;布尔操作进阶
// 平滑并集 float smoothUnion(float a, float b, float k) { float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0); return mix(b, a, h) - k*h*(1.0-h); }纹理集成
// 基于距离场的纹理映射 float pattern = sin(d * 30.0) * 0.5 + 0.5; col *= texture(iChannel0, p).rgb * pattern;2D到3D的延伸
// 简单的3D扩展 float sdSphere(vec3 p, float r) { return length(p) - r; }在Shadertoy社区中,艺术家们已经用SDF创造了无数令人惊叹的作品。从抽象的视觉艺术到逼真的场景渲染,SDF都展现出惊人的表现力。记住,每个复杂效果都是从一个简单的圆形或线段开始的——你现在已经掌握了这些基础构建模块,接下来就是发挥创意的时候了。