继今年(虽然离2015年只有一天了但还是2014)暑假简单接触了一些flocking 集群的概念后一直没有完完整整的把这个算法里面的一些行为规则做一些归类和实现。最近一直还在深入的学习Houdini里面的VEX语言,这里简单讲一讲我的一些学习过程,并附上在Houdini里面实现的方法和源码。
集群模拟中每个个体的行为规则(前 1 - 8 是已经实现了的,后面的在游戏领域拥有应用,这里只提一下)。
-
seek 寻找
-
flee 逃跑
-
wander 随意行走
-
separation 分离
-
cohesion 聚集
-
alignment 方向对齐
-
obstacle avoidance 躲避障碍
-
follow (path following, leader following, wall following )
Seek 寻找 与 Flee 逃跑:
寻找 是集群模拟里面最基本的行为之一,其实算法也非常简单,给定一个或者多个目标点,让物体自行确定自己唯一的目标点后,根据两点之间的位置和距离来确定寻找行为的这个力大小和方向,这里我们称之为seek_force,之后的都会是以 *_force 来命名。
(本人太懒了,直接用的学习的文献里面的图片来解释了)
用目标点减去物体现在所在位置得出来的向量方向是我们希望物体能够拥有的方向,但是我们不能直接在下一帧就把物体变成那个方向,这样会使得物体转向特别的突兀,所以一般是用希望方向减去目前方向然后除step变成当前这一帧物体受到的seek_force力。这里希望方向上的力的大小取决于物体与目标点的距离远近,越近这个力就会越小因为快到了我们就不需要再拼命加速了,反之则越大。
flee_force 的运算性质与seek_force性质一样只是希望逃离的方向与希望寻找方向刚好取反。
Wander 随意行走:
这个行为规则相对来说稍微复杂了一点,主要的问题是随意行走不是随机行走,如果随意行走的力在方向上完全没有连续性而是随机控制的话,那物体的行走感觉就像是粒子在随机抖动。好比一个人随意向前走,他下一步要改也只是向前的方向随意调整一点点角度,而不是立刻180度转变。
这里有一个小技巧能够解决这个问题,在物体运动方向前延伸一定距离的位置为圆心点随机取一个半径较小的表面坐标点,物体当前所在位置到改点的矢量方向便是下一步的wander_force方向了。
Separation 分离,Cohesion 聚集,alignment 方向对齐:
这三个力摆在一起来讲,主要是因为他们都是物体与物体之间的作用力关系。
Separation 的算法是求得物体一定半径范围内的其他临近物体的位置,并用当前物体位置分别减去临近物体位置,用这个方向上的单位向量乘以根据距离远近算出来的权重,越近权重越大。再把所有的向量相加除以临近物体的数量便得到了separation_force的向量方向。
Cohesion 是模拟群体之间总是保持相对距离的关系,这里不是说要保持距离而是说千万别掉队。求得物体一定半径范围内的其他临近物体的所有位置,把所有位置相加除以邻居数量得到平均位置,再用这个位置减去物体当前所在位置便是cohesion_force的向量方向了,这里没有考虑邻居与当前物体距离关系的权重影响。
Alignment 模拟的是让每一个物体的运动能够随着附近临近物体的大方向保持一致,如果这个分量力权重变大的话,能导致群体有大范围方向一致的运动的效果。算法是求得物体一定半径范围内的其他临近物体的所有运动速度方向及大小,把所有速度取平均值得到Alignment_force的向量方向。
上图从左到右分别是 separation,cohesion,alignment
Obstacle Avoidance 躲避障碍:
最后想讲的一个分力是Obstacle Avoidance,这个力是所有求得的作用力里面相对最复杂的。主要的行为规则是物体检测是否在一定范围内出现了障碍物,如果有的话,计算与障碍物可能的碰撞点。并用碰撞点到物体的方向向量与碰撞点所在面的法相向量进行叉乘 cross product。所得的向量便是物体躲避障碍的力的方向。
这里讲一讲本人实现这个分力的两个方法,他们的不同之处在于如何得到可能的碰撞点。第一个方法是用物体的速度方向向量映射到物体上(在houdini vex环境里面有个函数是intersect()来处理可能碰撞位置)来确定可能的碰撞点,这样的出来的躲避向量比较符合常理,因为你只有看到障碍物才会采取躲避的措施,但是这个方法有一个不方便的地方在于如果障碍物与物体运动方向大致相同或者垂直的话,物体是不会检测到这个障碍物,在Houdini里面则还是需要另外计算实际的物理碰撞来防止障碍物从物体背后靠近。第二个方法则是比较有欺骗性的,但是效果也不差,我们直接用障碍物中心点减去物体位置得到检测碰撞的向量,只要使用一个半径来做判定这个向量不论怎样都会与障碍物产生交叉点。
(这个我自己画的,超丑)
在Houdini里面的应用:
使用到的分量力解释了这么多,我们再来看看在Houdini里面使用的方法。
我建立了一个geo 下面的digital asset,把所有的算法写在了这个里面。这个自定义的node有三个入口,第一个给要进行模拟的群体目标(默认是只接受点),第二个入口给目标点,这里的目标点可以是一个也可以是很多个,第三个入口给封闭的几何体。
最后把这个自定义的节点放入sop下面的solver让函数能够随着帧数变化而变化。
这里是对我算法实现帮助非常大的两份参考,非常有用:
1,Understanding Steering Behaviors
2,Steering Behaviors For Autonomous Characters
最后不废话了,这是该节点的源代码,希望废话这么多能够对你有些些帮助。
#define dao 6.28318530718
#pragma label max_force "Max Factor Force"
#pragma label slow_radius "Seek Slow Radius"
#pragma label acc_weight "Acceleration weight"
#pragma label pc_radius "Neighbours Radius"
#pragma label pc_ptnum "Neighbours number"
#pragma label wander_max "Wander Max Velocity"
#pragma label sep_weight "Seperation Weight"
#pragma label co_weight "Cohesion Weight"
#pragma label ali_weight "Alignment Weight"
#pragma hint avoid_center invisible
//get target
vector gettarget(int target_handle){
vector target;
while(pciterate(target_handle)){
pcimport(target_handle, "P", target);
}
return target;
}
// seek + flee + cohesion + seperation
vector seek(vector target; float slow_radius; float max_force;){
float seek_power = fit(length(target - P), 0, slow_radius, 0, 1) * max_force;
vector seek_force = seek_power * normalize(target - P);
return seek_force;
}
vector flee(vector target; float slow_radius; float max_force;){
float flee_power = -fit(length(target - P), 0, slow_radius*1.5, 1, 0) * max_force;
vector flee_force = flee_power * normalize(target - P);
return flee_force;
}
vector wander(vector max_force; float wander_max;){
//wander force
//to constrain the steering force to the surface of a sphere located slightly ahead of the character
//any point on the sphere: 0 < theta < dao ; 0 < beta < dao/2
// x = r * cos theta * sin beta
// y = r * sin theta * sin beta
// z = r * cos beta
float theta, beta;
theta = fit01(noise(P*37), 0, dao);
beta = fit01(noise(P*10+234), 0, dao/2);
vector displacement = set(sin(theta) * cos(beta), sin(theta) * sin(beta), cos(beta));
vector wander_dir = normalize(2 * normalize(v) + displacement) * wander_max;
return wander_dir; // * fit(length(v), 0, max_force, 1, 0) ;
}
vector cohesion(int self_handle; float pc_radius; float co_weight){
//Cohesion
vector co_target = set(0,0,0);
while(pciterate(self_handle)){
vector current_point;
pcimport(self_handle, "P", current_point);
co_target += current_point;
}
vector co_force = normalize(co_target - P) * pow(fit(length(co_target - P), 0, pc_radius, 1, 0), 2) * co_weight;
return co_force;
}
vector seperation(int self_handle; float pc_radius; float sep_weight){
//Seperation
vector sep_force = set(0,0,0);
while(pciterate(self_handle)){
vector current_point ;
pcimport(self_handle, "P", current_point);
sep_force += normalize(P - current_point) * pow(fit(length(current_point - P), 0, pc_radius, 1, 0), 2);
}
sep_force = normalize(sep_force) * sep_weight;
return sep_force;
}
vector alignment(int self_handle; float pc_radius; float ali_weight;){
//Alignment
vector average_dirction = set(0,0,0);
while(pciterate(self_handle)){
vector current_velocity;
vector current_point;
pcimport(self_handle, "v", current_velocity);
pcimport(self_handle, "P", current_point);
average_dirction += normalize(current_velocity) * pow(fit(length(current_point - P), 0, pc_radius, 1, 0), 2);
}
average_dirction = normalize(average_dirction) * ali_weight;
return average_dirction;
}
vector obstacle_avoidance(vector avoid_center; float avoid_radius; float avoid_weight;){
vector hit_position;
float p_u, p_v;
int hit_prim = intersect(2, P, normalize(avoid_center - P) * avoid_radius, hit_position, p_u, p_v);
if(hit_prim != -1){
vector turn_force, turn_direction, second_direction;
second_direction = prim_normal(2, hit_prim, p_u, p_v);
turn_direction = cross((hit_position - P), second_direction);
turn_force = normalize(turn_direction) * fit(length(hit_position - P), 0, avoid_radius*10, 1, 0) * avoid_weight;
return turn_force;
}else{return 0;}
}
sop
steering(
// max velocity
float max_force = 1.0;
// the area radius around target,
//when charactor get in this area, the the seek_power will drop down
float slow_radius = 1.0;
//acceleration scale for all charactors
float acc_weight = 0.3;
//point cloud radius
float pc_radius = 0.5;
//point cloud point number
int pc_ptnum = 10;
//wander max
float wander_max = 0.02;
//seperation weight
float sep_weight = 0.2;
//cohesion weight
float co_weight = 1.0;
//alignment weight
float ali_weight = 1.0;
//obstacle avoidance weight
float avoid_weight = 1.0;
//obstacle avoidance radius
float avoid_radius = 1.0;
// avoid target centroid
export vector avoid_center = 0;
)
{
// get the target location
vector target;
int target_handle;
target_handle = pcopen(1, "P", P, 99999, 1);
target = gettarget(target_handle);
// open the point cloud
int self_handle;
self_handle = pcopen(geoself(), "P", P, pc_radius, pc_ptnum);
// desired_v is the force that guides the character towards its target using the shortest path possible
// we add wander_force as an extra factor that will effect desired_v
vector desired_v;
vector steering;
//define every factor forces
vector seek_force;
vector flee_force;
vector wander_force;
vector co_force;
vector sep_force;
vector ali_force;
vector avoid_force;
//get each forces
seek_force = seek(target, slow_radius, max_force);
flee_force = flee(target, slow_radius, max_force);
wander_force = wander(max_force, wander_max);
co_force = cohesion(self_handle, pc_radius, co_weight);
sep_force = seperation(self_handle, pc_radius, sep_weight);
ali_force = alignment(self_handle, pc_radius, ali_weight);
avoid_force = obstacle_avoidance(avoid_center, avoid_radius, avoid_weight);
// desired_v = seek force + flee force + wander force + seperation force
desired_v = seek_force + flee_force + wander_force + co_force + sep_force + ali_force + avoid_force;
steering = desired_v - v;
steering = steering * acc_weight;
v += steering;
P += v;
}