分段函数

536 阅读1分钟

源码:github.com/buglas/shad…

1-分段函数的概念

分段函数我们在高中就学了,其定义是:在不同定义域中的集合间有不同的映射规则。

比如下面的函数:

image-20220825151602219

其函数图像如下:

image-20220825151735854

利用分段函数,我们可以做一些非常有用的效果,比如棋盘格:

07

接下来,咱们就通过棋盘格说一下如何绘制分段图像。

2-绘制棋盘格

绘制棋盘格的方法有很多,IQ大佬就给我提供了三种绘制棋盘格的方法。

  • mod 取余
float checkers( in vec2 p ){
    vec2 q = floor(p);
    return mod(q.x+q.y,2.);
}
  • xor 异或
float checkers( in vec3 p ){
    ivec2 ip = ivec2(round(p+.5));
    return float((ip.x^ip.y)&1);
}
  • sign
float checkers( in vec3 p ){
    vec2 s = sign(fract(p*.5)-.5);
    return .5 - .5*s.x*s.y;
}

上面的代码对于初学者而言,理解起来会有点费劲,我们可以将其画出来看看,就好理解了。

2-1-mod 棋盘格

1.我们先把函数图像画出来看看。

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 坐标轴
vec4 AxisHelper(in vec2 coord, in float axisWidth, in vec4 xAxisColor, in vec4 yAxisColor) {
  vec4 color = vec4(0, 0, 0, 0);
  float dx = dFdx(coord.x) * axisWidth;
  float dy = dFdy(coord.y) * axisWidth;
  if(abs(coord.x) < dx) {
    color = yAxisColor;
  } else if(abs(coord.y) < dy) {
    color = xAxisColor;
  }
  return color;
}

// 栅格
vec4 GridHelper(in vec2 coord, in float gridWidth, in vec4 gridColor) {
  vec4 color = vec4(0, 0, 0, 0);
  float dx = dFdx(coord.x) * gridWidth;
  float dy = dFdy(coord.y) * gridWidth;
  vec2 fraction = fract(coord);
  if(fraction.x < dx || fraction.y < dy) {
    color = gridColor;
  }
  return color;
}

// 投影坐标系辅助对象
vec4 ProjectionHelper(in vec2 coord, in float axisWidth, in vec4 xAxisColor, in vec4 yAxisColor, in float gridWidth, in vec4 gridColor) {
  // 坐标轴
  vec4 axisHelper = AxisHelper(coord, axisWidth, xAxisColor, yAxisColor);
  // 栅格
  vec4 gridHelper = GridHelper(coord, gridWidth, gridColor);
  // =投影坐标系
  return bool(axisHelper.a) ? axisHelper : gridHelper;
}

//  线段
vec4 Segment(in vec2 C, in vec2 A, in vec2 B, in float lineWidth, in vec4 lineColor) {
  // 向量AB
  vec2 AB = B - A;
  // 向量AB的单位向量
  vec2 ABn = normalize(AB);
  // 向量AC
  vec2 AC = C - A;
  // 点C到直线AB的距离 = 向量AC与单位向量ABn的叉乘
  float distance = abs(AC.x * ABn.y - AC.y * ABn.x);
  // 偏移导数
  float dx = dFdx(C.x);
  // 基于偏移导数的线宽
  float width = dx * lineWidth;
  // 收缩线宽
  float shrinkWidth = max(width - dx * 2., dx);
  // 用于抗锯齿的透明度
  float a = 1. - smoothstep(shrinkWidth, width, distance);
  // 将有向距离AC和有向距离AB的比值收束在[0,1] 之间
  float ratio = clamp(dot(AC, AB) / dot(AB, AB), 0., 1.);
  // 线段
  return length(ratio * AB - AC) < width ? vec4(vec3(lineColor), a * lineColor.a) : vec4(0);
}

// 分段算法
float PiecewiseFn(float x) {
  // return mod(x, 2.);
  // return floor(mod(x, 2.));
  return mod(floor(x), 2.);
}

//分段路径(当前点coord,起点star,结束点end,段数segs,线宽lineWidth,颜色sinColor)
vec4 PicewisePath(vec2 coord, float start, float end, int segs, float lineWidth, vec4 sinColor) {
  vec4 color = vec4(0);
  float step = (end - start) / float(segs);
  for(int n = 0; n < segs; n++) {
    float x = start + float(n) * step;
    float nextX = x + step;
    vec2 A = vec2(x, PiecewiseFn(x));
    vec2 B = vec2(nextX, PiecewiseFn(nextX));
    vec4 segment = Segment(coord, A, B, lineWidth, sinColor);
    if(segment.a != 0.) {
      color = segment;
    }
  }
  return color;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标系缩放系数
  float scale = 3.;

  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord, scale);
  // 背景色  
  vec4 backgroundColor = vec4(0, 0, 0, 1);
  // 投影坐标系辅助对象
  vec4 projectionHelper = ProjectionHelper(coord, 2., vec4(0, .4, 0, 1), vec4(.4, 0, 0, 1), 2., vec4(vec3(.3), 1));

  /* 分段路径 */
  // 起点和终点
  float A = -2.;
  float B = 2.;
  // 分段数
  int segs = int(iResolution.x * (B - A) / (scale * 2.));
  // 路径
  vec4 path = PicewisePath(coord, A, B, segs, 2., vec4(1));
  // 最终的颜色
  fragColor = mix(backgroundColor + projectionHelper, path, path.a);
}

