在前面的文章有写过如何绘制一个贝塞尔曲线,学了太久的SDF的推导,今天换换口味基于五边形镶嵌看看能探索出什么样的图案吧 Cairo Tiling
(又名Cairo Tessellation),它通常指的是一种特定的平面镶嵌方式,也就是平面铺砖模式,它由镀矿学家哈夫·艾哈迈德·哈德(Hafez A. Harutyunyan)和另一位未命名的研究人员于2009年在埃及开罗国际联合培训研讨会上提出。这种瓷砖图案由五边形组成,且该五边形的每个内角都不相同。Cairo Tiling是已知的仅有的14种单一五边形可以完整覆盖平面的情形之一。
绘制原理
做画布
完成SDF推导和编写后,已经有一些很套路的技能, 先搞一个正方形出来作为我们的画布
glsl
// uv -> [-1.0, 1.0]
vec2 uv = (2.0* fragCoord- iResolution.xy)/iResolution.y;
vec3 col = vec3(0.0);
uv *= 1.1;
vec2 p = abs(uv);
// square black board
if (max(p.x, p.y) > 1.0) col.g += 0.8;
当我们想要画y轴,直接将d=p.x
,同理如果想要画x轴,直接将d=p.y
. 在SDF逻辑运算这一篇中,如果想要union
两个SDF使用min
,在这里也一样 通过d=min(p.x, p.y)
可以获得坐标轴
画条斜线
- geogebra: www.geogebra.org/classic/jgg...
紧接着在画布里面画一条线, 虽然这里没有用到距离场函数,但是相关的计算技巧都是距离场会用到的, 假定有一条与x axis夹角为 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ的Line, 那么这条线的单位向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> v ⃗ \vec{v} </math>v 为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( c o s ( θ ) , s i n ( θ ) ) (cos(\theta), sin(\theta)) </math>(cos(θ),sin(θ)), 对于坐标系中的任意点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P, 其在向量上的投影长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> d o t ( O P ⃗ , v ⃗ ) dot(\vec{OP}, \vec{v}) </math>dot(OP ,v ), 有单位向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> v 2 v_2 </math>v2为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( s i n ( θ ) , c o s ( θ ) ) (sin(\theta), cos(\theta)) </math>(sin(θ),cos(θ)), 点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P在向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> v 2 v_2 </math>v2上的投影长度就是与 <math xmlns="http://www.w3.org/1998/Math/MathML"> v v </math>v的距离。 推理过程可以从下面的图中直观的看到

将上面的推导转化为代码便是
glsl
float angle = 0.4 * PI;
vec2 n = vec2(sin(angle), cos(angle));
float d = dot(p, n);
有了距离之后,我们可以很容易的画出一条直线,原理就是,如果 abs(d) < lineThinkness
就把颜色变成白色,当然使用老技巧,用fwidth
和smoothstep
进行抗锯齿操作
glsl
d = abs(d);
float thinkness = 0.01;
col += smoothstep(d-fwidth(d), d+fwidth(d), thinkness);
于是我们得到了线
这里有点点奇怪是为什么得到了一个x形状,其实原因是因为前面我们做了vec2 p = abs(uv);
相当于做了镜像,如果把这段话去掉,就可以看到这条线本来的样子
同时做一个坐标平移操作,把线的中心点挪到正方形的角上float d = dot(p-vec2(1.0), n);
用时间函数变化角度不是很好控制,我们将角度绑定 鼠标点击的y坐标, 这样就可以自主控制角度
glsl
// mouse belongs [0, 1], grow from west south
vec2 mouse = iMouse.xy/iResolution.xy;
float angle = mouse.y * PI;
随着鼠标的移动,可以看到线的变化
组合出五边形
通过距离的逻辑union和subtraction运算,可以组合出五边形镶嵌的部份组合。
glsl
d = min(d, p.x);
d = max(d, -p.y);

