<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: inf</title>
    <description>The latest articles on DEV Community by inf (@ujinf74).</description>
    <link>https://dev.to/ujinf74</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3767199%2F5a10c253-4b6c-4183-883e-5bac6495b828.png</url>
      <title>DEV Community: inf</title>
      <link>https://dev.to/ujinf74</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ujinf74"/>
    <language>en</language>
    <item>
      <title>Leading a moving target breaks once you add air drag — here's a solver that doesn't</title>
      <dc:creator>inf</dc:creator>
      <pubDate>Wed, 17 Jun 2026 15:00:57 +0000</pubDate>
      <link>https://dev.to/ujinf74/leading-a-moving-target-breaks-once-you-add-air-drag-heres-a-solver-that-doesnt-39m</link>
      <guid>https://dev.to/ujinf74/leading-a-moving-target-breaks-once-you-add-air-drag-heres-a-solver-that-doesnt-39m</guid>
      <description>&lt;p&gt;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 &lt;strong&gt;air drag&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/ujinf74/ballistic-solver" rel="noopener noreferrer"&gt;&lt;strong&gt;ballistic-solver&lt;/strong&gt;&lt;/a&gt;: 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.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop76jvam4zkl3lle3fle.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop76jvam4zkl3lle3fle.gif" alt=" " width="759" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: don't guess the formula, solve it
&lt;/h2&gt;

&lt;p&gt;Instead of a closed form, the solver &lt;strong&gt;simulates the projectile&lt;/strong&gt; (RK4 with drag and wind) and &lt;strong&gt;solves the intercept numerically&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;r = projectile(t*) − target(t*)        ∈ ℝ³&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;r&lt;/code&gt;. (Contrast with reconstructing an angle-space residual through an inverse — that's the older, fiddlier path; it's still in the library as &lt;code&gt;solve_aux&lt;/code&gt; for reproducibility.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it fast &lt;em&gt;and&lt;/em&gt; robust
&lt;/h2&gt;

&lt;p&gt;Two unknowns, a 3D residual, sub-millisecond budget. What makes it quick:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;pip install ballistic-solver&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ballistic_solver&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relPos0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;relVel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kDrag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;theta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;miss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C++&lt;/strong&gt; (modern surface; C++20 designated-init shown):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;ballistic_solver.hpp&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;({.&lt;/span&gt;&lt;span class="n"&gt;rel_pos0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rel_vel&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v0&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;k_drag&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;aim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Godot&lt;/strong&gt; (GDExtension):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;solver&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;BallisticSolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;solver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel_pos0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_drag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;aim_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a stable C ABI underneath for FFI from anywhere.&lt;/p&gt;

&lt;p&gt;Being honest about it&lt;/p&gt;

&lt;p&gt;Two things I tried hard to do straight:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Try it&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ujinf74/ballistic-solver" rel="noopener noreferrer"&gt;https://github.com/ujinf74/ballistic-solver&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pip install ballistic-solver&lt;/li&gt;
&lt;li&gt;Numerical method and limitations are documented in docs/.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback very welcome — especially on the numerical method and where the failure modes bite in real use.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>python</category>
      <category>showdev</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
