From 8cb096fc211db12fcf6f114d49be9aea1c6bbc94 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 14 Nov 2026 08:06:51 -0800 Subject: [PATCH] Draw aircraft models in their own mesh colours Every aircraft model (Flights, Military Flights, Local ADS-B) mixed 94% of a flat class colour over its mesh, so airliners drew as white silhouettes and military ones as amber. Models now use HIGHLIGHT: white leaves the mesh's own colours as built, and the tracked and emergency colours tint the mesh without hiding its paint. The IR boost still draws them flat white. --- src/data/aircraftEmergencyAnalyst.test.mjs | 3 ++- src/data/aircraftModelLighting.js | 24 +++++++++++++++++----- src/data/trackedReadout.test.mjs | 4 +--- src/layers/flights/modelSpec.js | 3 --- src/layers/flights/policy.js | 4 ---- src/layers/flights/rendering.js | 20 ++++++++---------- src/layers/localAdsb/index.js | 1 - src/layers/localAdsb/index.test.mjs | 9 +++----- src/layers/localAdsb/models.js | 16 +++++++-------- src/layers/military/policy.js | 4 ---- src/layers/military/rendering.js | 23 +++++++++++---------- 11 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/data/aircraftEmergencyAnalyst.test.mjs b/src/data/aircraftEmergencyAnalyst.test.mjs index 27deb2b46..f26c28abc 100644 --- a/src/data/aircraftEmergencyAnalyst.test.mjs +++ b/src/data/aircraftEmergencyAnalyst.test.mjs @@ -458,7 +458,8 @@ test('tint priorities cover normal, priority, emergency, tracking and Cockpit do ); flightState._trackedIcao = null; flightState.records.data.set('civil', { emergency: null }); - assert.equal(rendering._modelColor('civil'), normal); + // An ordinary model draws its mesh's own colours, whatever the billboard's. + assert.equal(rendering._modelColor('civil'), Cesium.Color.WHITE); } }); diff --git a/src/data/aircraftModelLighting.js b/src/data/aircraftModelLighting.js index c8051c67e..38725be93 100644 --- a/src/data/aircraftModelLighting.js +++ b/src/data/aircraftModelLighting.js @@ -1,3 +1,5 @@ +import * as Cesium from 'cesium'; + /** * Lighting options shared by every aircraft 3D model (Flights, Military * Flights, Local ADS-B). @@ -6,12 +8,24 @@ * the model and reads the result back from the GPU (`readPixels`) to light it, * when the model is first placed and again when it moves more than 1 km, when * the scene's atmosphere settings change, or every scene hour under sunlight - * lighting. Aircraft models take 94% of their surface from the class colour - * (`colorBlendAmount`), so that lighting contributes little; without it they - * are lit from Cesium's default spherical harmonics. Measured 2026-10-24 - * (Intel Arc, Google 3D, ~150 models in the scene): GPU per frame at rest - * 14.3-14.8 -> 12.1-12.5 ms. + * lighting. Without it they are lit from Cesium's default spherical + * harmonics. Measured 2026-10-24 (Intel Arc, Google 3D, ~150 models in the + * scene): GPU per frame at rest 14.3-14.8 -> 12.1-12.5 ms. */ export const AIRCRAFT_ENVIRONMENT_MAP_OPTIONS = Object.freeze({ enabled: false, }); + +/** + * How an aircraft model's colour meets its mesh. HIGHLIGHT multiplies, so + * white draws the mesh's own colours as built, and the tracked or emergency + * colour tints the mesh without hiding its paint. The IR boost draws the + * model flat white instead (MIX at 1, with its unlit shader). + * @param {boolean} irBoost + * @returns {{colorBlendMode: Cesium.ColorBlendMode, colorBlendAmount: number}} + */ +export function aircraftModelBlend(irBoost) { + return irBoost + ? { colorBlendMode: Cesium.ColorBlendMode.MIX, colorBlendAmount: 1 } + : { colorBlendMode: Cesium.ColorBlendMode.HIGHLIGHT, colorBlendAmount: 0 }; +} diff --git a/src/data/trackedReadout.test.mjs b/src/data/trackedReadout.test.mjs index 425a69ceb..a6b69c7e4 100644 --- a/src/data/trackedReadout.test.mjs +++ b/src/data/trackedReadout.test.mjs @@ -291,7 +291,7 @@ test('tracking layers write gevLabelModel and expose only their cached display p assert.equal(sources['satellites.js'].includes('_trackedEntity.label.text'), false); }); -test('civilian and military trail heads use the lower-centre model anchor and weak-texture tint', async () => { +test('civilian and military trail heads use the lower-centre model anchor', async () => { const files = await Promise.all(['flights.js', 'militaryFlights.js'].map(async (name) => ( [name, readLayerSource(new URL(`./${name}`, import.meta.url))] ))); @@ -300,7 +300,5 @@ test('civilian and military trail heads use the lower-centre model anchor and we (/const\s*head\s*=\s*(?:parts\.motion\.)?_trackedTrailCached\(\s*,?\s*\)\s*\|\|\s*(?:parts\.motion\.)?_trackedDisplayPosition\(\s*(?:flightState\.)?_trackedIcao,?\s*\);/).test(source), `${name} trail head uses the dedicated lower-centre model anchor`, ); - assert.ok(source.includes('const MODEL_COLOR_BLEND_AMOUNT = 0.94;'), - `${name} keeps diffuse texture contribution weak through code-side MIX`); } }); diff --git a/src/layers/flights/modelSpec.js b/src/layers/flights/modelSpec.js index 444a22702..2a4455b0e 100644 --- a/src/layers/flights/modelSpec.js +++ b/src/layers/flights/modelSpec.js @@ -4,7 +4,6 @@ import { aircraftModelSpec, fleetModelSet, } from '../../data/fleetModels.js'; -import { MODEL_COLOR_BLEND_AMOUNT } from './policy.js'; /** * What an aircraft draws with before the fleet model set has loaded: no @@ -22,7 +21,6 @@ export const UNLOADED_AIRCRAFT_MODEL_SPEC = Object.freeze({ bellyM: 0, elevationM: 0, tinted: false, - blendAmount: MODEL_COLOR_BLEND_AMOUNT, visualCenterNative: Object.freeze([0, 0, 0]), trailAnchorNative: Object.freeze([0, 0, 0]), key: 'unloaded', @@ -58,7 +56,6 @@ export function aircraftLayerModelSpec(aircraft, set = fleetModelSet()) { layerSpec = Object.freeze({ ...spec, bellyM: spec.clearanceM, - blendAmount: MODEL_COLOR_BLEND_AMOUNT, }); cache.set(key, layerSpec); } diff --git a/src/layers/flights/policy.js b/src/layers/flights/policy.js index 21a559769..3b89a26f6 100644 --- a/src/layers/flights/policy.js +++ b/src/layers/flights/policy.js @@ -85,10 +85,6 @@ export const MODEL_ALL_KEEP_M = 450000; export const MODEL_HEADING_OFFSET_DEG = 180; // every fleet model is nose -X: opposite Cesium heading 0 -// Owner launch-polish direction: models should read as clean light silhouettes, -// with only a weak diffuse contribution from the models' own colours. - -export const MODEL_COLOR_BLEND_AMOUNT = 0.94; export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0); diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index 1ebbf495f..64d9064ff 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -45,7 +45,10 @@ import { STALE_ALPHA, displayDelayMs, } from './policy.js'; -import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js'; +import { + AIRCRAFT_ENVIRONMENT_MAP_OPTIONS, + aircraftModelBlend, +} from '../../data/aircraftModelLighting.js'; export function createRendering({ flightState, @@ -192,11 +195,12 @@ export function createRendering({ * @returns {void} */ - /** Model tint, mirroring the billboard color rules. */ + /** Model colour: the mesh's own (white), tinted cyan while tracked and red + * while broadcasting an emergency. */ function _modelColor(icao24) { if (icao24 === flightState._trackedIcao) return Cesium.Color.CYAN; - return _fleetBillboardColor(icao24); + return _isEmergency(icao24) ? EMERGENCY_TINT : Cesium.Color.WHITE; } /** The FLEET's 3D-model regime: models3d enabled AND the camera zoomed in past the altitude @@ -526,10 +530,7 @@ export function createRendering({ minimumPixelSize: MODEL_MIN_PX, scale: spec.scale, color: flightState._irBoost ? Cesium.Color.WHITE : _modelColor(icao24), - colorBlendMode: Cesium.ColorBlendMode.MIX, - // Launch presentation keeps the code-side tint dominant for every approved - // model; IR boost removes the remaining diffuse hint with flat UNLIT white. - colorBlendAmount: flightState._irBoost ? 1.0 : spec.blendAmount, + ...aircraftModelBlend(flightState._irBoost), customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined, @@ -748,10 +749,7 @@ export function createRendering({ minimumPixelSize: TRACKED_MODEL_MIN_PX, scale: trackedSpec.scale, color: flightState._irBoost ? Cesium.Color.WHITE : Cesium.Color.CYAN, - colorBlendMode: Cesium.ColorBlendMode.MIX, - // The tracked aircraft uses the same dominant light tint as the fleet; - // IR boost removes the remaining diffuse hint with flat UNLIT white. - colorBlendAmount: flightState._irBoost ? 1.0 : trackedSpec.blendAmount, + ...aircraftModelBlend(flightState._irBoost), customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined, diff --git a/src/layers/localAdsb/index.js b/src/layers/localAdsb/index.js index a8c62b2bc..7cf01031c 100644 --- a/src/layers/localAdsb/index.js +++ b/src/layers/localAdsb/index.js @@ -700,7 +700,6 @@ export function createLocalAdsbLayer({ if (viewer.scene?.primitives) { models = createLocalAdsbModels({ viewer, - color, resolveAsset, groundSnap: services.groundSnap?.createGroundSnap?.() || null, ...(loadModel ? { loadModel } : {}), diff --git a/src/layers/localAdsb/index.test.mjs b/src/layers/localAdsb/index.test.mjs index 431caf9b6..573adc14d 100644 --- a/src/layers/localAdsb/index.test.mjs +++ b/src/layers/localAdsb/index.test.mjs @@ -548,13 +548,10 @@ test('the DISPLAY 3D toggle gives local aircraft magenta class models', async (t assert.equal(model.scale, spec.scale, 'drawn life size'); assert.equal(model.id, 'local-adsb:a0b702'); assert.ok( - Cesium.Color.equals( - model.color, - Cesium.Color.fromCssColorString('#ff4fd8'), - ), + Cesium.Color.equals(model.color, Cesium.Color.WHITE), + 'the mesh draws its own colours', ); - assert.equal(model.colorBlendMode, Cesium.ColorBlendMode.MIX); - assert.equal(model.colorBlendAmount, 0.94); + assert.equal(model.colorBlendMode, Cesium.ColorBlendMode.HIGHLIGHT); assert.equal(model.show, true); const heli = sources[0].entities.getById('local-adsb:a0b702'); assert.equal( diff --git a/src/layers/localAdsb/models.js b/src/layers/localAdsb/models.js index 9f6700847..307412a78 100644 --- a/src/layers/localAdsb/models.js +++ b/src/layers/localAdsb/models.js @@ -16,7 +16,10 @@ import { MODEL_PROX_ADD_M, MODEL_PROX_KEEP_M, } from '../flights/policy.js'; -import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js'; +import { + AIRCRAFT_ENVIRONMENT_MAP_OPTIONS, + aircraftModelBlend, +} from '../../data/aircraftModelLighting.js'; const specOf = (marker) => aircraftLayerModelSpec({ @@ -31,15 +34,14 @@ const specOf = (marker) => * Mirrors the public Flights fleet: models only below the altitude ceiling, * the same proximity/all caps and add/keep radii, the same visible-first * eligibility (`selectModelEligible`), the same fleet model per type, life - * size (`aircraftLayerModelSpec`) and the same tint path (a MIX colour blend, here - * magenta, as Flights tints military amber). Each model owns its matrix. A + * size (`aircraftLayerModelSpec`) and the same colours: the mesh's own + * (`aircraftModelBlend`). Each model owns its matrix. A * billboard stays the visual until its model is loaded AND placed; a grounded * aircraft's model rides the one-shot ground snap plus the class belly * offset, and without a resolved ground it stays a billboard. Every fleet * model loads once, up front, when the layer starts (`loadFleetModelSet`). * @param {object} options * @param {object} options.viewer Cesium viewer. - * @param {Cesium.Color} options.color Model tint. * @param {(url: string) => string} options.resolveAsset * @param {object|null} [options.groundSnap] `createGroundSnap()` instance. * @param {(options: object) => Promise} [options.loadModel] @@ -47,7 +49,6 @@ const specOf = (marker) => */ export function createLocalAdsbModels({ viewer, - color, resolveAsset, groundSnap = null, loadModel = (options) => Cesium.Model.fromGltfAsync(options), @@ -104,9 +105,8 @@ export function createLocalAdsbModels({ environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS, minimumPixelSize: MODEL_MIN_PX, scale: spec.scale, - color, - colorBlendMode: Cesium.ColorBlendMode.MIX, - colorBlendAmount: spec.blendAmount, + color: Cesium.Color.WHITE, + ...aircraftModelBlend(false), id, }, { resolveAsset, load: loadModel }, diff --git a/src/layers/military/policy.js b/src/layers/military/policy.js index 7a62bc92d..ac23188ff 100644 --- a/src/layers/military/policy.js +++ b/src/layers/military/policy.js @@ -107,10 +107,6 @@ export const MODEL_ALL_KEEP_M = 450000; export const MODEL_HEADING_OFFSET_DEG = 180; // every fleet model is nose -X: opposite Cesium heading 0 -// Preserve the layer's amber identity while reducing the models' own colours -// to a weak diffuse hint, matching civilian launch presentation. - -export const MODEL_COLOR_BLEND_AMOUNT = 0.94; /** Amber fade target for the tracked billboard once the model takes over (mirrors flights.js CYAN_TRANSPARENT). */ diff --git a/src/layers/military/rendering.js b/src/layers/military/rendering.js index a63955a36..7e19b4567 100644 --- a/src/layers/military/rendering.js +++ b/src/layers/military/rendering.js @@ -34,7 +34,6 @@ import { MODEL_PROX_KEEP_M, IR_RELOAD_BATCH, MODEL_MIN_PX, - MODEL_COLOR_BLEND_AMOUNT, TRACKED_MODEL_MIN_PX, TRACKED_MODEL_MAX_PX, FLEET_DR_INTERVAL_MS, @@ -43,7 +42,10 @@ import { COURSE_MAX_DPS, STALE_ALPHA, } from './policy.js'; -import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js'; +import { + AIRCRAFT_ENVIRONMENT_MAP_OPTIONS, + aircraftModelBlend, +} from '../../data/aircraftModelLighting.js'; export function createRendering({ flightState, @@ -81,7 +83,6 @@ export function createRendering({ const base = aircraftLayerModelSpec(meta); spec = Object.freeze({ ...base, - blendAmount: MODEL_COLOR_BLEND_AMOUNT, headingOffsetDeg: MODEL_HEADING_OFFSET_DEG, }); // Not cached before the model set loads: the answer changes once it has. @@ -152,11 +153,15 @@ export function createRendering({ const _iconKind = (icao24, klass) => tr3bIconKind(icao24, klass, { hot: flightState._irBoost }); - /** Model tint, mirroring the billboard color rules (amber instead of cyan/white). */ + /** Model colour: the mesh's own (white), tinted amber while tracked and red + * while broadcasting an emergency. */ function _modelColor(icao24) { if (icao24 === flightState._trackedIcao) return TRACKED_ICON_COLOR; - return _fleetBillboardColor(icao24); + const info = flightState.records.data.get(icao24); + return isEmergency(info?.emergency) + ? EMERGENCY_ICON_COLOR + : Cesium.Color.WHITE; } /** Fleet (untracked) tint: red while the latest poll broadcast an emergency, @@ -443,9 +448,7 @@ export function createRendering({ minimumPixelSize: MODEL_MIN_PX, scale: spec.scale, color: flightState._irBoost ? Cesium.Color.WHITE : _modelColor(icao24), - colorBlendMode: Cesium.ColorBlendMode.MIX, - // near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot) - colorBlendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT, + ...aircraftModelBlend(flightState._irBoost), customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined, @@ -662,9 +665,7 @@ export function createRendering({ minimumPixelSize: TRACKED_MODEL_MIN_PX, scale: trackedSpec.scale, color: flightState._irBoost ? Cesium.Color.WHITE : TRACKED_ICON_COLOR, - colorBlendMode: Cesium.ColorBlendMode.MIX, - // near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot) - colorBlendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT, + ...aircraftModelBlend(flightState._irBoost), customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined,