算法背景
弹性碰撞是经典力学中的核心概念,广泛应用于游戏物理引擎、分子动力学模拟和工程碰撞分析中。当两个物体发生碰撞且总动能守恒时,称为完全弹性碰撞。通过动量守恒定律和能量守恒定律,可以精确计算碰撞后的速度。
核心物理定律
动量守恒定律
系统总动量在碰撞前后保持不变:
m1 * v1 + m2 * v2 = m1 * v1' + m2 * v2'
能量守恒定律(完全弹性碰撞)
系统总动能在碰撞前后保持不变:
0.5 * m1 * |v1|^2 + 0.5 * m2 * |v2|^2 = 0.5 * m1 * |v1'|^2 + 0.5 * m2 * |v2'|^2
一维弹性碰撞解
联立上述两方程,可得:
v1' = ((m1 - m2) * v1 + 2 * m2 * v2) / (m1 + m2)
v2' = ((m2 - m1) * v2 + 2 * m1 * v1) / (m1 + m2)
二维碰撞分解
二维碰撞的处理关键在于将速度分解为沿碰撞法线方向和切线方向的分量:
- 切线方向:速度分量不变(无摩擦力假设)
- 法线方向:按一维弹性碰撞公式计算
速度分解公式:
v_normal = v · n (法线分量)
v_tangent = v · t (切线分量,t为切线单位向量)
Java完整实现
import java.util.ArrayList;
import java.util.List;
/**
* 二维弹性碰撞模拟系统
* 支持多物体碰撞检测与响应
*/
public class ElasticCollision {
// 二维向量
public static class Vec2 {
public double x, y;
public Vec2(double x, double y) {
this.x = x;
this.y = y;
}
public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
public Vec2 multiply(double s) { return new Vec2(x * s, y * s); }
public double dot(Vec2 v) { return x * v.x + y * v.y; }
public double length() { return Math.sqrt(dot(this)); }
public Vec2 normalize() {
double len = length();
return len > 0 ? multiply(1.0 / len) : new Vec2(0, 0);
}
@Override
public String toString() {
return String.format("(%.3f, %.3f)", x, y);
}
}
// 刚体球
public static class Body {
public Vec2 position; // 位置
public Vec2 velocity; // 速度
public double mass; // 质量
public double radius; // 半径
public int color; // 颜色标识
public Body(Vec2 pos, Vec2 vel, double mass, double radius, int color) {
this.position = pos;
this.velocity = vel;
this.mass = mass;
this.radius = radius;
this.color = color;
}
// 动能
public double kineticEnergy() {
double v = velocity.length();
return 0.5 * mass * v * v;
}
// 动量
public Vec2 momentum() {
return velocity.multiply(mass);
}
}
// 碰撞信息
public static class Collision {
public Body bodyA, bodyB;
public Vec2 normal; // 碰撞法线(从A指向B)
public double penetration; // 穿透深度
public Collision(Body a, Body b, Vec2 normal, double penetration) {
this.bodyA = a;
this.bodyB = b;
this.normal = normal;
this.penetration = penetration;
}
}
// 模拟世界
public static class World {
public List<Body> bodies = new ArrayList<>();
public Vec2 gravity = new Vec2(0, -9.8);
public double restitution = 1.0; // 弹性系数:1.0为完全弹性
public double width = 10.0; // 边界宽度
public double height = 10.0; // 边界高度
public void addBody(Body body) {
bodies.add(body);
}
/**
* 检测两个球体是否碰撞
*/
public Collision detectCollision(Body a, Body b) {
Vec2 delta = b.position.sub(a.position);
double distance = delta.length();
double minDistance = a.radius + b.radius;
if (distance >= minDistance || distance < 0.0001) {
return null;
}
Vec2 normal = delta.normalize();
double penetration = minDistance - distance;
return new Collision(a, b, normal, penetration);
}
/**
* 求解碰撞响应(冲量法)
*/
public void resolveCollision(Collision col) {
Body a = col.bodyA;
Body b = col.bodyB;
Vec2 normal = col.normal;
// 相对速度
Vec2 relativeVel = b.velocity.sub(a.velocity);
double velAlongNormal = relativeVel.dot(normal);
// 若物体正在分离,不处理
if (velAlongNormal > 0) return;
// 冲量大小
double j = -(1.0 + restitution) * velAlongNormal;
j /= (1.0 / a.mass + 1.0 / b.mass);
// 应用冲量
Vec2 impulse = normal.multiply(j);
a.velocity = a.velocity.sub(impulse.multiply(1.0 / a.mass));
b.velocity = b.velocity.add(impulse.multiply(1.0 / b.mass));
// 位置校正(防止穿透累积)
double percent = 0.8; // 校正比例
Vec2 correction = normal.multiply(col.penetration / (1.0 / a.mass + 1.0 / b.mass) * percent);
a.position = a.position.sub(correction.multiply(1.0 / a.mass));
b.position = b.position.add(correction.multiply(1.0 / b.mass));
}
/**
* 边界碰撞处理
*/
public void handleBoundary(Body body) {
double left = body.radius;
double right = width - body.radius;
double bottom = body.radius;
double top = height - body.radius;
// 左右边界
if (body.position.x < left) {
body.position.x = left;
body.velocity.x = -body.velocity.x * restitution;
} else if (body.position.x > right) {
body.position.x = right;
body.velocity.x = -body.velocity.x * restitution;
}
// 上下边界
if (body.position.y < bottom) {
body.position.y = bottom;
body.velocity.y = -body.velocity.y * restitution;
} else if (body.position.y > top) {
body.position.y = top;
body.velocity.y = -body.velocity.y * restitution;
}
}
/**
* 单步物理更新
* @param dt 时间步长
*/
public void step(double dt) {
// 1. 应用重力
for (Body body : bodies) {
body.velocity = body.velocity.add(gravity.multiply(dt));
}
// 2. 更新位置
for (Body body : bodies) {
body.position = body.position.add(body.velocity.multiply(dt));
}
// 3. 碰撞检测与响应(迭代多次提高稳定性)
for (int iter = 0; iter < 3; iter++) {
for (int i = 0; i < bodies.size(); i++) {
for (int j = i + 1; j < bodies.size(); j++) {
Collision col = detectCollision(bodies.get(i), bodies.get(j));
if (col != null) {
resolveCollision(col);
}
}
}
}
// 4. 边界处理
for (Body body : bodies) {
handleBoundary(body);
}
}
/**
* 计算系统总动能
*/
public double totalKineticEnergy() {
double total = 0;
for (Body body : bodies) {
total += body.kineticEnergy();
}
return total;
}
/**
* 计算系统总动量
*/
public Vec2 totalMomentum() {
Vec2 total = new Vec2(0, 0);
for (Body body : bodies) {
total = total.add(body.momentum());
}
return total;
}
}
/**
* 打印场景状态
*/
public static void printScene(World world, int step) {
System.out.println("\n=== Step " + step + " ===");
System.out.println("总动能: " + String.format("%.4f", world.totalKineticEnergy()));
System.out.println("总动量: " + world.totalMomentum());
for (int i = 0; i < world.bodies.size(); i++) {
Body b = world.bodies.get(i);
System.out.println("Body " + i + ": pos=" + b.position +
", vel=" + b.velocity +
", KE=" + String.format("%.4f", b.kineticEnergy()));
}
}
public static void main(String[] args) {
World world = new World();
world.restitution = 1.0; // 完全弹性碰撞
world.width = 20.0;
world.height = 15.0;
// 场景1:对心碰撞(验证动量守恒)
System.out.println("=== 场景1:对心弹性碰撞 ===");
world.bodies.clear();
world.addBody(new Body(new Vec2(5, 7.5), new Vec2(3, 0), 2.0, 0.5, 1));
world.addBody(new Body(new Vec2(15, 7.5), new Vec2(-1, 0), 1.0, 0.5, 2));
System.out.println("碰撞前总动量: " + world.totalMomentum());
System.out.println("碰撞前总动能: " + world.totalKineticEnergy());
for (int i = 0; i < 50; i++) {
world.step(0.02);
if (i % 10 == 0) printScene(world, i);
}
System.out.println("\n碰撞后总动量: " + world.totalMomentum());
System.out.println("碰撞后总动能: " + world.totalKineticEnergy());
// 场景2:斜碰(二维碰撞分解验证)
System.out.println("\n\n=== 场景2:斜碰场景 ===");
world.bodies.clear();
world.addBody(new Body(new Vec2(8, 8), new Vec2(2, 1), 3.0, 0.6, 1));
world.addBody(new Body(new Vec2(12, 6), new Vec2(-1, 2), 2.0, 0.6, 2));
for (int i = 0; i < 100; i++) {
world.step(0.02);
if (i % 20 == 0) printScene(world, i);
}
// 场景3:多体混合碰撞
System.out.println("\n\n=== 场景3:多体混合碰撞 ===");
world.bodies.clear();
world.addBody(new Body(new Vec2(5, 5), new Vec2(2, 1.5), 1.0, 0.4, 1));
world.addBody(new Body(new Vec2(10, 7), new Vec2(-1.5, -1), 1.5, 0.4, 2));
world.addBody(new Body(new Vec2(15, 5), new Vec2(-2, 1), 1.0, 0.4, 3));
world.addBody(new Body(new Vec2(8, 12), new Vec2(1, -2), 0.8, 0.4, 4));
world.addBody(new Body(new Vec2(12, 3), new Vec2(-0.5, 2), 1.2, 0.4, 5));
for (int i = 0; i < 200; i++) {
world.step(0.016);
}
printScene(world, 200);
}
}
碰撞响应推导详解
一维弹性碰撞公式推导
从动量守恒和能量守恒出发:
m1*v1 + m2*v2 = m1*v1' + m2*v2' (1)
m1*v1^2 + m2*v2^2 = m1*v1'^2 + m2*v2'^2 (2)
由(1)得:m1*(v1 - v1') = m2*(v2' - v2)
由(2)因式分解得:m1*(v1 - v1')*(v1 + v1') = m2*(v2' - v2)*(v2' + v2)
两式相除得:v1 + v1' = v2 + v2'
联立求解即得最终公式。
二维碰撞速度分解
设碰撞法线为 n,切线为 t(与 n 垂直):
v1_normal = v1 · n
v1_tangent = v1 · t
v2_normal = v2 · n
v2_tangent = v2 · t
切线方向速度不变:v1_tangent' = v1_tangent,v2_tangent' = v2_tangent
法线方向按一维弹性碰撞处理,然后将结果合成回二维向量。
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 碰撞检测 | O(n²) | n个物体两两检测 |
| 碰撞响应 | O(c) | c为碰撞对数,c ≤ n(n-1)/2 |
| 空间复杂度 | O(n) | 存储物体状态 |
优化策略
- 空间哈希:将空间划分为网格,只检测相邻网格内的物体
- 四叉树:递归划分空间,实现O(n log n)的碰撞检测
- Sweep and Prune:沿坐标轴排序后检测重叠区间
非完全弹性碰撞
实际游戏中通常使用恢复系数 e(0 ≤ e ≤ 1):
v2' - v1' = e * (v1 - v2)
e = 1:完全弹性碰撞(台球)e = 0:完全非弹性碰撞(泥球碰撞后粘在一起)0 < e < 1:部分弹性碰撞(大多数实际情况)
扩展应用
- 台球游戏:精确碰撞模拟与旋转效应
- 粒子系统:烟雾、火花等大量微小粒子的碰撞
- 分子动力学:模拟气体、液体分子运动
- 刚体物理:扩展到多边形碰撞与角动量守恒
总结
弹性碰撞算法通过动量守恒和能量守恒两大物理定律,精确计算碰撞后的物体运动状态。二维碰撞的关键在于将速度正确分解为法线和切线分量。冲量法配合位置校正能有效处理数值误差累积。该算法是2D/3D物理引擎碰撞模块的核心基础。