大家好,我是Shawn,今天我们用shaderlab来实现一个最简单的Voronoi图。首先什么是Voronoi图?Voronoi图,又叫泰森多边形或Dirichlet图,它是由一组由连接两邻点直线垂直平分线组成的连续多边形组成。它是一个程序纹理,通常用来实现一些随机的事件。有时候,我们可以简单的使用random函数来实现一些伪随机,但实际上,这通常表现的很不理想。想象一下你处在完全随机的迷宫里,或许它根本就没有出口。所以我们通常使用“噪声”来实现一些看起来比较自然的随机,而Voronoi图就是噪声的一种。
Voronoi是一种多孔状图,通常来模拟岩石,木纹等结构。
那这种Voronoi用数学是如何实现的呢?
现在我们有两个点,如果我们以这两个点为圆心向外扩张,我们会得到一条交线:
好了,我们给它再加几个点:
实现了!这就是Voronoi 几何图案
我们可以通过他的构造直观的了解到图形的边界就是两点之间的垂直平分线,每个顶点与相邻的点的距离又是相等的。
我们用shaderlab实现一个最简单的Voronoi图:
我们的思路是,抛出一些随机的点,计算每个点和所有点之间的距离,取最小值最为他的值,然后着色,当然您也可以选择第二小的值,第三小的值,这里我们使用最小值。
第一步,我们创建一个unlitshader 文件,添加如下自定义函数,这个函数帮助我们返回一个“随机”的粒子。
float2 N22(float2 p){
float3 a = frac(p.xyx*float3(123.34,234.34,345.65));
a+=dot(a,a+100.45);
return frac(float2 (a.x*a.y ,a.y*a.z));
}
然后呢在我们的片元着色器中输入如下代码:
fixed4 frag (v2f i) : SV_Target
{
// sample the texture fixed4 col = tex2D(_MainTex, i.uv);
float m = 0;
float t = _Time;
for(float j=0; j<50 ; j++){
float2 n = N22(float2(j,j));
float2 p = sin(n*t);
p = p*0.5+0.5;
float d = length(i.uv-p);
m += smoothstep(0.02,0.01,d);
}
col *=m;
return col;
}
该代码用我们自定义的函数抛出50个粒子,然后计算点与uv之间的距离,我们把点显示的表示出来即用smoothstep函数构造一个0.01半径的圆。
我们创建一个材质,把材质赋给四边面,之后我们就得到了50个不规则运动的粒子。
我们会发现有些粒子会与闪烁的痕迹,这其实就是并行时间造成的偏差导致每一次遍历不能确保每个点都是相同的位置,所以有些像素就变黑了,造成了闪烁的现象。
好了
接下来,我们计算离该像素点最近的点的:
向片元着色器中添加代码如下:
float minDist = 100;
minDist = (d
之后我们返回窗口得到如下效果:
实现了! 一个简单的Voronoi图!
我们用一个值来记录最近的点,只需要在片元着色器中添加并修改相应代码:
float cellIndex = 0 ;
if(d
minDist = d;
cellIndex = j;
}
注意在输出的时候要缩放,控制在0-1之间:
col *=cellIndex*0.02;
这样我们可以用一个渐变黑白灰来表示各个色块
显然,我们已经注意到,这样写开销实在是太大了,如果我抛出100个粒子呢,1000个粒子呢?每个像素都要比较1000,实在是不可取的方法。
所以我们我们换一种思路:
我们用一个3x3的网格覆盖每个像素,该网格的点与像素的距离,确定当前网格的值,来当做全局的值。
我们先把UV分成10x10:
fixed4 frag (v2f i) : SV_Target
{
// sample the texture fixed4 col = tex2D(_MainTex, i.uv);
float m = 0;
float t = _Time;
float minDist = 100;
float cellIndex = 0 ;
if(false){
for(float j=0; j<50 ; j++){
float2 n = N22(float2(j,j));
float2 p = sin(n*t);
p = p*0.5+0.5;
float d = length(i.uv-p);
m += smoothstep(0.02,0.01,d);
if(d
minDist = d;
cellIndex = j;
}
}
}else{
float2 uv1 = i.uv *10 ;
float2 gv = frac(uv1);
col.rg = gv;
}
//col *=cellIndex*0.02;
return col;
}
之后返回Unity 看看我们的效果:
接着我们构造一个3x3的网格,在宏观上被分割的UV被当做独立的gv都有九个sites
然后分别比较,找出最近的点,把距离当做值,输出:
代码如下:
float2 uv1 = i.uv *10 ;
float2 gv = frac(uv1);
float2 id = floor(uv1);
//col.rg = id;
for(float x =-1 ; x<=1 ; x++){
for(float y =-1 ; y<=1 ; y++){
float2 offs = float2 (x,y);
float2 n = N22(id+offs);
float2 p = sin(n*t*5)*0.5+0.5+offs;
float d = length (gv-p);
if(d
minDist = d;
}
}
}
col *= minDist;
我们返回Unity查看效果:
看起来还不错。
好了这就是,这就是生成Voronoi图的基本步骤了,但是要注意在确定点的位置的时候要做重映射,确保值在0-1之间这样就不会有断边现象啦。
完整代码:
Shader "Custom/Voronoi"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float2 N22(float2 p){
float3 a = frac(p.xyx*float3(123.34,234.34,345.65));
a+=dot(a,a+100.45);
return frac(float2 (a.x*a.y ,a.y*a.z));
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
float m = 0;
float t = _Time;
float minDist = 100;
float cellIndex = 0 ;
if(false){
for(float j=0; j<50 ; j++){
float2 n = N22(float2(j,j));
float2 p = sin(n*t);
p = p*0.5+0.5;
float d = length(i.uv-p);
m += smoothstep(0.02,0.01,d);
if(d
minDist = d;
cellIndex = j;
}
}
}else{
float2 uv1 = i.uv *10 ;
float2 gv = frac(uv1);
float2 id = floor(uv1);
//col.rg = id;
for(float x =-1 ; x<=1 ; x++){
for(float y =-1 ; y<=1 ; y++){
float2 offs = float2 (x,y);
float2 n = N22(id+offs);
float2 p = sin(n*t*5)*0.5+0.5+offs;
float d = length (gv-p);
if(d
minDist = d;
}
}
}
col *= minDist;
}
//col *= minDist;
//col *=cellIndex*0.02;
return col;
}
ENDCG
}
}
}