Artist Toolbox2_Voronoi Diagram 大自然的几何艺术

以下的图案都是基于Voronoi创作出的一些艺术图形,怎么样,非常动人吧,只用了大概不到200行的代码。

www.shadertoy.com/view/wsfXDS

www.shadertoy.com/view/4sl3Dr

www.shadertoy.com/view/Mld3Rn

Voronoi Diagram

定义

给定一个由(n)个点组成的集合 <math xmlns="http://www.w3.org/1998/Math/MathML"> P = p 1 , p 2 , ... , p n P = {p_1, p_2, \ldots, p_n} </math>P=p1,p2,...,pn,这些点被称为种子点。Voronoi图则将整个空间划分为(n)个区域 <math xmlns="http://www.w3.org/1998/Math/MathML"> V ( p 1 ) , V ( p 2 ) , ... , V ( p n ) V(p_1), V(p_2), \ldots, V(p_n) </math>V(p1),V(p2),...,V(pn),其中每个区域 <math xmlns="http://www.w3.org/1998/Math/MathML"> V ( p i ) V(p_i) </math>V(pi)是由所有距离种子点 <math xmlns="http://www.w3.org/1998/Math/MathML"> p i p_i </math>pi更近于其它种子点的所有点组成的集合。即对于每个点 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x在区域 <math xmlns="http://www.w3.org/1998/Math/MathML"> V ( p i ) V(p_i) </math>V(pi)内,满足欧几里得距离 <math xmlns="http://www.w3.org/1998/Math/MathML"> d ( x , p i ) < d ( x , p j ) d(x, p_i) < d(x, p_j) </math>d(x,pi)<d(x,pj) 这样说明有点过于抽象,可以参考下图理解, 下图中黄色的点为种子点,多边形便是划分出来的区域,

大自然

大自然中Voronoi图样非常常见,其原因主要归结于Voronoi图的基本特性------空间分割和最近邻优化。这些特性一方面与自然界物理和生物过程的基本法则相契合,另一方面也是自然界追求效率和平衡的结果。

  1. 资源分配的效率: 在自然界中,许多生物体(如细胞、植物)需要争夺有限的资源(如光、水、营养物质)。Voronoi图自然而然地模拟了这种资源分配的过程,其中每个点(种子)代表一个获取资源的主体,而Voronoi单元代表该主体能够有效利用的区域。这种分配方式保证了每个生物体都有相对公平的资源获取机会,并且对资源的利用达到了局部最优。
  2. 自然现象的物理规律:一些自然过程,如裂纹的形成、干涸泥土的裂缝、冰的结晶等,遵循最小能量原则。Voronoi图在形成时,每个单元的边缘可以视为平衡点,即不同力量相互作用的结果。这与自然界中力和能量分布均匀的原则相一致。
  3. 生物组织和结构:在生物学中,许多生物组织的结构在微观上呈现出与Voronoi图相似的模式。例如,昆虫的眼睛、植物的叶脉、动物皮肤的斑点等。这些结构通常是由成长、细胞分裂和资源竞争等过程自然形成的,Voronoi图自然地描绘了这些过程中的空间动态和结构形态。
  4. 最近邻效应:自然界中的许多现象和互动是基于最近邻效应的,如动物的领地划分、种群的分布等。Voronoi图恰好提供了一种直观的方式来描述和模拟这种最近邻的空间关系。

以长颈鹿为例,长颈鹿胚胎中的黑色素分泌细胞不规则的分散分布,在怀孕过程中,这些细胞逐渐释放黑色素,最终形成了长颈鹿深色斑纹。有兴趣可以参考这篇论文Integrating Shape and Pattern in Mammalian Models,作者使用Voronoi Diagram 来模拟哺乳类动物的斑纹生成。

推导与实现

实现Voronoi Diagram都好几种方法, 下面会介绍好理解的暴力法, 以及有些限制但是计算快的网格法,最后会介绍IQ的优化边界问题的方法。 其中网格法最早是解决二维欧几里得最近邻搜索(Nearest Neighbor Search, NNS)问题, 有意思的是解决NNS问题也可以通过构造Voronoi Diagram来加速求解过程

暴力解法

暴力解法简单粗暴,假设有50个种子点,代码中对每个种子点进行比较,然后取最小的dist, 说先利用首先引入一个随机函数,用于产生种子点

glsl 复制代码
// from https://www.shadertoy.com/view/ldl3W8
vec2 hash22( vec2 p )
{
	return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}

增加一个运动函数,然种子点动起来, 以下我们产生了50个种子点,同时通过sin函数与时间的结合,让点的位置distortion。将点通过smmothstep画出来

glsl 复制代码
	float m = 0.;
    float t = iTime*.8;
    
    for (float i = 0.; i < 50.; i++) {
        vec2 n = hash22(vec2(i));
        vec2 p = sin(n*t);
        float d = length(uv-p);
        m += smoothstep(.02, .01, d);
    }
    col += m;

