From 68ff4e1c25fad0e75fb399f6a70588740d7f46a5 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 20:51:36 +0400 Subject: [PATCH] fix(globe): Navara overlay colours are sRGB Review of Tasks 4-5: the overlay pass encodes linear to sRGB, and the instance and line colours went in as if linear: grey 0.5 drew 187 (Cesium 128) and [1, 0.6, 0.2] drew 255/203/124 (Cesium 255/153/52). The sprite, point and line shaders now decode them (sRGBTransferEOTF), as the sRGB atlas texture already was. A contract check draws mid-tone sprites, points and lines (grey 0.5 and [1, 0.6, 0.2]) and reads their centres: within 3 per channel on every engine. Navara 79/79. --- src/globe/navara/lines/lineMesh.js | 3 +- src/globe/navara/movers/moverMesh.js | 7 +- src/globe/testing/contractSuite.mjs | 98 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/globe/navara/lines/lineMesh.js b/src/globe/navara/lines/lineMesh.js index 664acffb1..a4ac7d5de 100644 --- a/src/globe/navara/lines/lineMesh.js +++ b/src/globe/navara/lines/lineMesh.js @@ -90,7 +90,8 @@ float gevTactical() { const FRAGMENT_COLOR = /* glsl */ ` #include if (gevOverlayHidden()) discard; -diffuseColor = uGevColor; +// sRGB colours (as Cesium takes them) into the linear output. +diffuseColor = sRGBTransferEOTF(uGevColor); diffuseColor.a *= uGevAlphaScale; if (uGevDash > 1.5) { diffuseColor.a *= gevTactical(); diff --git a/src/globe/navara/movers/moverMesh.js b/src/globe/navara/movers/moverMesh.js index e8951e80d..fa647356d 100644 --- a/src/globe/navara/movers/moverMesh.js +++ b/src/globe/navara/movers/moverMesh.js @@ -89,6 +89,9 @@ varying float vGevOutline; ${OVERLAY_DEPTH_FRAGMENT} `; +// Instance colours are sRGB (as Cesium takes them) and the overlay output +// encodes linear to sRGB, so they are decoded first (the atlas is an sRGB +// texture, decoded by the sampler): grey 0.5 draws 128, not 187. const fragmentColor = (point) => point ? /* glsl */ ` @@ -96,13 +99,13 @@ const fragmentColor = (point) => if (gevOverlayHidden()) discard; float gevRadius = length(vGevLocal) * 2.0; if (gevRadius > 1.0) discard; -diffuseColor *= vGevColor; +diffuseColor *= sRGBTransferEOTF(vGevColor); if (gevRadius > 1.0 - vGevOutline) diffuseColor.rgb = vec3(0.0); ` : /* glsl */ ` #include if (gevOverlayHidden()) discard; -diffuseColor *= vGevColor; +diffuseColor *= sRGBTransferEOTF(vGevColor); `; // Overlay uniforms of a mesh drawn outside an overlay (tests): no depth. diff --git a/src/globe/testing/contractSuite.mjs b/src/globe/testing/contractSuite.mjs index f595669d9..12571f2cf 100644 --- a/src/globe/testing/contractSuite.mjs +++ b/src/globe/testing/contractSuite.mjs @@ -591,6 +591,104 @@ export function createContractSuite( ); }, ], + [ + 'colours: mid-tone sprite, point and line colours draw as asked (within 3 per channel)', + async () => { + if (!readRegion || !makeCanvas) return; + await nadirView(1e6); + const at = (lon, lat, height = 20_000) => + cartesian3.fromDegrees(lon, lat, height); + const GREY = [0.5, 0.5, 0.5, 1]; + const ORANGE = [1, 0.6, 0.2, 1]; + const atlasCanvas = makeCanvas(16, 16); + const context = atlasCanvas.getContext('2d'); + context.fillStyle = '#ffffff'; + context.fillRect(0, 0, 16, 16); + const sprites = engine.movers.createSet({ + id: 'contract-colour-sprites', + kind: 'sprite', + capacity: 2, + order: 100, + depthTest: 'always', + sprite: { + atlas: { canvas: atlasCanvas, cells: { box: [0, 0, 1, 1] } }, + sizePx: 32, + }, + }); + const points = engine.movers.createSet({ + id: 'contract-colour-points', + kind: 'point', + capacity: 2, + order: 100, + depthTest: 'always', + point: { sizePx: 32 }, + }); + const lines = [GREY, ORANGE].map((color, index) => + engine.lines.createLine({ + id: `contract-colour-line-${index}`, + widthPx: 16, + color, + }), + ); + // The centre 4×4 pixels' mean, as 0..255 per channel. + const centre = (position) => { + const screen = engine.projector.worldToWindow(position); + const data = readRegion( + Math.round(screen.x) - 2, + Math.round(screen.y) - 2, + 4, + 4, + ); + const sum = [0, 0, 0]; + for (let i = 0; i < data.length; i += 4) + for (let k = 0; k < 3; k++) sum[k] += data[i + k]; + return sum.map((value) => value / (data.length / 4)); + }; + const cases = [ + ['sprite', GREY, at(19.5, 10.5)], + ['sprite', ORANGE, at(20.5, 10.5)], + ['point', GREY, at(19.5, 9.5)], + ['point', ORANGE, at(20.5, 9.5)], + ['line', GREY, at(19.5, 10)], + ['line', ORANGE, at(20.5, 10)], + ]; + try { + sprites.set(0, { position: cases[0][2], color: GREY, cell: 'box' }); + sprites.set(1, { position: cases[1][2], color: ORANGE, cell: 'box' }); + sprites.setCount(2); + sprites.commit(); + points.set(0, { position: cases[2][2], color: GREY }); + points.set(1, { position: cases[3][2], color: ORANGE }); + points.setCount(2); + points.commit(); + // Short vertical lines through their sample points. + lines[0].setPositions([at(19.5, 9.9), at(19.5, 10.1)]); + lines[1].setPositions([at(20.5, 9.9), at(20.5, 10.1)]); + const off = (seen, colour) => + Math.max( + ...seen.map((value, k) => Math.abs(value - colour[k] * 255)), + ); + await until( + () => cases.every(([, colour, p]) => off(centre(p), colour) <= 3), + 'mid-tone colours within 3', + ); + } catch (error) { + const seen = cases + .map( + ([kind, colour, p]) => + `${kind} ${colour.slice(0, 3)} → ${centre(p) + .map((v) => v.toFixed(0)) + .join('/')}`, + ) + .join('; '); + throw new ContractError(`${error.message}: ${seen}`); + } finally { + sprites.destroy(); + points.destroy(); + for (const line of lines) line.destroy(); + } + }, + ], [ 'lines: a line draws between its projections, turns with its model matrix and dims where hidden', async () => {