一.Metaball定义
Metaball(元球)技术是由Blinn于1982年开发一种适用于建立可变形表面的技术。此技术利用Metaball建立能量场,然后通过标量域的等势面来建立3D模型来表现软体或者隐式曲面。简单的说,就是在空间里布置一些Metaball,每个Metaball都有一个能量场,通常用势函数来表示。设空间里均布着无数个点。在其中某一点,它的能量为每个Metaball对它的势的叠加。然后在空间的所有点找出势能相同的点,就得到一个由这些点组成的曲面。
二.对Metaball的理解
metaball可以理解为3D空间的等值面。通常来说, 它是3D空间里定义的一个方法:输入为(x,y,z)的坐标,输出为一个float数。输出取决于阈值,阈值之上返回1,小于阈值返回0.这样就把3D空间分成了两部分,一部分是实心的一部分是空心的。当两部分连续的表面相遇时,我们把它叫做等值面。这个等值面会看起来和其他部分不一样,即我们需要处理的就是元球连接的位置
三.2D Metaball核心算法
空间中一点的势能计算公式,先只在场景中设两个元球,A和B,A的球心为(x1,y1,z1), 能量为E1 , B为球心为(x1,y2,z2) , 能量为E2 , 计算空间点(x',y',z')势能的势函数都为:
E'(x', y', z') = E² / (x-x')² + (y-y')² + (z-z')²
这样对于每个空间点,设坐标为(x',y',z'),则它的势能可以用以下公式表示:
E'(x', y', z') = E² / (x1-x')² + (y1-y')² + (z1-z')² + E² / (x2-x')² + (y2-y')² + (z2-z')²
计算示例:
场景中有20个元球,如何绘制元球融合效果
1.计算空间点的势能
如前面所说,某一点的势能为每个Metaball对它的势的叠加,那么我们可以在片元Shader中这样计算:
//根据20个元球 计算每个点的势能
float v = 0.0;
for ( int i = 0; i < 20; i++ ) {
vec4 mb = u_metaballs[i];
float dx = st.x - mb.x;
float dy = st.y - mb.y;
float r = mb.z; //半径
v += r * r / (dx * dx + dy * dy); //按照上面给的公式叠加20个Metaball的势
}
u_metaballs是uniform变量,存放小球的位置和半径信息,由javascript传入。
注:这里因为是2D元球所以没有空间Z方向的坐标,即dz=0
2.根据阈值来绘制
vec4 color = vec4(1.0);
float rangeMax = 5.2; //最高阈值
float rangeMin = 4.7; //最低阈值
if (v > rangeMax) {
color = vec4(0.0, 0.0, 0.0, 1.0); //势能大于rangMax 则黑色
} else if (v > rangeMin) {
color = vec4(0.0, 0.0, 0.0, smoothstep(1.0, 0.0, (rangeMax - v) / (rangeMax - rangeMin))); //势能处于大小阈值之间,则设置平滑的透明度
} else { //势能小于rangeMin 则透明
color = vec4(1.0, 1.0, 1.0, 0.0);
}
这里smoothstep就实现了融合部分的处理,补充介绍一下GLSL内置函数smoothstep函数
genType smoothstep (genType edge0,genType edge1,genType x)
如果x <= edge0,返回0.0 ;如果x >= edge1 返回1.0;
如果edge0 < x < edge1,则执行0~1之间的平滑埃尔米特插值。
如果edge0 >= edge1,结果是未定义的。
下图将根据时间顺序比较 smoothstep 和 linstep 返回的值:
四.让小球动起来
动态改变Metaball的坐标即可,这里直接将dx,dy计算改成:
float dx = st.x + cos(u_time + mb.w) * mb.x; //u_time 和 w 属性变化来使小球运动
float dy = st.y + sin(u_time + mb.w) * mb.y;
那么mb的坐标会根据u_time一直发生变化,周期为2Π/mb.w,振幅为2
其中u_time由javascript传入,u_time一直在累加
五.扩展变换
由上面的一些公式
改变u_metaballs中的x,y可以改变metaball的初始位置
改变u_metaballs中的z可以改变metaball的半径,即元球的大小
改变u_metaballs中的w可以改变metaball位置变换的周期
我们还可以再将cos(u_time + mb.w) * mb.x 乘以一个系数,即可以改变变化的振幅
再通过gsap动作可以创建更多更平滑有趣的动作
例如:
//x,y位置范围变远 z增大即球半径增大 w值增大则周期变短
this.balls.forEach(ball => {
TweenLite.killTweensOf(ball);
TweenLite.to(ball, 3, {
delay: Utils.random(0, 0.5),
x: Utils.random(-0.5, 0.5),
y: Utils.random(-0.5, 0.5),
z: Utils.random(0.04, 0.3),
w: ball.x + Utils.random(4, 15),
ease: Power2.easeInOut, //'power2.inOut',
});
});