最后有效果 然后通过计算像素点到50个种子点的距离,取最小距离。

glsl 复制代码
    float minDist = 100.;
    for (float i = 0.; i < 50.; i++) {
	    ...
		if (d < minDist) minDist = d;
	}
	col += minDist;

Voronoi 就出来了,而且已经挺漂亮的啦

当然和上文的Cairo Tilling一样,我们需要知道每个块的index, 这个非常简单其实就是那种子点的index

glsl 复制代码
        if (d < minDist) {
            minDist = d;
            cellIdx = i;
        }
	col += cellIdx/50.0;

于是可以看到每块的灰度值不一样了

网格法

显而易见,上面的方法性能消耗验证,每一个像素点需要对种子点做计算。 考虑下面这个九宫格 假设种子点分布在不同的格子中,那么5号格子内所有点去找最近的种子点,只需要在[1,9]格子中去找。这就是优化的思路,种子点会移动,但是只会在某个格子中移动, 而格子的划分在前面sdf repeat里面已经推导过如何做, 就三步,放大,分割,坐标

glsl 复制代码
    uv *= 3.;
    vec2 gv = 2.0 * (fract(uv) -.5;)
    vec2 id = floor(uv);

接下来就是为每个九宫格求出9个种子点,

glsl 复制代码
    for (float y =-1.; y <=1.; y++){
    for (float x =-1.; x <=1.; x++){
        vec2 offs = vec2(x,y);
        vec2 n = hash22(id+offs);
        vec2 p = offs+sin(n*t);   
    }}

接下去的方法和暴力法一样,不过要注意的是格子索引是通过grid+offset得到的

glsl 复制代码
	float d = length(gv-2.0 * p);
	if (d < minDist){
		minDist = d;
		cid = id +offs;
	}

最后我们将索引变成颜色col.rb = (cid + vec2(5.0)) / 10.0; 感觉不错啊

边界距离

好用的shape函数可以返回shape更多的特征,上节的Voronoi 给出了 distance和cellIndex 两个特征,现在尝试获取第三个特征,点到边界的距离。

画边界的算法是存下最近的两个距离,即上图的 <math xmlns="http://www.w3.org/1998/Math/MathML"> d 1 d1 </math>d1 <math xmlns="http://www.w3.org/1998/Math/MathML"> d 2 d2 </math>d2. 对两个距离做一个差值,如果差值越接近于0 ,说明考边界越近。 变成代码便是

ini 复制代码
    vec2 cloest12Distance = vec2(100.0);
	 if (ed < cloest12Distance.x){
		cloest12Distance.y = cloest12Distance.x;
		cloest12Distance.x = ed;
	} else if (ed < cloest12Distance.y) {
		cloest12Distance.y  = ed;
	}
	...
    col += 1.0 - smoothstep(0.0,0.05,cloest12Distance.y - cloest12Distance.x);

于是我们得到了边界

当上面的方法有两个问题

  1. 没有获得实际的距离
  2. 边界的粗细不均匀,距离两特征点中心点越远越不准,当两个特征点距离越近不准的程度会加剧

为了克服上面的问题,IQ大神 iquilezles.org/articles/vo... 这篇文章提出了一个直接计算的方法。 从上图可以看出,实际上点到边界的距离就是, <math xmlns="http://www.w3.org/1998/Math/MathML"> m x ⃗ \vec{mx} </math>mx 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> a b ⃗ \vec{ab} </math>ab 单位向量的投影. 即
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C l o e s t D i s t a n c e ( x , b o r d e r ) = ( x − a + b 2 ) ⋅ n o r m a l i z e ( b − a ) = ( a − x ) + ( b − x ) 2 ⋅ n o r m a l i z e ( ( b − x ) − ( a − x ) ) = x a ⃗ + x b ⃗ 2 ⋅ n o r m a l i z e ( x b ⃗ − x a ⃗ ) \begin{align} CloestDistance(x, border) &= (x - \frac{a+b}2) \cdot normalize(b - a) \\ &= \frac{(a - x) + (b - x)} 2 \cdot normalize((b-x) - (a-x)) \\ &= \frac{\vec{xa}+\vec{xb}}2 \cdot normalize(\vec{xb} - \vec{xa}) \end{align} </math>CloestDistance(x,border)=(x−2a+b)⋅normalize(b−a)=2(a−x)+(b−x)⋅normalize((b−x)−(a−x))=2xa +xb ⋅normalize(xb −xa )

上面等式中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x a ⃗ \vec{xa} </math>xa 不就是我们求距离用到的向量嘛~, 所以只要对上面的代码简单做个修改,就可以做出完美的边界

ini 复制代码
        if (ed < cloest12Distance.x){
            oA = p;
        } else if (ed < cloest12Distance.y) {
            oB = p;
        }
    }}
    float d2 = abs(dot( (oA + oB) * .5,normalize(oB-oA)));

最后有

资料

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