效果如下:

image-20220825155144033

上面的PiecewiseFn() 方法便是分段函数。

float PiecewiseFn(float x) {
  // return mod(x, 2.);
  // return floor(mod(x, 2.));
  return mod(floor(x), 2.);
}
  • mod(x,y):基于y定义x取余

在PiecewiseFn方法里我对mod算法进行了逐步分解,我可以通过函数图像,更好的认识mod方法的作用。

比如取消第一行的注释:

float PiecewiseFn(float x) {
  return mod(x, 2.);
  // return floor(mod(x, 2.));
  return mod(floor(x), 2.);
}

效果如下:

image-20221004135808262

根据PiecewiseFn()方法的分段,我们可以绘制一个棋盘格出来:

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 棋盘
float checkers(in vec2 p) {
  //[0,1]
  vec2 q = floor(p);
  //0,1
  return mod(q.x + q.y, 2.);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord, 3.);
  // 棋盘
  vec3 color = vec3(checkers(coord));
  // 最终的颜色
  fragColor = vec4(color, 1);
}

效果如下:

image-20221004140137602

以此原理,我们还可以用其它的两种方法绘制棋盘格。

2-2-xor棋盘格

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 棋盘
float checkers(in vec2 p) {
  // [1,2,3,……]
  ivec2 ip = ivec2(round(p + .5));
  //^按位异或、&按位与
  return float((ip.x ^ ip.y) & 1);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord, 3.);
  // 棋盘
  vec3 color = vec3(checkers(coord));
  // 最终的颜色
  fragColor = vec4(color, 1);
}

2-3-sign棋盘格

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 棋盘
float checkers(in vec2 p) {
  vec2 s = sign(fract(p * .5) - .5);
  // return sign(fract(p.x* .5)- .5);
  // return fract(p.x* .5)- .5;
  // return fract(p.x* .5);
  // return fract(p.x);
  // return s.x * s.y;
  // return s.x;
  return 0.5 - 0.5 * s.x * s.y;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord, 3.);
  // 棋盘
  vec3 color = vec3(checkers(coord));
  // 最终的颜色
  fragColor = vec4(color, 1);
}

除此之外,还有一种方法可以绘制出模糊的棋盘格。

3-绘制模糊的棋盘格

模糊的棋盘格可以解决棋盘格在密度极大时,栅格图像的采样密度跟不上棋盘密度后,出现的采样失真问题。这个问题我们会在后面的三维棋盘格中详解。

在此,我们先简单说一下模糊棋盘格的绘制。

整体代码如下:

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord, in float scale) {
  return scale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 三角形分段函数
vec2 tri(in vec2 p) {
  vec2 h = fract(p * .5) - .5;
  return 1. - 2. * abs(h);
}

// 棋盘
float checkers(in vec2 p) {
  // 精度
  vec2 w = vec2(.5);
  // 求导
  vec2 i = (tri(p + 0.5 * w) - tri(p - 0.5 * w)) / w;
  return 0.5 - 0.5 * i.x * i.y;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord, 3.);
  // 棋盘
  vec3 color = vec3(checkers(coord));
  // 最终的颜色
  fragColor = vec4(color, 1);
}

效果如下:

image-20221004154505045

解释一下其绘制原理。

1.准备一个 三角形分段函数。

vec2 tri(in vec2 p) {
  vec2 h = fract(p * .5) - .5;
  return 1. - 2. * abs(h);
}

其函数图像如下:

image-20221004154657188

2.按照特定距离对上面的分段函数求导。

float checkers(in vec2 p) {
  // 精度
  vec2 w = vec2(.5);
  // 求导
  vec2 i = (tri(p + 0.5 * w) - tri(p - 0.5 * w)) / w;
  return 0.5 - 0.5 * i.x * i.y;
}

其函数图像如下:

image-20221004161736036

导数公式如下:

img

在checkers() 方法中,w值很重要,这个值越大,会让棋盘格越模糊。

比如,我让w等于vec2(.9)。

float checkers(in vec2 p) {
  // 精度
  vec2 w = vec2(.9);
  // 求导
  vec2 i = (tri(p + 0.5 * w) - tri(p - 0.5 * w)) / w;
  return 0.5 - 0.5 * i.x * i.y;
}

效果如下:

image-20221004162230749

关于棋盘格,我们就说到这,这里更多的还是让大家知道如何用算法绘图。

扩展

我们可以用之前说过的绘图方法来绘制一个矩形。

vec2 sqr_tri(in float x) {
  float h = fract(x * 0.5) - .5;
  return vec2(sign(h), abs(h));
}

效果如下:

image-20221004172712858

大家可以想象一下为什么这样就可以画出矩形。

提示一下:注意x=0和x=1时的前后点。

若想不来可以跟我微信联系。

关于分段函数的应用我们就说到这,下一节课我们会说SDF。

参考链接:

iquilezles.org/articles/ch…

space.bilibili.com/10707223/ch…