Shader Shapes1_开罗平面镶嵌

在前面的文章有写过如何绘制一个贝塞尔曲线,学了太久的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)可以获得坐标轴

画条斜线

紧接着在画布里面画一条线, 虽然这里没有用到距离场函数,但是相关的计算技巧都是距离场会用到的, 假定有一条与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 就把颜色变成白色,当然使用老技巧,用fwidthsmoothstep进行抗锯齿操作

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 ) 判断未知其实很简单,主要依靠两个步骤
  1. 由于我们使用了p = abs(uv), 所有点都转换到了第一象限,以白线为界,白线左边的归属与 1, 白线右边归属与2
  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变换

资料

  1. wiki en.wikipedia.org/wiki/Cairo_...
  2. bigwings www.youtube.com/watch?v=51L...
相关推荐
不惑_3 天前
最佳ThreeJS实践 · 实现赛博朋克风格的三维图像气泡效果
javascript·node.js·webgl
小彭努力中4 天前
50. GLTF格式简介 (Web3D领域JPG)
前端·3d·webgl
小彭努力中4 天前
52. OrbitControls辅助设置相机参数
前端·3d·webgl
幻梦丶海炎5 天前
【Threejs进阶教程-着色器篇】8. Shadertoy如何使用到Threejs-基础版
webgl·threejs·着色器·glsl
小彭努力中5 天前
43. 创建纹理贴图
前端·3d·webgl·贴图
小彭努力中6 天前
45. 圆形平面设置纹理贴图
前端·3d·webgl·贴图
Ian10256 天前
webGL入门(五)绘制多边形
开发语言·前端·javascript·webgl
小彭努力中7 天前
49. 建模软件绘制3D场景(Blender)
前端·3d·blender·webgl
优雅永不过时·10 天前
使用three.js 实现着色器草地的效果
前端·javascript·智慧城市·webgl·three·着色器
baker_zhuang11 天前
Threejs创建胶囊体
webgl·threejs·web3d