// BLOG

The Moon Disappeared Because Its Pixels Were in the Wrong Place

The Moon Disappeared Because Its Pixels Were in the Wrong Place

On 9 May, Selene’s night side went black. Not artistically dark. Gone.

The failure appeared while testing celestial occlusion in Ashes of the Unbound. The expected Aetheria contribution vanished even though the source and receiver uniforms looked correct, the ray calculations checked out and the same scene returned when a diagnostic switch bypassed the relevant branch.

The eventual cause was wonderfully specific: the shader was running the correct mathematics against a receiver position that did not exist on the moon.

Reproducing the blackout

The default night test produced the blackout:

Selene night test with the celestial contribution incorrectly blacked out.

Changing the diagnostic mode restored the expected lighting:

The same test with the diagnostic bypass restoring the expected night contribution.

That comparison narrowed the search. The light existed. Exposure was not hiding it. The data reached the shader. A specific occlusion path was rejecting the contribution.

It did not establish that the occlusion path should be removed.

Checking inputs and ray geometry

The debugging pass verified the source and receiver uniforms and checked the ray geometry. This is where the investigation could easily have stopped with a workaround: if the inputs look right and bypassing occlusion looks good, disable the failing test.

But the test represented real behaviour. A receiver body has to shadow itself, and future eclipses need the same geometry. Removing the branch would trade an obvious blackout for subtly impossible lighting.

So the next question was not whether the formula was correct. It was whether the point supplied to the formula was actually on the receiver.

Finding the fictional receiver point

The per-pixel path reconstructed a position from projected celestial SceneDepth. That proxy was suitable for placing the rendered celestial image on screen, but it was not guaranteed to describe a point on the physical receiver sphere.

The occlusion math then faithfully evaluated that fictional point. A perfectly implemented ray test can only be as truthful as its geometry.

The production correction snaps the reconstructed receiver back onto the actual sphere before evaluating self-occlusion and eclipse relationships. In conceptual form:

  1. reconstruct the screen-derived candidate;
  2. form the direction from the receiver centre to that candidate;
  3. normalize it and place the point on the known receiver radius;
  4. evaluate the existing occlusion math from that physical point.

This preserves the intended shadow logic rather than skipping it.

Verifying the eclipse boundary

The diagnostic eclipse mode made the geometry visible with an intentionally artificial red/green presentation:

Diagnostic mode showing the curved eclipse and self-occlusion boundary.

The colour is not a final visual effect. It is evidence that the boundary follows the receiver geometry. A straight or screen-aligned transition would indicate that some projected approximation was still leaking into the physical test.

The corrected default mode restored the intended lit state:

The production path after snapping the receiver position to the physical sphere.

Separating the diagnostic from the production fix

The bypass mattered because it isolated the failure. It was never a safe final state.

This distinction is worth preserving because screenshots often reward the wrong conclusion. The bypassed image looked better than the broken default. If the acceptance criterion had been “produce a lit screenshot,” the defective path could have shipped disabled and the project would have rediscovered it during the first real eclipse.

A diagnostic intervention answers a question. A production fix restores the contract.

Keeping the feature opt-in

The rendering capability remains opt-in. The engine-side mechanism can support projected celestial occlusion, while Ashes of the Unbound owns the policy and data that enable it.

The fix therefore did not silently impose the project’s celestial assumptions on every engine user. It corrected the physical receiver reconstruction within the capability, then left activation with the project.

Automating the test sequence

This bug required a familiar ritual: set a precise scene state, change a diagnostic mode, wait, capture the result, record the configuration and compare it with another run. Doing that manually once is fine. Doing it across every celestial issue is an invitation to mislabeled screenshots and irreproducible claims.

The next development phase turned that ritual into an editor tool: Turning a Debugging Ritual Into a Tool.