The sun was closer than the tree
The report was a single sentence: "sometimes the sun or the moon is closer than the building or the tree." It sounds like a rendering bug, and it was the camera's fault, not the renderer's.
Where the sun actually is
The sky in TERMINAL DESTINY is layered. There is a dome, a big sphere around the camera with the sky colour painted on it, and it deliberately does not write depth, so anything in the world draws over it. In front of the dome sit the sun, the moon, the stars, the occasional shooting star and, in a storm, the lightning. Each of those is a flat quad that rides the camera at a fixed distance, turned to face you.
The fixed distance was 270 units, "just inside the dome's 300-unit radius", and the original comment explained why that was clever:
// Far enough that terrain and the horizon mountains occlude it (they are real
// geometry and write depth, while the dome deliberately does not)
const DIST = 270;
That was true on the day it was written. A sun 270 units out sits behind the horizon mountains, so it sets behind them, and that's lovely.
Then two things changed and nobody went back to this line. The horizon mountains were removed. And the world grew: it is now about 1,150 units across, and fog doesn't finish until 360–900 units depending on the device. The desktop camera draws out to 10,000.
Why "behind" became "in front"
A depth-tested quad has one rule: it's hidden by anything closer than it is, and it's drawn over anything farther. At 270 units:
- a tree 150 units away is closer than the sun, so it hides it. Correct.
- a building 400 units away is farther than the sun, so the sun paints over it.
From the player's chair, that second case is a sun hanging in front of a building that's visibly between you and the horizon. The same went for the moon, every star in the night sky and, worst of all, the lightning, whose 130-unit glow could light up in front of a distant treeline.
I didn't trust a screenshot for this, because a screenshot can't tell "in front" from "beside". So the test was a tall block placed on the line between the camera and the moon at two distances:
block at 180 units → hidden behind it (before and after)
block at 400 units → moon drawn ON it (before) / hidden (after)
The fix is one function
The bodies should sit behind everything the camera can draw, so their distance has to come from the camera, not from the dome:
export function skyDist(cam) {
return Math.min(2000, (cam?.maxZ || 10000) * 0.94);
}
Just inside the far plane, capped at 2,000 on desktop. Nothing in a world 1,150 units wide is farther than that, and a tighter range keeps depth precision where the world actually is. On phones the far plane is 500, so the sky sits at 470.
Moving a quad farther away makes it smaller on screen, and every size in that file had been tuned by eye at 270. So each body is also scaled by skyDist / 270, which keeps exactly the angular size it was tuned at. The moon in the "after" screenshot is the same moon, in the same place, at the same size, except now a building can stand in front of it.
While I was in there I fixed a small inconsistency. The moon already dimmed behind storm cloud; the sun never did, so a downpour could fall under a full, bright noon disc. It dims now too.
The same week: every bridge was a roof
The weather system needs to know whether you're under cover. Indoors, the rain moves outside the doorway instead of falling through the ceiling, splashes stop appearing on the floor, the storm's sound is muffled, and lightning is dimmed. The test is a single ray cast straight up from the player. If it hits something, you're sheltered.
The bug was where the ray started:
const _gy = bScene._terrainFn(_ct.x, _ct.z); // the TERRAIN under you
_upRay.origin.copyFromFloats(_ct.x, _gy + 1.2, _ct.z);
"1.2 units above the terrain under the player" is the right answer almost everywhere. On a bridge, the terrain under the player is the riverbed, about ten units below the deck you're standing on. So the ray started underwater, hit the underside of the bridge, and reported a roof.
The result: walk onto any bridge in a storm and the splashes vanished, the rain went quiet and the lightning dimmed, as though you'd stepped indoors. On phones it was worse, because the coarser phone terrain sat just low enough that even the bridge's end ramps did it.
The fix starts the ray from the floor the player is actually standing on. The game already knew that number, because it's what keeps you on top of the deck:
_gy = Math.max(_gy, bScene._lastFloorY);
Fixing that exposed a second problem. The concrete bridge has a steel truss over its deck, and the ray hit a cross-member, so the middle of that bridge was still "indoors". A truss is not a roof; rain falls through it. Anything named as a truss is now ignored by the roof test.
Fixing shelter also made a third thing visible. Splashes and puddles were placed at terrain height, which on a bridge is under the water: the deck you stood on never got wet, while the riverbed under the bridge, which is sheltered, splashed away. Rain now lands on the highest walkable surface at that spot, unless that surface has a roof over it. To decide that, nine probes go up across the floor and it counts as covered only if most of them hit. A single probe got three places wrong on the first try: a cooking pot over a campfire, the bridge truss again, and an observatory whose roof has a slot in the middle for the telescope.
What I'd do differently
Both bugs are the same shape: a constant that described the world at the moment it was written. "270 is behind the mountains" stopped being true when the mountains left. "1.2 above the terrain is above your feet" was never true on a bridge, and the bridges came later.
Neither number was wrong on the day it was chosen, and neither had anything attached to it that would fail when the world changed. What I do now is derive the number from the thing it's really about: the sky's distance from the camera's far plane, and the shelter probe's height from the floor you're standing on. If the assumption is "behind everything", ask the camera what "everything" is.