DEV Community

Cover image for Leading a moving target breaks once you add air drag — here's a solver that doesn't
inf
inf

Posted on

Leading a moving target breaks once you add air drag — here's a solver that doesn't

If you've ever coded a turret that "leads" a moving target, you've used the vacuum formula: predict where the target will be, aim there. It's clean, it's closed-form, and it falls apart the moment the projectile feels air drag.

There's no closed-form trajectory for quadratic drag, so the nice "solve for the lead point" algebra has nowhere to stand. Most projects respond by either ignoring drag (and missing), or hand-tuning fudge factors. I wanted something that just… hits.

So I built ballistic-solver: a small native solver that computes the launch angles to intercept a moving target under gravity, quadratic drag, and wind. MIT, v1.0, with Python, C++, C# and Godot bindings.

Here it is leading a noisy, diving target in real time (Kalman tracker + range rings + tracers + hit-rate HUD):

The idea: don't guess the formula, solve it

Instead of a closed form, the solver simulates the projectile (RK4 with drag and wind) and solves the intercept numerically.

The trick is choosing the right residual. For a candidate (elevation, azimuth), integrate the trajectory, find the time of closest approach t*, and take the 3D miss vector at t* as the residual:

r = projectile(t*) − target(t*) ∈ ℝ³

That miss vector is well-conditioned and varies smoothly with the angles, so the two launch angles fall out of a Gauss-Newton iteration on r. (Contrast with reconstructing an angle-space residual through an inverse — that's the older, fiddlier path; it's still in the library as solve_aux for reproducibility.)

Making it fast and robust

Two unknowns, a 3D residual, sub-millisecond budget. What makes it quick:

  1. Vacuum warm start. The closed-form vacuum lead is a great initial guess — exact when drag is zero, close when it's small.
  2. Analytic Jacobian seed. The initial inverse-Jacobian is finite-differenced from the closed-form vacuum-arc mapnot from the simulated trajectory. So seeding the Jacobian costs no extra RK4 integrations. This is the part I'mmost happy with.
  3. Broyden refinement. Rank-1 updates keep the Jacobian honest as it converges; full steps, no damping, no line search.
  4. Multistart fallback. If the warm start stalls, retry from a small arc-appropriate elevation grid. This is what makes the hard cases reliable.

Using it

Python (pip install ballistic-solver):

import ballistic_solver as bs

r = bs.solve(relPos0=(120, 30, 5), relVel=(2, -1, 0), v0=90, kDrag=0.002)
print(r["theta"], r["phi"], r["miss"], r["success"])
Enter fullscreen mode Exit fullscreen mode

C++ (modern surface; C++20 designated-init shown):

#include <ballistic_solver.hpp>

auto r = bs::solve({.rel_pos0 = {120, 30, 5},
                    .rel_vel  = {2, -1, 0},
                    .v0       = 90,
                    .k_drag   = 0.002});
if (r) aim(r.theta, r.phi);
Enter fullscreen mode Exit fullscreen mode

Godot (GDExtension):

var solver := BallisticSolver.new()
var r := solver.solve(rel_pos0, rel_vel, v0, k_drag, 0)  # 0 low / 1 high
if r.success:
    aim_at(r.theta, r.phi)
Enter fullscreen mode Exit fullscreen mode

There's a stable C ABI underneath for FFI from anywhere.

Being honest about it

Two things I tried hard to do straight:

Benchmarks by difficulty, not one flattering average. The grid spans easy → hard (low/high arc × drag strength). The expensive corner — high-arc plunging shots with strong drag — has a real long tail (P99 in the tens of milliseconds) where the multistart fallback does the most work. I show that tail instead of averaging it away. Everything else is sub-millisecond at 100% success on the reachable cases.

A limitations page. It's a point-mass solver: single drag coefficient (no Mach-dependent table), no spin drift, no terrain collision, and it's not a certified fire-control system. It's for games, simulation, robotics, and research.

Try it

Feedback very welcome — especially on the numerical method and where the failure modes bite in real use.

Top comments (0)