再用到上一篇讲到的SDF复制, 同时由于我们将坐标有调整到[-1, 1]了,需要将box值调整
glsl
uv *= 2.1;
uv = fract(uv);
uv = 2.0*uv - 1.0;
if (max(p.x, p.y) > 0.95) col.yz += 0.8;
然后得到这样,确实有点像监狱的铁丝网 不过上图其实是由菱形与六边形组成,现在需要将其转换成五边形,要做其实就是每间隔一个格子,将格子翻转一下。在完成这个目标之前,需要准确的标记出index,id标记非常简单其实就是
floor
一下,我们可以根据x,y
和的奇偶做背景颜色,获得像国际象棋一样的棋盘效果
glsl
vec2 id = floor(uv);
float check = mod(id.x + id.y , 2.0);
col += check*.5;
通过index,做翻转动作就非常简单, 去除掉背景,终于得到了开罗五边形镶嵌,而且还能动....
glsl
if (check == 1.0) p.xy = p.yx;

Distance 纠正
我们现有的distance是距离cell网格的距离,而实际上在做一些图形探索希望用到的是距离边缘的distance。 由于我们为了组合出五边形做的反转动作,通过col += d
将距离可视化可以看到图形的距离是很奇怪, 这里我用绿色格子相邻布局,一个五边形的三条边和两条边分别在不同的格子中,在下图中,蓝色边缘以内的距离是对的,但是到了红色边缘因为是另外一个格子所以是错的。
为了解决这个问题,就是将点到红色线的距离重新算一遍,这里要注意到一个特性是蓝色线和红色线永远是垂直的,那么套用我们前面画斜线的推导,可以有
glsl
d = min(d, dot(p-1.0, vec2(n.y, -n.x)));

五边形索引
在前面通过fract 获得了每一个cell的索引,但是我们的图形元素是五边形,所以我们还需要获得五边形的索引,来支撑后续的图形艺术创作. 以上图为例,将五边形的索引与cell的索引关联,其中
- 五边形1 为 (cell.x, cell.y - 0.5)
- 五边形2 为 (cell.x+0.5, cell.y )
- 五边形3 为 (cell.x, cell.y + 0.5)
- 五边形4 为 (cell.x-0.5, cell.y ) 判断未知其实很简单,主要依靠两个步骤
- 由于我们使用了p = abs(uv), 所有点都转换到了第一象限,以白线为界,白线左边的归属与 1, 白线右边归属与2
- 进一步通过x,y与0的大小,就可以明确属于哪个五边形 这里要注意的事情,我们在相邻的cell中,做了一次翻转,这回影响到第二步的判断。最后代码有
glsl
vec2 tileId = cellId;
if (check == 0.0) {
if (d > 0.0 ) {
if (uv.x < 0.) {
tileId.x -= .5;
} else {
tileId.x += .5;
}
} else {
if (uv.y < 0.) {
tileId.y -=.5;
} else {
tileId.y += .5;
}
}
} else {
if (d < 0.0 ) {
if (uv.x < 0.) {
tileId.x -=.5;
} else {
tileId.x += .5;
}
} else {
if (uv.y < 0.) {
tileId.y -=.5;
} else {
tileId.y += .5;
}
}
}
上面的代码又臭又长,我们做个优化
glsl
if (d * (check-0.5) < 0.0) {
tileId.x += sign(uv.x) * .5;
} else {
tileId.y += sign(uv.y) * .5;
}
最后根据坐标每个五边形来个不同的颜色
glsl
col += (tileId.x + tileId.y * scale * 2.0 + scale * scale * 2.0) / (scale * scale * 4.0);

艺术探索
以上便完成所有的代码推导,接下去虚拟的世界里找点乐子。
不同Tile不同颜色
这个很简单,主要是根据Tile的坐标Hash出一个颜色即可,这里使用网上找到代码, 下面的函数输入一个二维向量(坐标),产生一个三维向量(颜色)
glsl
// https://www.shadertoy.com/view/4djSRW Hash without Sine
vec3 hash23(vec2 p) by Dave_Hoskins
{
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
p3 += dot(p3, p3.yxz+33.33);
return fract((p3.xxy+p3.yzz)*p3.zyx);
}
运行之后的效果
距离场的Wave
基于距离的wave函数,在之前sdf推导过,这里不在重复推导
scss
void wave(float dist, inout vec3 color) {
color *= .8 + 0.2 * sin(dist * 50.0 - iTime * 5.0);
}

当然还有很多可以持续探索的的点,例如增加光照,对空间做一些affine变换