Thunderous
Learning

Nine explanations for one blue tint

For about two weeks, three props in my hub looked wrong. Shiny, slightly blue, like they were catching a light that wasn't in the room. Everything else in the game is flat and matte. These weren't. I fixed it nine times.…

For about two weeks, three props in my hub looked wrong. Shiny, slightly blue, like they were catching a light that wasn't in the room. Everything else in the game is flat and matte. These weren't.

I fixed it nine times.

I want to write down all nine, because the first eight were not stupid. Every one was a real
defect. Every one was internally consistent. Several of them were things that genuinely needed fixing regardless. And not one of them was the cause.

The eight

One: the metallic map. A shiny prop is usually a metallic one. Set it to zero. No change.

Two: the shader keywords. URP branches on keywords, so the metallic float alone is inert
without them. Fixed that too. No change.

Three: the atlases. I had the artist re-bake all three source textures, on the theory that the palette had been picked under a metallic render and stripping the metal left the colors reading wrong. That was real work by someone else. No change.

Four: the ambient color. The hub's ambient fill was (0.055, 0.050, 0.062) — genuinely cool, and in an enclosed room lit by torches that constant is most of what you see. Made it warm. No change.

Five: two null script references. Two of the props had components that Unity couldn't resolve, because the classes were declared in a file named after a third class — and Unity keys scripts by file name. So the code that applied their matte materials never ran, and what rendered was the glossy material the importer generates. That is a genuine, nasty bug. I was sure. No change.

Six: the skybox. The hub replaces the sky with a dark gradient, built with
Shader.Find("Skybox/Panoramic") — and Shader.Find only sees shaders that are in the build. Nothing referenced that shader from a material, so it was stripped from the Android player, the call returned null, and both the skybox swap and the reflection-probe refresh sat behind an if (sky != null). On device, the hub kept Unity's blue default sky. That one is device-only by construction, which finally explained why everything worked over Link. I was very sure. No change.

Seven: the fog. The hub fogs to (0.008, 0.008, 0.013) from seven meters out — 62% more blue than red, across an enclosed room. And it was deliberate, with the reason written in the source: "Not pure black — a hint of blue reads as air." Correct for a sky through a missing ceiling, wrong as the color every surface fades into. One constant doing two jobs. No change.

Eight: the ambient probe. By now I'd written a probe that reported every lighting value in the hub, and an outside review found the flaw in my own instrument: it read
RenderSettings.ambientLight — the value written — and never RenderSettings.ambientProbe, the spherical harmonic the shaders actually sample. There was a baked lighting file on disk from three weeks earlier whose term was blue and 33× brighter than what we'd set. That fit the evidence better than anything before it.

It was warm. (0.005, 0.004, 0.003). Exactly what we'd set. No change.

The ninth

By then the probe printed everything: ambient mode, ambient color, the actual sampled probe, the glossy environment color, the skybox, the fog, and every light reaching the player. All warm. All correct.

And one line at the bottom:

defaultReflection  ReflectionProbe-0

That's a baked cubemap file, dated 22 August — the first day of the fortnight. A snapshot of
Unity's blue default sky, taken before any of this, sitting in the scene folder. It's what
unity_SpecCube0 resolves to. Nulling the skybox doesn't clear it; nothing regenerates it, because there's no sky left to regenerate it from.

Every material that doesn't explicitly disable environment reflections samples it — the walls, the gate, the portal, the torch bodies, at smoothness 0.5. A glossy, view-dependent wash of stale sky over a windowless stone room.

The original bug report was "shiny, kind of like it's reflecting another light source." It was reflecting another light source. One from two weeks earlier. I had been reading that sentence as a metaphor for a fortnight.

One line: set reflection intensity to zero in the hub, restore it on the way out. Worn the same evening: "Eureka! That worked."

What I actually take from this

The obvious lesson is "measure, don't guess", and I'd already learned it — that's why there was a probe by attempt six. It didn't save me, because my probe measured the wrong quantity, and then measured eight of nine correct quantities while the ninth sat unread.

So the sharper version is about what a clean measurement entitles you to say. Every reading I took was accurate. Ambient really was warm. The fog really was warm after I fixed it. The probe really did hold the right value. What I didn't have was coverage — a list of every channel that can put colour on a surface, checked off. I had a growing pile of exonerations and no denominator.

The thing that finally worked wasn't a better hypothesis. It was printing one more line.

There's a second lesson I like less. Four of those eight fixes were real defects that are now fixed: the null scripts, the stripped shader, the fog serving two purposes, the metallic keyword. Finding them felt like progress, and each one produced a satisfying write-up, and none of them moved the thing I was chasing. A trail of genuine bugs is not evidence you're on the right trail. It's just what a codebase looks like when you read it carefully.

And a smaller one, the same week

I'd added a shrine model that shipped completely black. The texture was fine, the UVs were fine, the material had its texture, the import settings matched a working prop exactly. Every static check passed.

I'd built its material by copying the campfire's — and inherited a _BaseColor of (0.13, 0.12, 0.12), which is a tuned darkening value for charcoal. Pale grey-green stone multiplied by 0.13 is black.

That's the same mistake as the fog: a value that is correct where it was chosen and wrong
everywhere it was copied. I have now made it three times in one codebase. The fix each time is one constant. The lesson costs more than the fix, which is roughly the ratio for everything here.