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.
This commit is contained in:
German Khokhlov committed 2026-10-16 20:51:36 +04:00
1 parent 645f042450
commit 68ff4e1c25
3 files changed
+105 -3

No files matched your search

+2 -1
View File
@@ -90,7 +90,8 @@ float gevTactical() {
const FRAGMENT_COLOR = /* glsl */ `
#include <color_fragment>
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();
+5 -2
View File
@@ -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 <color_fragment>
if (gevOverlayHidden()) discard;
diffuseColor *= vGevColor;
diffuseColor *= sRGBTransferEOTF(vGevColor);
`;
// Overlay uniforms of a mesh drawn outside an overlay (tests): no depth.
+98
View File
@@ -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 () => {