引言
纸飞机是每个人童年都玩过的经典游戏。看似简单的一掷,背后却蕴含着丰富的物理学原理:重力、空气阻力、升力与俯仰力矩共同决定了纸飞机的飞行轨迹。本文将用 Java 构建一个简化的纸飞机飞行物理模型,通过数值积分模拟飞行轨迹,并计算给定条件下的 最优投掷角度 —— 让纸飞机飞得最远、留空时间最长或飞行高度最高。
物理模型建立
对纸飞机进行受力分析,主要考虑以下作用力:
- 重力:$F_g = mg$,方向竖直向下
- 升力(Lift):$F_L = \frac{1}{2} \rho v^2 S C_L$,垂直于飞行方向向上
- 阻力(Drag):$F_D = \frac{1}{2} \rho v^2 S C_D$,与飞行方向相反
其中:
– $\rho$ 为空气密度(约 1.225 kg/m³)
– $v$ 为飞行速度
– $S$ 为机翼参考面积
– $C_L$ 为升力系数(与攻角相关,典型值 0.1~0.5)
– $C_D$ 为阻力系数(典型值 0.01~0.1)
简化假设:
– 纸飞机质量集中于重心
– 忽略侧滑与偏航,仅考虑纵向平面运动
– 升力系数与攻角呈线性关系:$C_L = C_{L0} + C_{L\alpha} \cdot \alpha$
– 俯仰力矩由重心位置自动稳定(静态稳定)
运动方程
将力分解到水平和竖直方向:
$$F_x = -F_D \cos\theta – F_L \sin\theta$$
$$F_y = F_L \cos\theta – F_D \sin\theta – mg$$
其中 $\theta$ 为飞行方向与水平面的夹角(俯仰角)。
通过牛顿第二定律得到加速度,再使用 龙格-库塔法(RK4) 进行数值积分。
/**
* 纸飞机飞行物理模拟器
* 基于简化空气动力学模型,使用RK4数值积分
*/
public class PaperAirplaneSimulator {
// 物理常数
static final double RHO = 1.225; // 空气密度 kg/m³
static final double G = 9.81; // 重力加速度 m/s²
// 纸飞机参数(以标准A4纸折成的飞机为参考)
double mass; // 质量 kg
double wingArea; // 机翼面积 m²
double wingSpan; // 翼展 m
double cl0; // 零攻角升力系数
double clAlpha; // 升力线斜率 /rad
double cd0; // 零升阻力系数
double cdAlpha; // 攻角阻力增量
public PaperAirplaneSimulator() {
// 默认参数:A4纸飞机
this.mass = 0.005; // 5克
this.wingArea = 0.015; // 150 cm²
this.wingSpan = 0.20; // 20 cm
this.cl0 = 0.15;
this.clAlpha = 2.0; // 每弧度
this.cd0 = 0.02;
this.cdAlpha = 0.5;
}
/**
* 状态向量 [x, y, vx, vy]
* x: 水平位移, y: 高度, vx: 水平速度, vy: 竖直速度
*/
static class State {
double x, y, vx, vy;
State(double x, double y, double vx, double vy) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
}
State copy() {
return new State(x, y, vx, vy);
}
}
/**
* 计算空气动力学系数
* @param alpha 攻角 (rad)
* @return [CL, CD]
*/
double[] computeAeroCoefficients(double alpha) {
double cl = cl0 + clAlpha * alpha;
double cd = cd0 + cdAlpha * alpha * alpha;
// 限制合理范围
cl = Math.max(-1.0, Math.min(1.5, cl));
cd = Math.max(0.01, Math.min(2.0, cd));
return new double[]{cl, cd};
}
/**
* 计算状态导数 d(state)/dt
*/
State derivative(State s) {
double v = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
if (v < 0.01) v = 0.01; // 避免除零
double theta = Math.atan2(s.vy, s.vx); // 飞行方向角
double alpha = 0.05; // 简化:固定小攻角(静态稳定飞机自调节)
double[] coeff = computeAeroCoefficients(alpha);
double cl = coeff[0];
double cd = coeff[1];
double dynamicPressure = 0.5 * RHO * v * v * wingArea;
double lift = dynamicPressure * cl;
double drag = dynamicPressure * cd;
// 分解到水平和竖直方向
double fx = -drag * Math.cos(theta) - lift * Math.sin(theta);
double fy = lift * Math.cos(theta) - drag * Math.sin(theta) - mass * G;
return new State(s.vx, s.vy, fx / mass, fy / mass);
}
/**
* RK4 单步积分
* @param s 当前状态
* @param dt 时间步长
* @return 下一步状态
*/
State rk4Step(State s, double dt) {
State k1 = derivative(s);
State k2 = derivative(add(s, multiply(k1, dt * 0.5)));
State k3 = derivative(add(s, multiply(k2, dt * 0.5)));
State k4 = derivative(add(s, multiply(k3, dt)));
State result = s.copy();
result.x += dt * (k1.x + 2*k2.x + 2*k3.x + k4.x) / 6.0;
result.y += dt * (k1.y + 2*k2.y + 2*k3.y + k4.y) / 6.0;
result.vx += dt * (k1.vx + 2*k2.vx + 2*k3.vx + k4.vx) / 6.0;
result.vy += dt * (k1.vy + 2*k2.vy + 2*k3.vy + k4.vy) / 6.0;
return result;
}
State add(State a, State b) {
return new State(a.x + b.x, a.y + b.y, a.vx + b.vx, a.vy + b.vy);
}
State multiply(State s, double scalar) {
return new State(s.x * scalar, s.y * scalar, s.vx * scalar, s.vy * scalar);
}
/**
* 模拟单次飞行
* @param v0 初始速度 m/s
* @param angleDeg 投掷角度(度)
* @param dt 时间步长
* @return 飞行轨迹点列表
*/
public FlightResult simulate(double v0, double angleDeg, double dt) {
double angleRad = Math.toRadians(angleDeg);
State state = new State(0, 1.5, // 从1.5米高度投掷
v0 * Math.cos(angleRad),
v0 * Math.sin(angleRad));
List<double[]> trajectory = new ArrayList<>();
trajectory.add(new double[]{state.x, state.y});
double maxHeight = state.y;
double flightTime = 0;
while (state.y >= 0 && flightTime < 30) { // 最多模拟30秒
state = rk4Step(state, dt);
flightTime += dt;
trajectory.add(new double[]{state.x, state.y});
if (state.y > maxHeight) maxHeight = state.y;
}
// 线性插值找到精确落地点
int last = trajectory.size() - 1;
double[] p1 = trajectory.get(last - 1);
double[] p2 = trajectory.get(last);
double exactDistance = p1[0] + (p2[0] - p1[0]) * p1[1] / (p1[1] - p2[1]);
return new FlightResult(exactDistance, maxHeight, flightTime, trajectory);
}
static class FlightResult {
double distance; // 水平飞行距离 m
double maxHeight; // 最大高度 m
double flightTime; // 留空时间 s
List<double[]> trajectory;
FlightResult(double d, double h, double t, List<double[]> traj) {
this.distance = d;
this.maxHeight = h;
this.flightTime = t;
this.trajectory = traj;
}
@Override
public String toString() {
return String.format("距离=%.2fm, 最大高度=%.2fm, 留空时间=%.2fs",
distance, maxHeight, flightTime);
}
}
// ========== 最优角度搜索 ==========
/**
* 三分搜索寻找最优投掷角度(最大化飞行距离)
* 假设飞行距离关于角度是单峰函数(在合理范围内)
*/
public double findOptimalAngle(double v0, double target) {
double left = 0, right = 60; // 搜索范围 [0°, 60°]
double eps = 0.01;
while (right - left > eps) {
double m1 = left + (right - left) / 3.0;
double m2 = right - (right - left) / 3.0;
double d1 = simulate(v0, m1, 0.01).distance;
double d2 = simulate(v0, m2, 0.01).distance;
if (d1 < d2) {
left = m1;
} else {
right = m2;
}
}
return (left + right) / 2.0;
}
/**
* 穷举搜索不同初始速度下的最优角度
*/
public void analyzeOptimalAngles() {
System.out.println("===== 不同初始速度下的最优投掷角度 =====");
System.out.println("速度(m/s) | 最优角度 | 飞行距离 | 最大高度 | 留空时间");
System.out.println("----------|----------|----------|----------|----------");
for (double v0 = 3.0; v0 <= 12.0; v0 += 1.0) {
double optimalAngle = findOptimalAngle(v0, 0);
FlightResult result = simulate(v0, optimalAngle, 0.01);
System.out.printf(" %.1f | %.1f° | %.2fm | %.2fm | %.2fs\n",
v0, optimalAngle, result.distance,
result.maxHeight, result.flightTime);
}
}
// ========== 可视化轨迹输出 ==========
public void printTrajectory(FlightResult result, int samples) {
List<double[]> traj = result.trajectory;
int step = Math.max(1, traj.size() / samples);
System.out.println("\n飞行轨迹采样 (x, y):");
for (int i = 0; i < traj.size(); i += step) {
double[] p = traj.get(i);
System.out.printf(" (%.2f, %.2f)\n", p[0], p[1]);
}
}
public static void main(String[] args) {
PaperAirplaneSimulator sim = new PaperAirplaneSimulator();
// 示例1: 单次飞行模拟
System.out.println("===== 纸飞机飞行模拟 =====\n");
double v0 = 6.0; // 初始速度 6 m/s
double angle = 25.0; // 投掷角度 25°
System.out.println("参数: 初始速度=" + v0 + "m/s, 投掷角度=" + angle + "°");
FlightResult result = sim.simulate(v0, angle, 0.005);
System.out.println("结果: " + result);
sim.printTrajectory(result, 10);
// 示例2: 寻找最优角度
System.out.println("\n===== 最优投掷角度搜索 =====");
double optimal = sim.findOptimalAngle(v0, 0);
System.out.println("初始速度 " + v0 + " m/s 时,最优投掷角度: " +
String.format("%.2f°", optimal));
FlightResult optimalResult = sim.simulate(v0, optimal, 0.005);
System.out.println("最优结果: " + optimalResult);
// 示例3: 不同速度下的分析
System.out.println();
sim.analyzeOptimalAngles();
// 示例4: 角度对比
System.out.println("\n===== 不同投掷角度对比 (v0=6m/s) =====");
System.out.println("角度 | 飞行距离 | 最大高度 | 留空时间");
System.out.println("-----|----------|----------|----------");
for (double a = 0; a <= 60; a += 5) {
FlightResult r = sim.simulate(6.0, a, 0.01);
System.out.printf(" %2.0f° | %6.2fm | %6.2fm | %6.2fs\n",
a, r.distance, r.maxHeight, r.flightTime);
}
}
}
无阻力理想模型对比
在真空中,抛体运动的最优角度为 45°。但在空气中,由于升力和阻力的存在,最优角度通常更小(约 25°~35°),这与我们的模拟结果一致。
/**
* 理想抛体运动(无空气阻力)计算
* 用于与真实模型对比
*/
public static double idealRange(double v0, double angleDeg, double h0) {
double theta = Math.toRadians(angleDeg);
double vx = v0 * Math.cos(theta);
double vy = v0 * Math.sin(theta);
// 解方程: h0 + vy*t - 0.5*g*t^2 = 0
double discriminant = vy * vy + 2 * G * h0;
double t = (vy + Math.sqrt(discriminant)) / G;
return vx * t;
}
模型参数影响分析
| 参数 | 增大影响 | 实际调整方式 |
|---|---|---|
| 质量 | 惯性增大,轨迹更平直 | 使用更重的纸或加配重 |
| 翼展 | 升力面积增大,滑翔比提高 | 折出更宽的机翼 |
| CL(升力系数) | 留空时间增加,距离可能减少 | 调整机翼弯度(dihedral) |
| CD(阻力系数) | 减速更快,距离减少 | 使飞机更流线型 |
| 投掷速度 | 距离和高度均增加 | 更用力投掷 |
总结
通过 Java 实现的纸飞机物理模拟,我们展示了如何将空气动力学简化为可计算的模型。数值积分(RK4)让我们能精确追踪非线性运动,而最优角度搜索则揭示了空气阻力对经典 45° 结论的修正。这一项目不仅是物理与编程的有趣结合,也是数值方法与优化算法在实际问题中的生动应用。