mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(globe): Navara aircraft models face, size and light as Cesium's (10b review 3)
The near-pose finding (Navara models faint, Cesium ~23 bright aircraft) came from the side-by-side's frozen page clock: the fleet tick ran once per pose, so Cesium never handed its aircraft to their models and drew 3× glyphs, against real models on Navara. With the hand-off driven (the move ends every 10 frames while a pose loads), both engines draw models, and the comparison showed three real differences, all fixed: - Orientation: Navara's glTF loader is three's GLTFLoader, which keeps the scene Y-up; the instance turn is now Cesium's whole axis correction (Y_UP_TO_Z_UP × Z_UP_TO_X_UP), not Z_UP_TO_X_UP alone. Every fleet model drew 90° off its heading. - Size: minimumPixelSize is taken at Cesium's distance (Camera distanceToBoundingSphere: view depth less the radius at the model's own scale), in the mover and in ModelHandle.computedScale; the straight-line distance drew off-centre models up to 16 % larger. - Colour: the instance colour tints by the set's blend amount (Cesium's colorBlendAmount) and its alpha is the model's opacity (Cesium: a model colour's alpha makes it translucent), not a blend weight; a model is translucent only while faded. An ambient share of the tinted surface (MODEL_AMBIENT 0.5, Cesium's image-based lighting) keeps a tinted aircraft near-white at a low sun (median luminance 139 → 190; Cesium 197). qa-flights-engines (now --layer=flights|military) gates the near pose with --models=on: computed scale within ±5 % per model (ratio 1.000007), footprint against the same view with the layer off within ±20 % (1553 vs 1496 px, 104 %), median luminance within ±10 % (189.8 vs 196.7). The probe's drawn() reports a model's computedScale. Flights models on: 19/19.
This commit is contained in:
1 parent
9dc446bd24
commit
10d0e49ddb
6 files changed
+158
-23
No files matched your search
@@ -72,6 +72,7 @@ class GevMoverSetDesc extends MeshDesc {
|
||||
// The camera a mover needs each drawn frame (scratch: no allocation).
|
||||
const frameCamera = {
|
||||
cameraPosition: { x: 0, y: 0, z: 0 },
|
||||
cameraDirection: { x: 0, y: 0, z: -1 },
|
||||
cameraHeight: 0,
|
||||
fovDeg: 60,
|
||||
viewportHeightPx: 0,
|
||||
@@ -86,6 +87,12 @@ function updateMover(view, ctx, mover) {
|
||||
position.x = e[12];
|
||||
position.y = e[13];
|
||||
position.z = e[14];
|
||||
// A three camera looks down its local −z.
|
||||
const direction = frameCamera.cameraDirection;
|
||||
const length = Math.hypot(e[8], e[9], e[10]) || 1;
|
||||
direction.x = -e[8] / length;
|
||||
direction.y = -e[9] / length;
|
||||
direction.z = -e[10] / length;
|
||||
frameCamera.cameraHeight =
|
||||
Math.hypot(position.x, position.y, position.z) - EARTH_RADIUS_M;
|
||||
frameCamera.fovDeg = raw.fov;
|
||||
|
||||
@@ -19,12 +19,39 @@ import {
|
||||
} from 'three';
|
||||
import { footprintPerDepth, originFor } from './pack.js';
|
||||
|
||||
/**
|
||||
* Ambient light on the tinted surface, as an emissive share of it: Cesium
|
||||
* lights models with image-based lighting besides the sun, so a tinted
|
||||
* aircraft reads near-white at any sun angle; Navara's sky light alone left
|
||||
* them grey-brown at dawn. 0.5 matched Cesium's model luminance at the near
|
||||
* pose (qa-flights-engines --models=on: median 190 vs 193 on Cesium; 139
|
||||
* without it).
|
||||
*/
|
||||
export const MODEL_AMBIENT = 0.5;
|
||||
|
||||
/**
|
||||
* The distance Cesium sizes a model's minimumPixelSize at (Camera
|
||||
* distanceToBoundingSphere): |offset · view direction| less `radiusM`, at
|
||||
* least 0; the straight-line distance when no direction is known.
|
||||
*/
|
||||
export function modelPixelDistance(dx, dy, dz, direction, radiusM) {
|
||||
const along = direction
|
||||
? Math.abs(dx * direction.x + dy * direction.y + dz * direction.z)
|
||||
: Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
return Math.max(0, along - radiusM);
|
||||
}
|
||||
|
||||
const scratchMatrix = new Matrix4();
|
||||
const scratchQuaternion = new Quaternion();
|
||||
const scratchTranslation = new Vector3();
|
||||
const scratchScale = new Vector3();
|
||||
|
||||
/** Mixes the instance colour into the material's colour by uGevBlend × alpha. */
|
||||
/**
|
||||
* Mixes the instance colour 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.
|
||||
*/
|
||||
function blendInstanceColor(material, uniforms) {
|
||||
material.onBeforeCompile = (shader) => {
|
||||
Object.assign(shader.uniforms, uniforms);
|
||||
@@ -40,11 +67,15 @@ function blendInstanceColor(material, uniforms) {
|
||||
shader.fragmentShader = shader.fragmentShader
|
||||
.replace(
|
||||
'void main() {',
|
||||
'uniform float uGevBlend;\nvarying vec4 vGevColor;\nvoid main() {',
|
||||
'uniform float uGevBlend;\nuniform float uGevAmbient;\nvarying vec4 vGevColor;\nvoid main() {',
|
||||
)
|
||||
.replace(
|
||||
'#include <emissivemap_fragment>',
|
||||
'#include <emissivemap_fragment>\ntotalEmissiveRadiance += diffuseColor.rgb * uGevAmbient;',
|
||||
)
|
||||
.replace(
|
||||
'#include <map_fragment>',
|
||||
'#include <map_fragment>\ndiffuseColor.rgb = mix(diffuseColor.rgb, vGevColor.rgb, uGevBlend * vGevColor.a);',
|
||||
'#include <map_fragment>\ndiffuseColor.rgb = mix(diffuseColor.rgb, vGevColor.rgb, uGevBlend);\ndiffuseColor.a *= vGevColor.a;',
|
||||
);
|
||||
};
|
||||
material.customProgramCacheKey = () => 'gev-mover-model-blend';
|
||||
@@ -58,6 +89,7 @@ function blendInstanceColor(material, uniforms) {
|
||||
export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
const { capacity, model } = options;
|
||||
const uniforms = {
|
||||
uGevAmbient: { value: MODEL_AMBIENT },
|
||||
uGevBlend: {
|
||||
value: model.colorBlend
|
||||
? model.colorBlend.mode === 'replace'
|
||||
@@ -71,9 +103,24 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
let origin = null;
|
||||
let footprint = 0;
|
||||
let cameraPosition = null;
|
||||
let cameraDirection = null;
|
||||
let loaded = false;
|
||||
let disposed = false;
|
||||
const colorAttributes = [];
|
||||
const materials = [];
|
||||
// Translucent only while an instance is faded (opaque models keep the
|
||||
// opaque pass).
|
||||
const syncTransparency = () => {
|
||||
const { colors } = store;
|
||||
let faded = false;
|
||||
for (let index = 0; index < store.count && !faded; index++)
|
||||
if (store.shows[index] === 1 && colors[index * 4 + 3] < 1) faded = true;
|
||||
for (const material of materials)
|
||||
if (material.transparent !== faded) {
|
||||
material.transparent = faded;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
};
|
||||
|
||||
const write = (from, to) => {
|
||||
if (!loaded || !origin) return;
|
||||
@@ -84,7 +131,16 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
const dx = positions[index * 3] - cameraPosition.x;
|
||||
const dy = positions[index * 3 + 1] - cameraPosition.y;
|
||||
const dz = positions[index * 3 + 2] - cameraPosition.z;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
// Cesium's distance for minimumPixelSize (Camera
|
||||
// distanceToBoundingSphere): the depth along the view direction
|
||||
// less the model's radius at its own scale.
|
||||
const distance = modelPixelDistance(
|
||||
dx,
|
||||
dy,
|
||||
dz,
|
||||
cameraDirection,
|
||||
radius * scale,
|
||||
);
|
||||
const pixels = (2 * radius * scale) / (footprint * distance);
|
||||
if (pixels < model.minimumPixelSize)
|
||||
scale *= model.minimumPixelSize / pixels;
|
||||
@@ -119,6 +175,7 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
}
|
||||
for (const attribute of colorAttributes) attribute.needsUpdate = true;
|
||||
syncTransparency();
|
||||
};
|
||||
|
||||
const group = { meshes: [], ready: null };
|
||||
@@ -140,6 +197,7 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
});
|
||||
else material = material.clone();
|
||||
blendInstanceColor(material, uniforms);
|
||||
materials.push(material);
|
||||
const geometry = child.geometry.clone();
|
||||
const color = new InstancedBufferAttribute(
|
||||
new Float32Array(capacity * 4).fill(1),
|
||||
@@ -189,9 +247,16 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
for (const { mesh } of parts) mesh.count = store.count;
|
||||
},
|
||||
/** Each drawn frame: pixel limits need every instance rewritten. */
|
||||
update({ cameraPosition: camera, cameraHeight, fovDeg, viewportHeightPx }) {
|
||||
update({
|
||||
cameraPosition: camera,
|
||||
cameraDirection: direction = null,
|
||||
cameraHeight,
|
||||
fovDeg,
|
||||
viewportHeightPx,
|
||||
}) {
|
||||
footprint = footprintPerDepth(fovDeg, viewportHeightPx);
|
||||
cameraPosition = camera;
|
||||
cameraDirection = direction;
|
||||
if (!origin || store.count === 0) return false;
|
||||
const next = originFor(camera, origin, cameraHeight);
|
||||
if (next !== origin) placeAt(next);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { createMoverStore, normalizeMoverSetOptions } from '../../movers.js';
|
||||
import { createModelMover } from './modelMover.js';
|
||||
import { createModelMover, modelPixelDistance } from './modelMover.js';
|
||||
|
||||
const options = normalizeMoverSetOptions({
|
||||
id: 'm',
|
||||
@@ -69,6 +69,7 @@ test('a far model keeps its minimum pixel size', async () => {
|
||||
await mover.group.ready;
|
||||
mover.update({
|
||||
cameraPosition: { x: 6_378_137 + 100_000, y: 0, z: 0 },
|
||||
cameraDirection: { x: -1, y: 0, z: 0 },
|
||||
cameraHeight: 100_000,
|
||||
fovDeg: 60,
|
||||
viewportHeightPx: 800,
|
||||
@@ -76,11 +77,57 @@ test('a far model keeps its minimum pixel size', async () => {
|
||||
const matrix = new Matrix4();
|
||||
mover.group.meshes[0].getMatrixAt(0, matrix);
|
||||
const scale = new Vector3().setFromMatrixScale(matrix).x;
|
||||
// Box radius √3; 40 px at 100 km with a 60° fov over 800 px.
|
||||
// Box radius √3; 40 px at 100 km (less the radius, as Cesium) with a 60°
|
||||
// fov over 800 px.
|
||||
const footprint = (2 * Math.tan(Math.PI / 6)) / 800;
|
||||
const expected = (40 * footprint * 100_000) / (2 * Math.sqrt(3));
|
||||
const expected =
|
||||
(40 * footprint * (100_000 - Math.sqrt(3))) / (2 * Math.sqrt(3));
|
||||
assert.ok(
|
||||
Math.abs(scale - expected) / expected < 1e-6,
|
||||
`${scale} vs ${expected}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("the instance colour 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(),
|
||||
});
|
||||
store.set(0, {
|
||||
position: { x: 6_378_137, y: 0, z: 0 },
|
||||
color: [1, 1, 1, 0.4],
|
||||
});
|
||||
store.setCount(1);
|
||||
mover.commit(store.dirty());
|
||||
await mover.group.ready;
|
||||
const [mesh] = mover.group.meshes;
|
||||
const shader = {
|
||||
uniforms: {},
|
||||
vertexShader: 'void main() {\n#include <begin_vertex>\n}',
|
||||
fragmentShader:
|
||||
'void main() {\n#include <map_fragment>\n#include <emissivemap_fragment>\n}',
|
||||
};
|
||||
mesh.material.onBeforeCompile(shader);
|
||||
assert.match(
|
||||
shader.fragmentShader,
|
||||
/diffuseColor\.rgb = mix\(diffuseColor\.rgb, vGevColor\.rgb, uGevBlend\);\s*diffuseColor\.a \*= vGevColor\.a;/,
|
||||
);
|
||||
// Ambient light on the tinted surface (Cesium's image-based lighting).
|
||||
assert.match(
|
||||
shader.fragmentShader,
|
||||
/totalEmissiveRadiance \+= diffuseColor\.rgb \* uGevAmbient;/,
|
||||
);
|
||||
assert.equal(mover.uniforms.uGevAmbient.value, 0.5);
|
||||
assert.equal(mesh.material.transparent, true, 'a faded instance');
|
||||
store.set(0, { color: [1, 1, 1, 1] });
|
||||
mover.commit(store.dirty());
|
||||
assert.equal(mesh.material.transparent, false, 'all opaque again');
|
||||
});
|
||||
|
||||
test("the minimum pixel size is taken at Cesium's distance: view depth less the radius", () => {
|
||||
const ahead = { x: 0, y: 0, z: -1 };
|
||||
// 3 km ahead and 4 km aside: depth 3 km, not the 5 km straight line.
|
||||
assert.equal(modelPixelDistance(4000, 0, -3000, ahead, 30), 2970);
|
||||
assert.equal(modelPixelDistance(0, 0, -10, ahead, 30), 0);
|
||||
assert.equal(modelPixelDistance(4000, 0, -3000, null, 0), 5000);
|
||||
});
|
||||
@@ -30,7 +30,13 @@ export function aircraftQaDrawn(state) {
|
||||
}
|
||||
: null,
|
||||
model: model
|
||||
? { scale: model.scale, show: model.show, ready: model.ready }
|
||||
? {
|
||||
scale: model.scale,
|
||||
// The drawn scale, after the minimum pixel size.
|
||||
computedScale: model.computedScale,
|
||||
show: model.show,
|
||||
ready: model.ready,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,15 +27,15 @@ import { registerViewerEngine } from '../../../globe/cesium/engineFromViewer.js'
|
||||
const BASE_SIZE_PX = 20;
|
||||
|
||||
/**
|
||||
* The model frame → glTF scene axes on Navara. Cesium draws a glTF through
|
||||
* Y_UP_TO_Z_UP × Z_UP_TO_X_UP (data/modelVisualAnchor.js); Navara's glTF
|
||||
* loader already turns the scene Y-up to Z-up, so here it is Z_UP_TO_X_UP
|
||||
* alone: nose, top and heading of airplane.glb where Cesium draws them
|
||||
* (checked side by side with Cesium on a tracked aircraft at 300 m against
|
||||
* five other axis candidates).
|
||||
* The model frame → glTF scene axes on Navara: Cesium's own glTF axis
|
||||
* correction, Y_UP_TO_Z_UP × Z_UP_TO_X_UP (data/modelVisualAnchor.js).
|
||||
* Navara's glTF loader is three's GLTFLoader, which keeps the scene Y-up.
|
||||
* (10b used Z_UP_TO_X_UP alone, on the belief that the loader turned the
|
||||
* scene Z-up: every fleet model drew 90° off its heading — side by side
|
||||
* with Cesium at the near pose, 10b review item 3.)
|
||||
*/
|
||||
// Cesium's Axis.Z_UP_TO_X_UP, column-major.
|
||||
const NAVARA_GLTF_AXIS = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1];
|
||||
// Cesium's Axis.Y_UP_TO_Z_UP × Axis.Z_UP_TO_X_UP, column-major.
|
||||
const NAVARA_GLTF_AXIS = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1];
|
||||
const FIRST_CAPACITY = 1024;
|
||||
const ATLAS_PAD = 2;
|
||||
const TRAIL_ALPHA = 0.85;
|
||||
@@ -644,6 +644,8 @@ export function createAircraftNavaraRenderer({
|
||||
return bank;
|
||||
};
|
||||
|
||||
const scratchOffset = geo.cartesian3.create();
|
||||
|
||||
class ModelHandle {
|
||||
constructor(spec) {
|
||||
this.spec = spec;
|
||||
@@ -669,9 +671,13 @@ export function createAircraftNavaraRenderer({
|
||||
const minimum = this.spec.minimumPixelSize ?? 0;
|
||||
if (!(minimum > 0)) return this._scale;
|
||||
const cam = freshCamera();
|
||||
const distance = geo.cartesian3.distance(
|
||||
cam.positionWC,
|
||||
this.translation,
|
||||
// Cesium's distance (Camera distanceToBoundingSphere): the depth
|
||||
// along the view less the radius at the model's own scale.
|
||||
geo.cartesian3.subtract(this.translation, cam.positionWC, scratchOffset);
|
||||
const distance = Math.max(
|
||||
0,
|
||||
Math.abs(geo.cartesian3.dot(scratchOffset, cam.directionWC)) -
|
||||
this.radiusM * this._scale,
|
||||
);
|
||||
const height = engine.host.size().height;
|
||||
if (!(distance > 0) || !(height > 0)) return this._scale;
|
||||
|
||||
@@ -227,11 +227,15 @@ test("a model pose is the Cesium model frame, turned to Navara's glTF axes", asy
|
||||
);
|
||||
for (let index = 0; index < 16; index++)
|
||||
assert.ok(Math.abs(model.modelMatrix[index] - expected[index]) < 1e-6);
|
||||
// The instance orientation: modelMatrix × Cesium's Z_UP_TO_X_UP (Navara's
|
||||
// loader already turned the glTF Z-up).
|
||||
// The instance orientation: modelMatrix × Cesium's glTF axis correction
|
||||
// (Y_UP_TO_Z_UP × Z_UP_TO_X_UP; Navara's loader keeps the glTF Y-up).
|
||||
const corrected = Cesium.Matrix4.multiplyTransformation(
|
||||
expected,
|
||||
Cesium.Axis.Z_UP_TO_X_UP,
|
||||
Cesium.Matrix4.multiplyTransformation(
|
||||
Cesium.Axis.Y_UP_TO_Z_UP,
|
||||
Cesium.Axis.Z_UP_TO_X_UP,
|
||||
new Cesium.Matrix4(),
|
||||
),
|
||||
new Cesium.Matrix4(),
|
||||
);
|
||||
const q = Cesium.Quaternion.fromRotationMatrix(
|
||||
|
||||
Reference in new issue
Block a user