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)));

最后有

资料

相关推荐
小猪努力学前端1 天前
基于PixiJS的小游戏广告开发
前端·webgl·游戏开发
光影少年3 天前
WebGIS 和GIS学习路线图
学习·前端框架·webgl
DBBH3 天前
Cesium源码分析之渲染3DTile的一点思考
图形渲染·webgl·cesium.js
唯道行3 天前
计算机图形学·19 Shadings in OpenGL
人工智能·算法·计算机视觉·几何学·计算机图形学·opengl
Robet4 天前
TS2d渲染引擎
webgl
Robet4 天前
WebGL2D渲染引擎
webgl
goodName5 天前
如何实现精准操控?Cesium模型移动旋转控件实现
webgl·cesium
丫丫7237347 天前
Three.js 模型树结构与节点查询学习笔记
javascript·webgl
allenjiao10 天前
WebGPU vs WebGL:WebGPU什么时候能完全替代WebGL?Web 图形渲染的迭代与未来
前端·图形渲染·webgl·threejs·cesium·webgpu·babylonjs
mapvthree10 天前
mapvthree Engine 设计分析——二三维一体化的架构设计
webgl·数字孪生·mapvthree·jsapi2d·jsapigl·引擎对比