以下的图案都是基于Voronoi创作出的一些艺术图形,怎么样,非常动人吧,只用了大概不到200行的代码。
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图的基本特性------空间分割和最近邻优化。这些特性一方面与自然界物理和生物过程的基本法则相契合,另一方面也是自然界追求效率和平衡的结果。
- 资源分配的效率: 在自然界中,许多生物体(如细胞、植物)需要争夺有限的资源(如光、水、营养物质)。Voronoi图自然而然地模拟了这种资源分配的过程,其中每个点(种子)代表一个获取资源的主体,而Voronoi单元代表该主体能够有效利用的区域。这种分配方式保证了每个生物体都有相对公平的资源获取机会,并且对资源的利用达到了局部最优。
- 自然现象的物理规律:一些自然过程,如裂纹的形成、干涸泥土的裂缝、冰的结晶等,遵循最小能量原则。Voronoi图在形成时,每个单元的边缘可以视为平衡点,即不同力量相互作用的结果。这与自然界中力和能量分布均匀的原则相一致。
- 生物组织和结构:在生物学中,许多生物组织的结构在微观上呈现出与Voronoi图相似的模式。例如,昆虫的眼睛、植物的叶脉、动物皮肤的斑点等。这些结构通常是由成长、细胞分裂和资源竞争等过程自然形成的,Voronoi图自然地描绘了这些过程中的空间动态和结构形态。
- 最近邻效应:自然界中的许多现象和互动是基于最近邻效应的,如动物的领地划分、种群的分布等。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);
于是我们得到了边界
当上面的方法有两个问题
- 没有获得实际的距离
- 边界的粗细不均匀,距离两特征点中心点越远越不准,当两个特征点距离越近不准的程度会加剧
为了克服上面的问题,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)));
最后有
资料
- The art of code: www.youtube.com/watch?v=l-0...
- iq: iquilezles.org/articles/vo...
- the book of shader: thebookofshaders.com/12/?lan=en
- voronoi图的应用:www.bilibili.com/video/BV1p8...
- voronoi blender: www.youtube.com/watch?v=dSl...