小萌新刚开始学OpenGL,想做一个三维小球碰撞模拟。一开始试了好多写法,但都有问题,不断改进,终于完成了,感觉有必要记录一下。
首先,为了能够无限添加小球,我采用链表结构,并定义了小球结构体,其中包含小球的各个物理属性。
struct ball {
glm::vec3 position; //球心坐标
glm::vec3 speed; //速度矢量
glm::vec3 color;//可有可无
float r; //小球半径
float m; //小球质量
struct ball* next;
};
在渲染循环里面加上p->position += p->speed * deltaTime;实现小球移动。deltatime为渲染的时间间隔。
然后就是简单的循环,用来筛选发生碰撞的小球。
struct ball* p = head;
struct ball* q;
while (p != NULL) {
q = p->next;
while ((q != NULL)) {
if ((veclength(p->position - q->position) <= (p->r + q->r))) {
}
q = q->next;
}
p = p->next;
}
接着最关键的就是发生碰撞的两个小球的代码了。
一开始,我尝试先在草稿纸上,把碰撞后的速度算出来。然后if他们之间的距离小于半径之和,就给他们的速度附上碰撞后的值。
然而,当我满怀期待地运行的时候,发现只有少数小球符合要求,大多数小球刚一碰撞,就直接飞走了。于是我只好回来再看代码,发现可能是因为重复判定。也就是赋值完速度之后的下一帧,他们可能还没有分离,这时候又会给速度赋值一次。
于是我添加了一个开关,当两个球分离之前,只会执行一次碰撞速度赋值。
当我运行的时候又发现,当两个球未分离的时候,如果有第三个球撞上来,那第一个球和第二个球就会发生重合。这也是不对的。
觉得这个问题过于复杂的我决定另辟蹊径。想到了一种更接近自然界本质的方法,那就是弹力。小球碰撞速度的改变,终究还是因为他们之间的相互作用力,给了他们加速度。
于是我在小球属性里面添加了加速度glm::vec3 a,并且在渲染循环里面添加了p->speed += p->a * deltaTime;当小球发生碰撞时,根据质量反比,赋给他们分离的加速度99999.0f/m。
if ((veclength(p->position - q->position) <= (p->r + q->r))) {
p->a = glm::normalize(p->position - q->position) * 999999.0f / p->m;
q->a = glm::normalize(q->position - p->position) * 999999.0f / q->m;
}
经过不断实验,我发现虽然这样解决了上述问题,但是又出现了新的问题:
1、当两个质量较小的球,即使以很慢的速度碰撞,碰撞之后速度会变得很大。
2、当多个小球竖直叠在一起时,会发生严重的弹跳。
3、同一个小球无法同时和多个小球同时碰撞
针对上述问题我又进行了改进。
对于问题1、2,是因为当两个小球分离或者接触的瞬间,和加速度改变的瞬间有误差。这是由于小球的移动终究是离散的,不是移动的。于是我想到了把恒力改为随小球距离变化的保守力。并且当小球刚接触的时候,这个力得趋于0,并且要随距离减少快速增加(防止球质量过大时吞球)。于是我选择了指数函数,A^x^2-1.具体需要自己调试。
对于问题三,是因为一开始加速度用的是=,不能叠加,于是我改成了+=。完美(我觉得)解决了上述问题。
此外,由于自然界不存在完全弹性碰撞,因此我加了一个随速度阻尼,保证熵增。
最终代码如下:
void BALLMOVE() {
struct ball* p = head;
struct ball* q;
while (p != NULL) {
p->speed += p->a * deltaTime;
p->speed += 30.0f * deltaTime * glm::vec3(0, -1, 0);//这是重力加速度
p->position += p->speed * deltaTime;
q = p->next;
p->a = glm::vec3(0, 0, 0);
while ((q != NULL)) {
if ((veclength(p->position - q->position) <= (p->r + q->r))) {
float k = fabs(veclength(p->position - q->position) - (p->r + q->r));
float kn = pow(7,k*k)-1;
p->a += kn*(glm::normalize(p->position - q->position) * 999999.0f - (p->speed - q->speed) * 100000.0f) / p->m;
q->a += kn*(glm::normalize(q->position - p->position) * 999999.0f - (q->speed - p->speed)*100000.0f) / q->m;
}
q = q->next;
}
p = p->next;
}
}
将这个函数插入到渲染循环里面,就可以实现小球碰撞啦。