← all posts

Titancraft, day twelve: an APFSDS round computed in 50-nanosecond steps

Day twelve of Titancraft. The game got a next-era gun: a 120 mm smoothbore firing an armour-piercing fin-stabilized discarding-sabot round (APFSDS). The brief was “full simulation”: sabot separation, rod spin, penetration of a homogeneous armour plate, not a ready-made hit table. This entry is about the formulas behind a single shot, and why the penetration step is 50 nanoseconds.

The gunner and the ballistic computer

The player steps up to the gun and moves the reticle. A laser rangefinder measures the range along the reticle ray, to the plate, the ground or water, up to 5 km. The ballistic computer (Apfsds.solve_elevation) raises the barrel so the rod passes through the reticle point at that range. It flies a point mass with the same air drag and corrects the angle until the miss is under 2 mm.


Gunner's sight at ×24, range 300 m by laser; side cutaway camera on the left, target camera on the right
Sight at ×24: the laser reads 300 m, cutaway on the left, target camera on the right

Neither the trajectory nor the barrel angle goes over the network. The server sends three things: where the gun is, where the plate is, and the shot's seed. Every machine computes the shot itself, and randomness comes only from a hash of the seed, never from the engine's random generator. So for every player the rod takes the same path and punches the same hole.

One shot, four time steps

The whole shot is computed at once, the moment you fire: flight, sabot, penetration or ricochet. That takes about 0.1 s of CPU. The shot's history is recorded over time, and the replay only reads the record: live, with flight cameras, or slowed ×80 at the plate.

Inside one shot things happen at very different speeds, so each stage has its own step.

The replay is recorded less often than it is computed: the rod every 10 µs during sabot separation, every 200 µs in flight, penetration every 2 µs.

The sabot: three petals

The rod is thinner than the bore, so a sabot of three aluminium petals, 1.15 kg each, carries it. Each petal is a rigid body in its own plane. Each lets go of the rod at its own moment, 220 ± 60 µs. The oncoming flow pushes on the petal's front “scoop” with ½·ρ·v²·S and turns it outwards.


Flight camera at the muzzle: three sabot petals peel away from the rod, 5 ms after firing
5 ms after firing: petals 0.22 m off the axis

While a petal holds on, its force goes into the rod, sideways and as a turn about a 5 cm lever. Whichever petal lets go first pushes the rod its way. So sabot-induced yaw comes out on its own, with no dispersion table. Then the petals tumble, because pressure on the scoop ahead of the centre of mass makes them unstable, and fall to the ground.

Flight: drag, yaw, spin

The rod is a 17,600 kg/m³ tungsten alloy: 740 mm long, 22 mm across, 4.95 kg, leaving the muzzle at 1650 m/s. Drag grows with yaw ψ, the angle between the nose and the velocity:

F = ½·ρ·v²·S·Cd·(1 + 12·ψ²),   Cd = 0.78

Yaw is a damped oscillator in two transverse axes. Its frequency grows with speed, one wave per 70 m of travel, and the amplitude falls by e every 260 m:

ψ'' = −ω²·ψ − 2k·ψ' + sabot push,   ω = 2π·v / 70 m,   k = v / 260 m

The initial disturbance comes from muzzle gases: up to 0.15°. The fins are canted 0.5°, and the rod spins up towards the equilibrium ω = v·tan(0.5°) / R_fin, reaching 50 rev/s by 20 ms.


Flight camera alongside the rod: fins and the shock cone, 20 ms after firing
20 ms: 1647 m/s, 50 rev/s, yaw 0.14°

Tate–Alekseevskii penetration

The model is hydrodynamic: at these speeds both the rod and the armour at the bottom of the channel flow like fluids with strength. The bottom moves into the armour at a speed u found from the pressure balance:

½·ρr·(v − u)² + Yr = ½·ρt·u² + Rt

The rod's strength is 1.8 GPa, the armour's penetration resistance 5.5 GPa, its density 7850 kg/m³. The equation is quadratic in u. Every 50 ns step the nose erodes, the tail decelerates and the channel grows:

tail -= (v - u) * STEP_PIERCE                                    # the nose erodes
v -= ROD_STRENGTH / (ROD_DENSITY * tail) * STEP_PIERCE           # the tail decelerates
hole += u * STEP_PIERCE                                          # the channel grows


Plate camera: the rod enters a 200 mm plate, channel at 88 mm, bottom moving at 782 m/s
Channel 88 of 200 mm: the bottom moves at 782 m/s, 647 mm of rod left

In this frame the rod moves at 1615 m/s and the channel bottom at 782 m/s. So each step erodes ~42 µm of rod, a five-hundredth of its diameter. 2 diameters before the rear face the armour starts to give way: its resistance falls smoothly to zero and the plug is punched out.

Why 50 ns exactly: we ran the same model with other steps, 200 mm plate, 1631 m/s at the plate:

A 5 ns step gives the same numbers to a tenth of a millimetre, with 10 times as many steps. A 5 µs step is off by just 2 mm, but leaves 50 steps for the whole penetration, and the slow-motion replay has nothing to show. 50 ns is the coarsest step at which the replay (every 2 µs) runs smoothly and the numbers match the fine step.

If the armour is thicker than the rod can go, the channel ends where the rod has eroded away, and the rear face stays intact.


A 700 mm plate seen through: the channel ends at 644 mm, 1 mm of rod left
700 mm plate: channel 644 mm, 1 mm of rod left

Fins wider than the hole

The fin span is 90 mm, the entry hole 37 mm. The fins do not fit into the channel: they hit the front face when the tail reaches it. At 300 m that happens 0.46 ms after the nose strikes. The fins are sheared off and melted, and six slots are left on the face, matching the rod's roll at that moment.


Fins strike the front face: flash, molten spray, six slots around the entry
Fins hit the face at 1591 m/s, sheared off and melted

Ricochet: Tate's threshold

The critical impact angle comes from Tate's formula for a rod:

tan³β = ⅔ · (ρr·v² / Y) · (L² + D²) / (L·D)

For the armour strength Y we take the same penetration resistance of 5.5 GPa: one armour constant for both effects. The threshold comes out at 80° at 1620 m/s, which is what modern rods show on firing ranges.

Above the threshold the rod does not enter, it slides. That is a planar rigid-body problem with a 200 ns step: the nose and the tail below the face push on the armour and plough a groove, the ridge of metal in front pushes them outwards at 40°, and a couple of forces turns the rod away from the plate. Once it leaves, we check bending against a 2.5 GPa strength and break it into pieces.

A 200 mm plate at 85° from 300 m: a gouge 1153 mm long and 13 mm deep, and the rod leaves at 1525 m/s in three pieces.

Ricochet at 85°: the rod lies in the gouge, nose worn down to 703 mm
Ricochet at 85°: the rod lies in the gouge, nose worn down to 703 mm
The tail slaps the plate and the rod leaves, the gouge still glowing
The tail slaps the plate and the rod leaves, the gouge still glowing

Ribs, like the Strv-103B

To “torture the physics”, we put ribs on the front face: 40 mm high every 180 mm. A rib is the same armour. Through a rib the nose erodes by Tate, and across the axis the rib pushes with a share of the armour's resistance over the rod's cross-section.

The rod is a free beam with a force at its end: the peak bending 4·F·L/27 is a third of the length from the nose. If it breaks, the nose fragment enters the plate first along the same channel but loses part of its penetration to tumbling.

A 120 mm plate at 78°: 568 mm of armour along the path. The rib pushes 2044 kN sideways and snaps the nose 246 mm from the tip. The rest still exits the back at 1398 m/s.


120 mm plate at 78° with ribs: the result, nose broken on a rib, plate pierced
120 mm plate at 78°: the rib pushes 2044 kN sideways and snaps the nose (246 mm), the plate is still pierced, 125 mm of rod left

37 centimetres from 32 bits

On a range 3 km from the world centre, the gun started missing by 37 cm sideways. In Godot Vector3 is 32-bit: that far out a coordinate step is ~0.24 mm, and sabot separation takes 6000 steps of 3.3 mm. The rounding added up to a miss.

Now flight, petals and aiming are computed relative to the muzzle and converted to world coordinates only when recorded. The miss dropped to 5 mm.

The duel and the laser warning

For fun we added a duel with a bot: two guns 1500 m apart, 200 mm shields, 10 rounds each. The bot uses the same gun and the same shot computation: each machine works out the result itself. When the enemy lases us, the laser warning lights up the sector it shines from and beeps. After that the bot hurries.

Laser warning: the enemy is lasing us, a sector lights up
Laser warning: the enemy is lasing us, a sector lights up
Our rod from 1492 m goes through the 200 mm shield: enemy gun knocked out
Our rod from 1492 m goes through the 200 mm shield: enemy gun knocked out

While the gunner watches their shot through flight cameras, the duel does not pause: its clock runs with the replay. So “both knocked out” is possible: the enemy got its shot off while our rod was in flight.

What is left in the game

  • The rod and the rangefinder will start hitting trees and buildings in their path.
  • A second hit in an old hole will go through already weakened armour.
  • Rib strikes and ricochet gouges will get a full impact calculation instead of the beam and groove-volume model.
  • Sabot petals will fly apart as fast as in footage of real shots.
  • New rounds, HEAT and HE-frag, and spaced armour.

Need physics that is computed honestly?
Start with the brief: four questions and an honest answer on whether we are a good fit.
Take the brief