【shader基础】2D符号距离函数及其变换操作,用shader画个掘金logo

358 阅读6分钟

混掘金社区,是时候学习怎么用shader画一个掘金Logo了!

1. shadertoy

可以安装vscodeShader Toy插件或者使用线上的shadertoy(www.shadertoy.com/new)

  • 常用内置变量
uniform vec2 iResolution;//屏幕宽高
uniform vec3 iMouse;//x,y对应鼠标悬浮在屏幕的坐标,z>0的时候触发点击
uniform float iTime;//随时间递增的变量
  • 计算UV,画布像素坐标fragCoord范围[0,1],因为画布大小会导致画布形状拉伸变形,所以需要除以画布宽高的对应比例。为了绘制方便,将坐标的范围转换为[-1,1]
vec2 getUV(vec2 fragCoord) {
    return (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
}

2.用符号距离函数SDF画一个圆

符号距离函数(sign distance function),简称SDF,又可以称为定向距离函数(oriented distance function),在空间中的一个有限区域上确定一个点到区域边界的距离并同时对距离的符号进行定义:点在区域边界内部为正,外部为负,位于边界上时为0。

  • 圆的距离函数,p像素点坐标,center为圆的坐标,r为圆的半径,计算像素点到圆心的距离与半径的差
float sdCircle(vec2 p, vec2 center, float r) {
    return length(p - center) - r;
}
  • 绘制圆
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //圆的距离
    float d = sdCircle(uv, vec2(0.), 0.7);
    //圆的颜色,红色
    vec3 circleColor = vec3(1., 0., 0.);    
    vec3 color = (1. - sign(d)) * circleColor;
    fragColor = vec4(color, 1.);
}
  • sign:返回数值的正负符号,-1负,1正,0
  • sign(d):-1在圆内,0在边上,1在圆外
  • 1.-sign(d)通过取反,即1为在圆内,显示圆的颜色

image.png

//六边形
float sdHexagon( in vec2 p, in float r )
{
    const vec3 k = vec3(-0.866025404,0.5,0.577350269);
    p = abs(p);
    p -= 2.0*min(dot(k.xy,p),0.0)*k.xy;
    p -= vec2(clamp(p.x, -k.z*r, k.z*r), r);
    return length(p)*sign(p.y);
}

image.png

3.形状变换

  • 移动形状,就是坐标xy加减沿xy轴正负方向变化
const float PI = 3.1415926;
float shape(in vec2 p) {
    //沿着圆弧移动
    vec2 translate = vec2(cos(iTime * PI), sin(iTime * PI)) * 0.5;
    //坐标移动
    vec2 m = translate + p;
    //六边形
    return sdHexagon(m, 0.3);
}

20250119_220703.gif

  • 旋转形状
//旋转矩阵
mat2 rotate2d(float _angle) {
    return mat2(cos(_angle), -sin(_angle), sin(_angle), cos(_angle));
}
const float PI = 3.1415926;
float shape(in vec2 p) {
    //坐标旋转
    vec2 m = rotate2d(iTime * PI) * p;
    //六边形
    return sdHexagon(m, 0.7);
}

20250119_215921.gif

  • 放缩形状,将坐标xy乘以缩放值
const float PI = 3.1415926;
float shape(in vec2 p) {   
    //坐标放缩
    vec2 m = vec2(sin(iTime * PI * 0.2)) * p;
    //六边形
    return sdHexagon(m, 0.3);
}

20250119_223049.gif

4. 绘制多个形状

  • 取多个形状的符号距离函数的最小值min作为绘制形状的距离
//p像素坐标
float shape(in vec2 p) {
//左上圆
    float d = sdCircle(p - vec2(-0.5, 0.5), 0.2);
    //右上圆
    d = min(d, sdCircle(p - vec2(0.5, 0.5), 0.2));
    //下中圆
    return min(d, sdCircle(p - vec2(0., 0.), 0.5));
}

image.png

  • 按角度平均等分,设置形状位置
//r半径,a占角度的比例
vec2 anglePos(float r, float a) {
    a *= PI * 2.0;
    return r * vec2(sin(a), cos(a));
}
float shape(in vec2 p) {
    float d = 9.;
    //360度六等分
    float unit = 1. / 6.;
    //坐标偏移半径
    float radius = 0.5;
    //遍历绘制多个圆形
    for(int i = 0; i < 6; i++) {
        d = min(d, sdCircle(p - anglePos(radius, float(i) * unit), 0.2));
    }
    return d;
}

image.png

  • vec4(距离,颜色rgb)来记录每个形状颜色距离信息,给不同形状赋值不同颜色,重写min函数,返回距离函数中最小值的对应向量值
vec4 minV(vec4 a, vec4 b) {
    return a.x < b.x ? a : b;
}
  • 对应调整一下形状距离函数
