1-分段函数的概念
分段函数我们在高中就学了,其定义是:在不同定义域中的集合间有不同的映射规则。
比如下面的函数:
其函数图像如下:
利用分段函数,我们可以做一些非常有用的效果,比如棋盘格:
接下来,咱们就通过棋盘格说一下如何绘制分段图像。
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);
}
效果如下:
上面的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.);
}
效果如下:
根据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);
}
效果如下:
以此原理,我们还可以用其它的两种方法绘制棋盘格。
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);
}
效果如下:
解释一下其绘制原理。
1.准备一个 三角形分段函数。
vec2 tri(in vec2 p) {
vec2 h = fract(p * .5) - .5;
return 1. - 2. * abs(h);
}
其函数图像如下:
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;
}
其函数图像如下:
导数公式如下:
在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;
}
效果如下:
关于棋盘格,我们就说到这,这里更多的还是让大家知道如何用算法绘图。
扩展
我们可以用之前说过的绘图方法来绘制一个矩形。
vec2 sqr_tri(in float x) {
float h = fract(x * 0.5) - .5;
return vec2(sign(h), abs(h));
}
效果如下:
大家可以想象一下为什么这样就可以画出矩形。
提示一下:注意x=0和x=1时的前后点。
若想不来可以跟我微信联系。
关于分段函数的应用我们就说到这,下一节课我们会说SDF。
参考链接: