TERMINAL·DESTINY PLAY ▸
← ALL POSTS

Four rain bugs in four days, and all of them were one pixel wide

BUILD NOTES31 August 2026 · 7 min read

The first rain in TERMINAL DESTINY wasn't a particle system. It was five cylinders wrapped around the camera with a scrolling streak texture painted on them.

That sounds like a hack and it's actually the considered choice. Phone GPUs are tile-based and fill-rate bound, and alpha blending defeats the early-Z pass that normally saves them work — so a screen-filling emitter is precisely the worst shape you can hand the bottleneck, and its cost swings with where you point the camera. Five shells cost five draw calls, always, no matter how hard it rains. Measured at 1440×810: +5 meshes and 60 → 53 fps on desktop, a flat 60 on the low tier with its single shell.

Then I tried real particles behind a ?raintech flag to A/B on a device — and that's where the four days start.

Day one: it was fine here and invisible there

Reported from an S22: frame rate strong, rain barely visible.

Not taste — arithmetic. A drop 0.02 units wide, ten units out, subtends about 0.002 radians. A phone framebuffer is roughly 860 pixels of render width across a ~1.2 radian field of view, so about 720 pixels per radian:

0.002 rad × 720 px/rad ≈ 0.7 px

Under one pixel. It doesn't get dimmer — it misses the sample point entirely and aliases away to nothing. On my desktop the same drop cleared about 2.4 pixels, which is exactly why it looked correct on the machine I was building it on and vanished on the machine people actually play on.

The fix is to stop picking a width and start picking a pixel width: drops roughly doubled, and then scaled by the hardware scaling level, because a higher scaling level means a smaller framebuffer and drops have to grow in world space to hold the same size on screen.

Day two: a sky with no rain in it

Same phone: "looking down the rain fills the screen, looking up only the bottom half has it."

Geometry this time. Drops spawned in a band 9–15 units above the player and immediately fell, so nothing ever existed above 15 units. Look down and you see the whole falling column. Look up and you see the empty sky above the spawn ceiling, with the rain stopping in the middle of the frame.

The box became a tall column and the drop lifetimes grew so they fall its full height — and then the counts had to roughly triple, from 3,400 to 9,000 on phones. Density is count divided by volume, and the box had grown about 3×, so my first attempt filled the sky by thinning the rain everywhere else. Looking down went visibly sparse. Filling a bigger volume costs more drops; there is no clever way around that, and it is the honest price of the particle approach. The shells never had this failure mode, because they fill the view by construction rather than by volume.

Day three: zoom out and you leave the weather

Same phone again. The camera orbit runs out to 55 units. The emitter box was pinned to the player at ±22.

So past about 22 units of zoom, the camera was outside the rain, looking at a small distant patch of it — and every drop in that patch was far away, which by day one's arithmetic means far under a pixel. Two bugs stacked into one symptom.

The box now re-centres on the midpoint between the eye and the character and grows with the orbit, so both are always inside it, and the drop size scales with the orbit as k**0.8 rather than linearly — linear holds angular size exactly constant, but now that the box surrounds the eye, a drop a few units from your face would be drawn at the size chosen for one fifty units out. Huge streaks on the lens.

There's a testing note buried in that day which cost me an hour: driving the camera by assigning cam.radius directly proves nothing. The camera-clip loop glides it back to the preferred radius every frame, so my first run of that test silently measured the default zoom three times and reported three passes. Zoom has to be driven by dispatching real wheel events at the canvas.

Day four: the one that looked like the box and wasn't

"Looking up, only the bottom half of the screen has rain."

The previous three bugs had all been the emitter box, so I spent a while on the emitter box. It was fine. Every drop was being drawn the entire time — I confirmed it by magnifying a patch of "empty" sky and finding all the streaks present.

Against the sky, every drop on screen is far away, so every drop is sub-pixel and every drop vanishes. Over the ground, the near drops are several pixels across and read perfectly. That asymmetry is the whole illusion: it looked like half the sky was missing rain, and really it was one arithmetic threshold crossing at a particular distance.

Drops got wider without getting longer — the vertical scale had to come down to compensate, because stretched billboards take their length from their size too. Measured over five frames on a patch that is clean sky in every one, counting pixels brighter than the local background: 22 → 61, about 2.8× the visible rain up there, with the near field unchanged.

The honest footnote

In the same change I also made the emitter box lead the camera along the direction of view, because about a third of it sat behind the eye and rain behind you is rain nobody sees. Leading it took the front wall from ~50 to ~67 units on the same budget.

I measured that one and it landed under the frame-to-frame noise. It stayed in anyway, because spending a third of your drop budget behind the camera is wrong on its own terms — but it was not the bug, and I'd rather write that down than let it sit in the changelog looking like a fix.

What I'd do differently

Measure in pixels, not world units. Every one of these four reports was somebody telling me about a pixel count, and every one of my first instincts was to adjust a world-space number by feel. The moment I wrote down "how many pixels wide is this thing, on that device", each bug took minutes.

Sub-pixel geometry disappears — it does not fade. This is the fact I keep having to re-learn. An anti-aliased renderer makes you expect graceful degradation, and thin unlit streaks give you a cliff instead. Anything long and thin (rain, lightning bolts, tracers, wires) lives near that cliff, and "invisible on a phone" is its default state unless you did the arithmetic.

A run of similar bugs will misdiagnose the next one for you. Three box bugs in a row bought a fourth day of looking at the box. Worth saying out loud when you catch yourself pattern-matching on your own recent history rather than on the report.

Keep the boring version around. The shells are still what low-end devices get. They are less impressive, they cost a fixed five draw calls, and they have never once needed a bug fix — because coverage by construction has no arithmetic in it to get wrong.