From 71b7177dc0550aeecf855a679302518aa112b214 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 11:28:57 +0400 Subject: [PATCH] fix(globe): Navara model tints are decoded from sRGB before mixing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instance colour (sRGB, as Cesium takes colours: the military amber #FFB800) was mixed into the linear material colour as is, so a tinted model drew paler and brighter than Cesium's: the military footprint gate read 124 % (more pixels cleared the contrast threshold over water) and the median luminance +9 %. White (flights) is the same either way, which is why the flights gate passed. The tint is now sRGBTransferEOTF-decoded, as the sprite sets already decode theirs. qa-flights-engines' model gate adds the highlights: the p90 luminance of the model pixels within ±10 % of Cesium, with the near-white pixel count reported (10b review low 2). Models on, near pose (computed scale ratio ~1.0000 on both): - military (from 100 km): footprint 9,902 vs 8,610 px (115 %), median luminance 158.9 vs 155.3, p90 190.9 vs 184.5 — 18/18; - flights: footprint 104 %, median 189.8 vs 196.7, p90 223.1 vs 247.8 (near-white px 83 vs 510: Cesium's white models saturate where Navara's tone mapping rolls them off) — 20/20. Models off: flights 16/16, military 15/15. Side-by-sides regenerated. --- src/globe/navara/movers/modelMover.js | 5 +++-- src/globe/navara/movers/modelMover.test.mjs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/globe/navara/movers/modelMover.js b/src/globe/navara/movers/modelMover.js index b9e3b26fa..4f029b424 100644 --- a/src/globe/navara/movers/modelMover.js +++ b/src/globe/navara/movers/modelMover.js @@ -47,7 +47,8 @@ const scratchTranslation = new Vector3(); const scratchScale = new Vector3(); /** - * Mixes the instance colour into the material's colour by uGevBlend (the + * Mixes the instance colour, decoded from sRGB (as Cesium takes colours; + * the material works in linear), into the material's colour by uGevBlend (the * set's colorBlend, as Cesium's colorBlendAmount); the instance alpha is the * model's opacity (Cesium: a model colour's alpha makes it translucent; the * aircraft fade with distance through it), not a blend weight. @@ -75,7 +76,7 @@ function blendInstanceColor(material, uniforms) { ) .replace( '#include ', - '#include \ndiffuseColor.rgb = mix(diffuseColor.rgb, vGevColor.rgb, uGevBlend);\ndiffuseColor.a *= vGevColor.a;', + '#include \ndiffuseColor.rgb = mix(diffuseColor.rgb, sRGBTransferEOTF(vGevColor).rgb, uGevBlend);\ndiffuseColor.a *= vGevColor.a;', ); }; material.customProgramCacheKey = () => 'gev-mover-model-blend'; diff --git a/src/globe/navara/movers/modelMover.test.mjs b/src/globe/navara/movers/modelMover.test.mjs index 28c2c9562..f3aefbc45 100644 --- a/src/globe/navara/movers/modelMover.test.mjs +++ b/src/globe/navara/movers/modelMover.test.mjs @@ -88,7 +88,7 @@ test('a far model keeps its minimum pixel size', async () => { ); }); -test("the instance colour tints by the set's blend amount; its alpha is opacity, as Cesium's model colour", async () => { +test("the instance colour (sRGB, decoded) tints by the set's blend amount; its alpha is opacity, as Cesium's model colour", async () => { const store = createMoverStore(options); const mover = createModelMover(options, store, { load: async () => fakeGltf(), @@ -110,7 +110,7 @@ test("the instance colour tints by the set's blend amount; its alpha is opacity, mesh.material.onBeforeCompile(shader); assert.match( shader.fragmentShader, - /diffuseColor\.rgb = mix\(diffuseColor\.rgb, vGevColor\.rgb, uGevBlend\);\s*diffuseColor\.a \*= vGevColor\.a;/, + /diffuseColor\.rgb = mix\(diffuseColor\.rgb, sRGBTransferEOTF\(vGevColor\)\.rgb, uGevBlend\);\s*diffuseColor\.a \*= vGevColor\.a;/, ); // Ambient light on the tinted surface (Cesium's image-based lighting). assert.match(