vec4 shape(in vec2 p) {
    //左上红色圆
    vec4 d = vec4(sdCircle(p - vec2(-0.5, 0.5), 0.2), vec3(1., 0., 0.));
    //右上绿色圆
    d = minV(d, vec4(sdCircle(p - vec2(0.5, 0.5), 0.2), vec3(0., 1.0, 0.)));
   //下中蓝色圆
    return minV(d, vec4(sdCircle(p - vec2(0., 0.), 0.5), vec3(0., 0., 1.)));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //距离函数
    vec4 d = shape(uv);
    //形状的颜色
    vec3 shapeColor = d.yzw;
    vec3 color = (1. - sign(d.x)) * shapeColor;
    fragColor = vec4(color, 1.);
}

image.png

  • 多个形状平滑过渡,重写min函数使其一定程度融合,更多平滑最小函数
//平滑最小值,融合形状,k平滑过渡程度
float smin(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);
}
  • 对应调整一下shape函数里面的最小值函数
const float PI = 3.1415926;
float shape(in vec2 p) {
    //平滑融合程度
    float k = 0.5 * abs(sin(iTime * PI));
    float d = sdCircle(p - vec2(-0.5, 0.5), 0.2);
    d = smin(d, sdCircle(p - vec2(0.5, 0.5), 0.2), k);
    return smin(d, sdCircle(p - vec2(0., 0.), 0.5), k);
}

20250119_211819.gif

  • 重复绘制形状,使用mod函数
//圆半径
const float r = 0.3;
float shape(in vec2 p) {
    //像素坐标取模
    vec2 a = mod(p + r, 2. * r) - r;
    return sdCircle(a, r);
}

image.png

  • 更换成六边形
//六边形半径
const float r = 0.3;
float shape(in vec2 p) {
    //像素坐标取模
    vec2 a = mod(p + r, 2. * r) - r;
    //六边形半径减去空隙,保持形状独立不重叠
    return sdHexagon(a, r - 0.05);
}

image.png

5.布尔运算

上面的min其实就是union并集运算,这里就不重复了。

  • intersection交集运算,求两个形状相交的部分,
float shape(in vec2 p) {
    //两个形状的交集,六边形与圆形的距离,取两者的最大值
    return max(sdHexagon(p - vec2(-0.3, 0), 0.5), sdCircle(p - vec2(0.3, 0), 0.5));
}

最终形状正好是圆的一边弧与六边形的两条边形成的扇形

image.png

  • Subtraction差集运算,求两个形状的差,就是没相交的部分,通过取A形状的补集(不在A内)与B形状相交的部分计算出来集合即是。
float shape(in vec2 p) {
    //两个形状的差,六边形与圆形的负值的距离,取两者的最大值
    return max(sdHexagon(p, 0.5), -1. * sdCircle(p, 0.3));
}

image.png

  • 注意:差集运算,A-B与B-A是不一样的运算
float shape(in vec2 p) {
    //两个形状的差,负值六边形与圆形的距离,取两者的最大值
    return max(-1. * sdHexagon(p, 0.3), sdCircle(p, 0.5));
}

image.png

6.实战:shader画个掘金Logo

juejin.png

掘金的logo由一个菱形和两条有宽度的折线形成

  • 菱形的距离函数
float ndot(vec2 a, vec2 b) {
    return a.x * b.x - a.y * b.y;
}
//菱形 p像素坐标,b的x对应横向对角线长度,y对应纵向对角线长度
float sdRhombus(in vec2 p, in vec2 b) {
    p = abs(p);
    float h = clamp(ndot(b - 2.0 * p, b) / dot(b, b), -1.0, 1.0);
    float d = length(p - 0.5 * b * vec2(1.0 - h, 1.0 + h));
    return d * sign(p.x * b.y + p.y * b.x - b.x * b.y);
}
  • 两条有宽度的折线是两个同样大小的菱形形成的差集
//两个菱形的差集即为一条有宽度折线
// a为第一个菱形的位置,b为第二个菱形的位置,size两个菱形的大小
float line(vec2 p, vec2 a, vec2 b, vec2 size) {
    float d1 = sdRhombus(p - a, size);
    float d2 = sdRhombus(p - b, size);
    return max(-1. * d1, d2);
}
  • 将形状组合起来
 //顶部小菱形
    float d = sdRhombus(p - vec2(0., 0.2), vec2(0.12, 0.1));
    //折线1
    float l = line(p, vec2(0., 0.12), vec2(0., 0.0), vec2(0.36, 0.3));
   //折线2
    float ll = line(p, vec2(0., -0.12), vec2(0., -0.24), vec2(0.6, 0.5));
    //合并形状
    return min(ll, min(l, d));
  • 修改像素坐标p,让形状动起来
//绕圆移顺时针动起来
    vec2 m = vec2(-cos(iTime * PI * 0.5), sin(iTime * PI * 0.5)) * 0.5;
    p += m;
    //缩小成原来的1/2
    p *= 2.0;
  • 绘制符号距离函数
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = getUV(fragCoord);
    //距离函数
    float d = shape(uv);
    //背景颜色,蓝色
    vec3 bg = vec3(0.117, 0.5, 1.);
    //形状的颜色,白色
    vec3 shapeColor = vec3(1.0);
    vec3 color = (1. - sign(d)) * shapeColor + bg;
    fragColor = vec4(color, 1.);
}

20250120_003823.gif

啦啦啦,大功告成!小伙伴们学会了吗?

Github地址

https://github.com/xiaolidan00/awesome-bg

参考