@hztxi/cesium-engine 25.0.3 → 25.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/Build/Workers/chunk-3B2TIEWW.js +73 -0
  2. package/Build/Workers/chunk-3HOOLJ6E.js +1483 -0
  3. package/Build/Workers/chunk-3MWOYCFC.js +511 -0
  4. package/Build/Workers/chunk-3Q52BERF.js +476 -0
  5. package/Build/Workers/chunk-4MHUA5ZK.js +163 -0
  6. package/Build/Workers/chunk-5BVKUNRB.js +368 -0
  7. package/Build/Workers/chunk-642GMGGP.js +162 -0
  8. package/Build/Workers/chunk-65LZTCWG.js +1344 -0
  9. package/Build/Workers/chunk-6PYOAYJD.js +1046 -0
  10. package/Build/Workers/chunk-AH6GJ5MT.js +754 -0
  11. package/Build/Workers/chunk-AM56LGEB.js +352 -0
  12. package/Build/Workers/chunk-CADDH6MK.js +140 -0
  13. package/Build/Workers/chunk-CEIY3YH2.js +57 -0
  14. package/Build/Workers/chunk-CJT3FTZK.js +58 -0
  15. package/Build/Workers/chunk-CV7K25H3.js +138 -0
  16. package/Build/Workers/chunk-DNEERCHO.js +171 -0
  17. package/Build/Workers/chunk-EHXAKTME.js +147 -0
  18. package/Build/Workers/chunk-FF6JKPLF.js +454 -0
  19. package/Build/Workers/chunk-FTRHXHI5.js +3794 -0
  20. package/Build/Workers/chunk-GH2D6MPO.js +497 -0
  21. package/Build/Workers/chunk-GHVEEVR7.js +778 -0
  22. package/Build/Workers/chunk-HUNCRVVV.js +1067 -0
  23. package/Build/Workers/chunk-IFSGYIDE.js +257 -0
  24. package/Build/Workers/chunk-ISRYZ3F4.js +414 -0
  25. package/Build/Workers/chunk-JBJ5PC2D.js +102 -0
  26. package/Build/Workers/chunk-JFPBQCDB.js +398 -0
  27. package/Build/Workers/chunk-JKPELBSI.js +682 -0
  28. package/Build/Workers/chunk-JLLWLD6O.js +973 -0
  29. package/Build/Workers/chunk-KCUS7QEU.js +387 -0
  30. package/Build/Workers/chunk-KFRKYG4B.js +288 -0
  31. package/Build/Workers/chunk-LERIK6IA.js +219 -0
  32. package/Build/Workers/chunk-LOVXQI64.js +2719 -0
  33. package/Build/Workers/chunk-MD7CEJO5.js +2342 -0
  34. package/Build/Workers/chunk-OKPQ3QBG.js +124 -0
  35. package/Build/Workers/chunk-PC7533X2.js +1800 -0
  36. package/Build/Workers/chunk-PCXE4XMI.js +82 -0
  37. package/Build/Workers/chunk-PXPUYOFJ.js +1287 -0
  38. package/Build/Workers/chunk-QX6AYJX2.js +455 -0
  39. package/Build/Workers/chunk-RSJUCJPN.js +241 -0
  40. package/Build/Workers/chunk-SPTT255E.js +301 -0
  41. package/Build/Workers/chunk-STXWAEVD.js +101 -0
  42. package/Build/Workers/chunk-SUG6ECWR.js +76 -0
  43. package/Build/Workers/chunk-TPT5IM72.js +280 -0
  44. package/Build/Workers/chunk-TTG2JNFN.js +198 -0
  45. package/Build/Workers/chunk-UDXUBVNR.js +1992 -0
  46. package/Build/Workers/chunk-UNXJWCXV.js +9230 -0
  47. package/Build/Workers/chunk-UTKCX3AS.js +352 -0
  48. package/Build/Workers/chunk-VL2WHGQI.js +305 -0
  49. package/Build/Workers/chunk-W37YFRTG.js +38 -0
  50. package/Build/Workers/chunk-W5AGSDWX.js +44 -0
  51. package/Build/Workers/chunk-WBQDTVCN.js +97 -0
  52. package/Build/Workers/chunk-WDSHSR57.js +837 -0
  53. package/Build/Workers/chunk-XPIDT6UT.js +1054 -0
  54. package/Build/Workers/chunk-XQETP7YD.js +3201 -0
  55. package/Build/Workers/chunk-Z3KHKVNA.js +56 -0
  56. package/Build/Workers/chunk-Z7XRKD6H.js +693 -0
  57. package/Build/Workers/chunk-ZQUSI7YK.js +630 -0
  58. package/Build/Workers/chunk-ZY4DTNR2.js +56 -0
  59. package/Build/Workers/combineGeometry.js +24 -24
  60. package/Build/Workers/createBoxGeometry.js +18 -18
  61. package/Build/Workers/createBoxOutlineGeometry.js +16 -16
  62. package/Build/Workers/createCircleGeometry.js +26 -26
  63. package/Build/Workers/createCircleOutlineGeometry.js +19 -19
  64. package/Build/Workers/createCoplanarPolygonGeometry.js +33 -33
  65. package/Build/Workers/createCoplanarPolygonOutlineGeometry.js +31 -31
  66. package/Build/Workers/createCorridorGeometry.js +29 -29
  67. package/Build/Workers/createCorridorOutlineGeometry.js +28 -28
  68. package/Build/Workers/createCylinderGeometry.js +20 -20
  69. package/Build/Workers/createCylinderOutlineGeometry.js +18 -18
  70. package/Build/Workers/createEllipseGeometry.js +26 -26
  71. package/Build/Workers/createEllipseOutlineGeometry.js +19 -19
  72. package/Build/Workers/createEllipsoidGeometry.js +19 -19
  73. package/Build/Workers/createEllipsoidOutlineGeometry.js +18 -18
  74. package/Build/Workers/createFrustumGeometry.js +18 -18
  75. package/Build/Workers/createFrustumOutlineGeometry.js +18 -18
  76. package/Build/Workers/createGeometry.js +24 -24
  77. package/Build/Workers/createGroundPolylineGeometry.js +23 -23
  78. package/Build/Workers/createPlaneGeometry.js +16 -16
  79. package/Build/Workers/createPlaneOutlineGeometry.js +15 -15
  80. package/Build/Workers/createPolygonGeometry.js +32 -32
  81. package/Build/Workers/createPolygonOutlineGeometry.js +30 -30
  82. package/Build/Workers/createPolylineGeometry.js +25 -25
  83. package/Build/Workers/createPolylineVolumeGeometry.js +31 -31
  84. package/Build/Workers/createPolylineVolumeOutlineGeometry.js +27 -27
  85. package/Build/Workers/createRectangleGeometry.js +27 -27
  86. package/Build/Workers/createRectangleOutlineGeometry.js +20 -20
  87. package/Build/Workers/createSimplePolylineGeometry.js +23 -23
  88. package/Build/Workers/createSphereGeometry.js +19 -19
  89. package/Build/Workers/createSphereOutlineGeometry.js +18 -18
  90. package/Build/Workers/createTaskProcessorWorker.js +3 -3
  91. package/Build/Workers/createVectorTileClampedPolylines.js +14 -14
  92. package/Build/Workers/createVectorTileGeometries.js +24 -24
  93. package/Build/Workers/createVectorTilePoints.js +13 -13
  94. package/Build/Workers/createVectorTilePolygons.js +22 -22
  95. package/Build/Workers/createVectorTilePolylines.js +14 -14
  96. package/Build/Workers/createVerticesFromCesium3DTilesTerrain.js +26 -26
  97. package/Build/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js +23 -23
  98. package/Build/Workers/createVerticesFromHeightmap.js +23 -23
  99. package/Build/Workers/createVerticesFromQuantizedTerrainMesh.js +21 -21
  100. package/Build/Workers/createWallGeometry.js +24 -24
  101. package/Build/Workers/createWallOutlineGeometry.js +23 -23
  102. package/Build/Workers/decodeDraco.js +10 -10
  103. package/Build/Workers/decodeGoogleEarthEnterprisePacket.js +5 -5
  104. package/Build/Workers/decodeI3S.js +10 -10
  105. package/Build/Workers/gaussianSplatSorter.js +4 -4
  106. package/Build/Workers/gaussianSplatTextureGenerator.js +4 -4
  107. package/Build/Workers/incrementallyBuildTerrainPicker.js +10 -10
  108. package/Build/Workers/transcodeKTX2.js +6 -6
  109. package/Build/Workers/transferTypedArrayTest.js +1 -1
  110. package/Build/Workers/upsampleQuantizedTerrainMesh.js +23 -23
  111. package/Build/Workers/upsampleVerticesFromCesium3DTilesTerrain.js +26 -26
  112. package/Source/Scene/Globe.js +55 -0
  113. package/Source/Scene/GlobeSurfaceShaderSet.js +132 -1
  114. package/Source/Scene/GlobeSurfaceTileProvider.js +378 -2
  115. package/Source/Scene/ImageryLayer.js +244 -10
  116. package/Source/Scene/QuadtreePrimitive.js +256 -2
  117. package/Source/Shaders/GlobeFS.glsl +43 -0
  118. package/Source/Shaders/GlobeFS.js +43 -0
  119. package/Source/ThirdParty/zip-module.wasm +0 -0
  120. package/index.js +371 -371
  121. package/package.json +10 -10
@@ -1,4 +1,5 @@
1
1
  import Cartesian2 from "../Core/Cartesian2.js";
2
+ import Cartesian3 from "../Core/Cartesian3.js";
2
3
  import Cartesian4 from "../Core/Cartesian4.js";
3
4
  import Check from "../Core/Check.js";
4
5
  import createWorldImageryAsync from "../Scene/createWorldImageryAsync.js";
@@ -171,12 +172,92 @@ import TileImagery from "./TileImagery.js";
171
172
  * const imageryLayer = new ImageryLayer(imageryProvider);
172
173
  * tileset.imageryLayers.add(imageryLayer);
173
174
  */
175
+ // Clamps chosen so a mistyped profile cannot wash out the imagery entirely.
176
+ const LEVEL_COLOR_MIN_SCALE = 0.5;
177
+ const LEVEL_COLOR_MAX_SCALE = 2.0;
178
+ const LEVEL_COLOR_MAX_OFFSET = 0.25;
179
+
180
+ function clampLevelColorValue(value, min, max) {
181
+ return Math.min(Math.max(value, min), max);
182
+ }
183
+
184
+ /**
185
+ * Converts a public levelColorProfile object into per-level gain/bias pairs
186
+ * with the correction strength already folded in:
187
+ * gain = 1 + strength * (scale - 1), bias = strength * offset.
188
+ *
189
+ * @param {object|undefined} profile The public profile object.
190
+ * @returns {object|undefined} Map of level to {gain, bias}, or undefined.
191
+ * @private
192
+ */
193
+ function normalizeLevelColorProfile(profile) {
194
+ if (!defined(profile) || !defined(profile.levels)) {
195
+ return undefined;
196
+ }
197
+
198
+ const strength = clampLevelColorValue(profile.strength ?? 1.0, 0.0, 1.0);
199
+ const entries = {};
200
+ for (const level of Object.keys(profile.levels)) {
201
+ const entry = profile.levels[level];
202
+ if (!defined(entry)) {
203
+ continue;
204
+ }
205
+ const scale = defined(entry.scale) ? entry.scale : [1, 1, 1];
206
+ const offset = defined(entry.offset) ? entry.offset : [0, 0, 0];
207
+ entries[level] = {
208
+ gain: new Cartesian3(
209
+ clampLevelColorValue(
210
+ 1.0 + strength * (scale[0] - 1.0),
211
+ LEVEL_COLOR_MIN_SCALE,
212
+ LEVEL_COLOR_MAX_SCALE,
213
+ ),
214
+ clampLevelColorValue(
215
+ 1.0 + strength * (scale[1] - 1.0),
216
+ LEVEL_COLOR_MIN_SCALE,
217
+ LEVEL_COLOR_MAX_SCALE,
218
+ ),
219
+ clampLevelColorValue(
220
+ 1.0 + strength * (scale[2] - 1.0),
221
+ LEVEL_COLOR_MIN_SCALE,
222
+ LEVEL_COLOR_MAX_SCALE,
223
+ ),
224
+ ),
225
+ bias: new Cartesian3(
226
+ clampLevelColorValue(
227
+ strength * offset[0],
228
+ -LEVEL_COLOR_MAX_OFFSET,
229
+ LEVEL_COLOR_MAX_OFFSET,
230
+ ),
231
+ clampLevelColorValue(
232
+ strength * offset[1],
233
+ -LEVEL_COLOR_MAX_OFFSET,
234
+ LEVEL_COLOR_MAX_OFFSET,
235
+ ),
236
+ clampLevelColorValue(
237
+ strength * offset[2],
238
+ -LEVEL_COLOR_MAX_OFFSET,
239
+ LEVEL_COLOR_MAX_OFFSET,
240
+ ),
241
+ ),
242
+ };
243
+ }
244
+ return { entries };
245
+ }
246
+
174
247
  function ImageryLayer(imageryProvider, options) {
175
248
  this._imageryProvider = imageryProvider;
176
249
 
177
250
  this._readyEvent = new Event();
178
251
  this._errorEvent = new Event();
179
252
 
253
+ // Normalized form of the public levelColorProfile: per-level gain/bias
254
+ // Cartesian3 pairs with the correction strength already folded in.
255
+ this._levelColorProfile = undefined;
256
+ this._levelColorProfileInput = undefined;
257
+
258
+ this._levelColorEdgeBlend = 0.0;
259
+ this._levelColorEdgeCrossFade = true;
260
+
180
261
  options = options ?? Frozen.EMPTY_OBJECT;
181
262
  imageryProvider = imageryProvider ?? Frozen.EMPTY_OBJECT;
182
263
 
@@ -372,6 +453,116 @@ Object.defineProperties(ImageryLayer.prototype, {
372
453
  },
373
454
  },
374
455
 
456
+ /**
457
+ * Gets or sets a per-level color correction profile for this layer. Imagery
458
+ * pyramids from third-party providers are frequently assembled from different
459
+ * mosaic vintages per level, which shows up as a visible color seam wherever
460
+ * two levels are rendered next to each other (typically in tilted views).
461
+ * The profile applies one per-channel affine transform
462
+ * (<code>scale * color + offset</code>, normalized [0, 1] units) to every
463
+ * tile of a given level, which makes adjacent levels visually continuous.
464
+ * Levels not listed in the profile are left uncorrected.
465
+ *
466
+ * The profile is a plain object, typically generated by the
467
+ * <code>Apps/LevelColorCalibration.html</code> tool, of the form:
468
+ * <pre>{
469
+ * "strength": 0.8,
470
+ * "referenceLevel": 16,
471
+ * "levels": {
472
+ * "14": { "scale": [1.02, 1.01, 0.98], "offset": [-0.01, 0.0, 0.02] },
473
+ * "17": { "scale": [0.99, 1.0, 1.03], "offset": [0.0, -0.01, 0.03] }
474
+ * }
475
+ * }</pre>
476
+ * <code>strength</code> (default <code>1.0</code>) blends between the original
477
+ * and corrected color, <code>(1 - strength) * original + strength * (scale * original + offset)</code>,
478
+ * so values below 1 keep some of the original per-level character.
479
+ *
480
+ * Assigning a profile takes effect immediately, without reloading tiles;
481
+ * setting it to <code>undefined</code> disables the correction.
482
+ *
483
+ * @memberof ImageryLayer.prototype
484
+ * @type {object|undefined}
485
+ * @default undefined
486
+ *
487
+ * @example
488
+ * const response = await fetch("level-color-profile.json");
489
+ * layer.levelColorProfile = await response.json();
490
+ */
491
+ levelColorProfile: {
492
+ get: function () {
493
+ return this._levelColorProfileInput;
494
+ },
495
+ set: function (value) {
496
+ this._levelColorProfileInput = value;
497
+ this._levelColorProfile = normalizeLevelColorProfile(value);
498
+ },
499
+ },
500
+
501
+ /**
502
+ * Gets or sets the width, as a fraction of the tile's extent, of an
503
+ * edge band where this tile's {@link ImageryLayer#levelColorProfile}
504
+ * correction is smoothly faded into the correction of the adjacent
505
+ * coarser tile's level. Residual color steps at imagery LOD boundaries
506
+ * (levels that cannot fully converge, or local deviations from the
507
+ * calibration statistics) become gradual ramps, which are far less
508
+ * visible than abrupt steps. The fade is one-sided: only the finer
509
+ * side of a boundary fades, so both sides apply the same correction
510
+ * exactly at the shared edge. Requires a levelColorProfile; edges
511
+ * facing same-level or finer neighbors are not blended.
512
+ *
513
+ * The value is clamped to [0.0, 0.45]; 0 disables the fade. Values
514
+ * between 0.15 and 0.25 are a good starting point. Takes effect
515
+ * immediately (uniform-only after the first enabling, which compiles
516
+ * one additional shader variant).
517
+ *
518
+ * @memberof ImageryLayer.prototype
519
+ * @type {number}
520
+ * @default 0.0
521
+ */
522
+ levelColorEdgeBlend: {
523
+ get: function () {
524
+ return this._levelColorEdgeBlend;
525
+ },
526
+ set: function (value) {
527
+ //>>includeStart('debug', pragmas.debug);
528
+ if (defined(value) && typeof value !== "number") {
529
+ throw new DeveloperError("value must be a number.");
530
+ }
531
+ //>>includeEnd('debug');
532
+ this._levelColorEdgeBlend = defined(value)
533
+ ? CesiumMath.clamp(value, 0.0, 0.45)
534
+ : 0.0;
535
+ },
536
+ },
537
+ /**
538
+ * Gets or sets whether the {@link ImageryLayer#levelColorEdgeBlend} band
539
+ * cross-fades the imagery CONTENT with the ancestor imagery at the
540
+ * adjacent coarser tile's level (the very texture the coarser side
541
+ * renders), instead of only fading the correction parameters. Content
542
+ * cross-fading makes both sides of a boundary pixel-identical at the
543
+ * shared edge, which also removes seams caused by content differences
544
+ * (mosaic vintage, local deviations from the calibration statistics)
545
+ * that a correction-parameter fade cannot touch. When the ancestor
546
+ * texture is unavailable, the parameter fade is used as a fallback.
547
+ * Requires a levelColorProfile and levelColorEdgeBlend &gt; 0.
548
+ *
549
+ * @memberof ImageryLayer.prototype
550
+ * @type {boolean}
551
+ * @default true
552
+ */
553
+ levelColorEdgeCrossFade: {
554
+ get: function () {
555
+ return this._levelColorEdgeCrossFade;
556
+ },
557
+ set: function (value) {
558
+ //>>includeStart('debug', pragmas.debug);
559
+ if (defined(value) && typeof value !== "boolean") {
560
+ throw new DeveloperError("value must be a boolean.");
561
+ }
562
+ //>>includeEnd('debug');
563
+ this._levelColorEdgeCrossFade = value ?? true;
564
+ },
565
+ },
375
566
  /**
376
567
  * Returns true when the terrain provider has been successfully created. Otherwise, returns false.
377
568
  * @memberof ImageryLayer.prototype
@@ -1047,6 +1238,21 @@ ImageryLayer.prototype._createTileImagerySkeletons = function (
1047
1238
  return true;
1048
1239
  };
1049
1240
 
1241
+ /**
1242
+ * Returns the normalized color correction for tiles of the given imagery
1243
+ * level, or <code>undefined</code> if the level is not in this layer's
1244
+ * {@link ImageryLayer#levelColorProfile}.
1245
+ *
1246
+ * @param {number} level The imagery level.
1247
+ * @returns {object|undefined} An object with <code>gain</code> and
1248
+ * <code>bias</code> {@link Cartesian3} properties.
1249
+ *
1250
+ * @private
1251
+ */
1252
+ ImageryLayer.prototype._getLevelColorCorrection = function (level) {
1253
+ return this._levelColorProfile?.entries[level];
1254
+ };
1255
+
1050
1256
  /**
1051
1257
  * Calculate the translation and scale for a particular {@link TileImagery} attached to a
1052
1258
  * particular terrain tile.
@@ -1062,12 +1268,39 @@ ImageryLayer.prototype._calculateTextureTranslationAndScale = function (
1062
1268
  tile,
1063
1269
  tileImagery,
1064
1270
  ) {
1065
- let imageryRectangle = tileImagery.readyImagery.rectangle;
1271
+ return this._calculateImageryTranslationAndScale(
1272
+ tile,
1273
+ tileImagery.readyImagery,
1274
+ tileImagery.useWebMercatorT,
1275
+ new Cartesian4(),
1276
+ );
1277
+ };
1278
+
1279
+ /**
1280
+ * Calculates the translation and scale mapping a terrain tile's 0..1 UV into the
1281
+ * UV space of an arbitrary {@link Imagery} of this layer covering the tile,
1282
+ * writing into {@link result}. Works for coarser ancestors as well: the math only
1283
+ * depends on the two rectangles.
1284
+ *
1285
+ * @private
1286
+ *
1287
+ * @param {Tile} tile The terrain tile.
1288
+ * @param {Imagery} imagery The imagery whose texture space to map into.
1289
+ * @param {boolean} useWebMercatorT Whether the sampling tile uses the Web Mercator T coordinate.
1290
+ * @param {Cartesian4} result The Cartesian4 (x/y translation, z/w scale) to fill.
1291
+ * @returns {Cartesian4} The result.
1292
+ */
1293
+ ImageryLayer.prototype._calculateImageryTranslationAndScale = function (
1294
+ tile,
1295
+ imagery,
1296
+ useWebMercatorT,
1297
+ result,
1298
+ ) {
1299
+ let imageryRectangle = imagery.rectangle;
1066
1300
  let terrainRectangle = tile.rectangle;
1067
1301
 
1068
- if (tileImagery.useWebMercatorT) {
1069
- const tilingScheme =
1070
- tileImagery.readyImagery.imageryLayer.imageryProvider.tilingScheme;
1302
+ if (useWebMercatorT) {
1303
+ const tilingScheme = this.imageryProvider.tilingScheme;
1071
1304
  imageryRectangle = tilingScheme.rectangleToNativeRectangle(
1072
1305
  imageryRectangle,
1073
1306
  imageryBoundsScratch,
@@ -1083,13 +1316,14 @@ ImageryLayer.prototype._calculateTextureTranslationAndScale = function (
1083
1316
 
1084
1317
  const scaleX = terrainWidth / imageryRectangle.width;
1085
1318
  const scaleY = terrainHeight / imageryRectangle.height;
1086
- return new Cartesian4(
1087
- (scaleX * (terrainRectangle.west - imageryRectangle.west)) / terrainWidth,
1319
+ result.x =
1320
+ (scaleX * (terrainRectangle.west - imageryRectangle.west)) / terrainWidth;
1321
+ result.y =
1088
1322
  (scaleY * (terrainRectangle.south - imageryRectangle.south)) /
1089
- terrainHeight,
1090
- scaleX,
1091
- scaleY,
1092
- );
1323
+ terrainHeight;
1324
+ result.z = scaleX;
1325
+ result.w = scaleY;
1326
+ return result;
1093
1327
  };
1094
1328
 
1095
1329
  /**
@@ -4,6 +4,7 @@ import defined from "../Core/defined.js";
4
4
  import DeveloperError from "../Core/DeveloperError.js";
5
5
  import Event from "../Core/Event.js";
6
6
  import getTimestamp from "../Core/getTimestamp.js";
7
+ import IntersectionTests from "../Core/IntersectionTests.js";
7
8
  import CesiumMath from "../Core/Math.js";
8
9
  import Matrix4 from "../Core/Matrix4.js";
9
10
  import OrthographicFrustum from "../Core/OrthographicFrustum.js";
@@ -109,6 +110,36 @@ function QuadtreePrimitive(options) {
109
110
  */
110
111
  this.maximumScreenSpaceError = options.maximumScreenSpaceError ?? 2;
111
112
 
113
+ /**
114
+ * Gets or sets the uniform level-of-detail mode configuration, pushed down
115
+ * from {@link Globe#uniformLevelMode} every frame. When enabled, tile
116
+ * refinement is evaluated against a single per-frame reference distance
117
+ * instead of each tile's own distance, so visible tiles converge on one
118
+ * level instead of a distance-dependent mix. See updateUniformLevelState
119
+ * for the recognized fields.
120
+ *
121
+ * @type {object|undefined}
122
+ * @default undefined
123
+ */
124
+ this.uniformLevelMode = undefined;
125
+
126
+ // Per-frame uniform-level state, recomputed in beginFrame and consumed by
127
+ // screenSpaceError. version marks the uniformLevelMode implementation
128
+ // revision (1: initial, 2: nearest-fallback + clamp for Columbus,
129
+ // 3: maxCoarsenLevels near-side clamp); it lets callers detect which
130
+ // engine build they are actually running against.
131
+ this._uniformLevelState = {
132
+ version: 3,
133
+ active: false,
134
+ engage: 0.0,
135
+ referenceDistance: undefined,
136
+ deviationFactor: 8.0,
137
+ coarsenFactor: 2.0,
138
+ };
139
+ this._nearestTileDistanceThisFrame = undefined;
140
+ this._nearestTileDistanceLastFrame = undefined;
141
+ this._uniformLevelFrameNumber = -1;
142
+
112
143
  /**
113
144
  * Gets or sets the maximum number of tiles that will be retained in the tile cache.
114
145
  * Note that tiles will never be unloaded if they were used for rendering the last
@@ -343,6 +374,8 @@ QuadtreePrimitive.prototype.beginFrame = function (frameState) {
343
374
 
344
375
  clearTileLoadQueue(this);
345
376
 
377
+ updateUniformLevelState(this, frameState);
378
+
346
379
  if (this._debug.suspendLodUpdate) {
347
380
  return;
348
381
  }
@@ -722,6 +755,20 @@ function visitTile(
722
755
 
723
756
  ++debug.tilesVisited;
724
757
 
758
+ // Track the nearest visible tile distance for uniform-level mode's
759
+ // "nearest" reference. computeTileVisibility has already refreshed
760
+ // tile._distance for this frame by the time a tile is visited.
761
+ const uniformMode = primitive.uniformLevelMode;
762
+ if (defined(uniformMode) && uniformMode.enabled === true) {
763
+ const tileDistance = tile._distance;
764
+ if (defined(tileDistance)) {
765
+ const nearest = primitive._nearestTileDistanceThisFrame;
766
+ if (!defined(nearest) || tileDistance < nearest) {
767
+ primitive._nearestTileDistanceThisFrame = tileDistance;
768
+ }
769
+ }
770
+ }
771
+
725
772
  primitive._tileReplacementQueue.markTileRendered(tile);
726
773
  tile.updateCustomData();
727
774
 
@@ -1245,6 +1292,176 @@ function visitIfVisible(
1245
1292
  tile._lastSelectionResultFrame = frameState.frameNumber;
1246
1293
  }
1247
1294
 
1295
+ const uniformLevelScratchUp = new Cartesian3();
1296
+ const uniformLevelScratchRay = new Ray();
1297
+
1298
+ /**
1299
+ * Recomputes the per-frame uniform-level state consumed by screenSpaceError.
1300
+ *
1301
+ * Uniform-level mode makes tiles in view converge on one LOD level by
1302
+ * evaluating refinement against a single per-frame reference distance instead
1303
+ * of each tile's own distance:
1304
+ *
1305
+ * reference: "center" (default) uses the camera-to-globe-center distance
1306
+ * along the view ray; "nearest" uses the distance of the nearest
1307
+ * visible tile from the previous frame.
1308
+ * maxDeviationLevels (default 3) caps how many levels above the natural LOD
1309
+ * far tiles may be forced to refine, bounding the tile-count
1310
+ * increase toward the horizon.
1311
+ * maxCoarsenLevels (default 1, clamped to [0, 10]) caps how many levels
1312
+ * below the natural LOD near (foreground) tiles may be held
1313
+ * back. Uncapped, the foreground of a tilted far view lands
1314
+ * 2+ levels coarser than natural, and verticalExaggeration > 1
1315
+ * multiplies the interpolation error of coarse terrain
1316
+ * (1 level = 4x, 2 levels = 16x) into visible tearing. 1
1317
+ * measures +0~4% tile requests; 0 makes the foreground fully
1318
+ * natural (+5~69% requests) but mixes near/far levels again,
1319
+ * bringing back the dataset's cross-level color bands.
1320
+ * pitchRangeDegrees (default undefined = always engaged) is a [full, none]
1321
+ * pair of pitch angles; the effect fades out between them as the
1322
+ * view approaches the horizon. Pass null or leave undefined to
1323
+ * keep it fully engaged (e.g. for offline export).
1324
+ *
1325
+ * The effect is blended in by an engage factor, and the reference distance is
1326
+ * EMA-smoothed (snapping across large jumps, e.g. camera cuts during export)
1327
+ * so the selected level does not flap frame to frame.
1328
+ *
1329
+ * @private
1330
+ */
1331
+ function updateUniformLevelState(primitive, frameState) {
1332
+ const state = primitive._uniformLevelState;
1333
+ state.active = false;
1334
+ state.engage = 0.0;
1335
+
1336
+ // Roll over nearest-tile-distance tracking once per frame. Multiple render
1337
+ // passes can occur within one frame (as in 2D), so key on frame number.
1338
+ if (primitive._uniformLevelFrameNumber !== frameState.frameNumber) {
1339
+ primitive._uniformLevelFrameNumber = frameState.frameNumber;
1340
+ primitive._nearestTileDistanceLastFrame =
1341
+ primitive._nearestTileDistanceThisFrame;
1342
+ primitive._nearestTileDistanceThisFrame = undefined;
1343
+ }
1344
+
1345
+ const mode = primitive.uniformLevelMode;
1346
+ if (!defined(mode) || mode.enabled !== true) {
1347
+ // Drop stale smoothing state so re-enabling starts fresh.
1348
+ state.referenceDistance = undefined;
1349
+ return;
1350
+ }
1351
+
1352
+ // The 3D SSE path (perspective frustum) is the distance-dependent one, and
1353
+ // it is used in both SCENE3D and Columbus view, so both need the mode.
1354
+ // True 2D (SCENE2D) SSE is already distance-independent.
1355
+ const isColumbusView = frameState.mode === SceneMode.COLUMBUS_VIEW;
1356
+ if (frameState.mode !== SceneMode.SCENE3D && !isColumbusView) {
1357
+ return;
1358
+ }
1359
+
1360
+ const camera = frameState.camera;
1361
+ const alignment = CesiumMath.clamp(
1362
+ -Cartesian3.dot(
1363
+ Cartesian3.normalize(camera.positionWC, uniformLevelScratchUp),
1364
+ camera.directionWC,
1365
+ ),
1366
+ 0.0,
1367
+ 1.0,
1368
+ );
1369
+ // alignment is 1.0 looking straight down and 0.0 at or above the horizon.
1370
+
1371
+ let engage = 1.0;
1372
+ const pitchRange = mode.pitchRangeDegrees;
1373
+ if (defined(pitchRange) && pitchRange !== null) {
1374
+ const fullAlignment = Math.sin(
1375
+ CesiumMath.toRadians(Math.abs(pitchRange[0])),
1376
+ );
1377
+ const noneAlignment = Math.sin(
1378
+ CesiumMath.toRadians(Math.abs(pitchRange[1])),
1379
+ );
1380
+ const alignmentRange = Math.max(fullAlignment - noneAlignment, 1e-6);
1381
+ let t = (alignment - noneAlignment) / alignmentRange;
1382
+ t = CesiumMath.clamp(t, 0.0, 1.0);
1383
+ engage = t * t * (3.0 - 2.0 * t);
1384
+ }
1385
+ if (engage <= 0.0) {
1386
+ return;
1387
+ }
1388
+
1389
+ let referenceDistance;
1390
+ const nearestDistance = primitive._nearestTileDistanceLastFrame;
1391
+ if (mode.reference === "nearest") {
1392
+ referenceDistance = nearestDistance;
1393
+ } else if (isColumbusView) {
1394
+ // In Columbus view the flattened map is the x = 0 plane in world
1395
+ // coordinates (see pickMapColumbusView): the center-of-view distance is
1396
+ // the view ray parameter where it crosses that plane.
1397
+ const directionX = camera.directionWC.x;
1398
+ if (directionX !== 0.0) {
1399
+ const scalar = -camera.positionWC.x / directionX;
1400
+ referenceDistance = scalar > 0.0 ? scalar : undefined;
1401
+ }
1402
+ // The plane math assumes the default Columbus embedding; a camera
1403
+ // reference transform (lookAt etc.) or animation-driven position can
1404
+ // break the sign assumptions. Fall back to the tracked nearest tile
1405
+ // distance, which is frame-consistent no matter how the camera got
1406
+ // there.
1407
+ if (!defined(referenceDistance)) {
1408
+ referenceDistance = nearestDistance;
1409
+ }
1410
+ } else {
1411
+ const ray = uniformLevelScratchRay;
1412
+ Cartesian3.clone(camera.positionWC, ray.origin);
1413
+ Cartesian3.clone(camera.directionWC, ray.direction);
1414
+ const intersection = IntersectionTests.rayEllipsoid(
1415
+ ray,
1416
+ primitive._tileProvider.tilingScheme.ellipsoid,
1417
+ );
1418
+ referenceDistance = defined(intersection) ? intersection.start : undefined;
1419
+ }
1420
+ // Horizon-looking views miss the ground entirely; fall back to the nearest
1421
+ // tracked distance there too so the mode stays engaged.
1422
+ if (!defined(referenceDistance)) {
1423
+ referenceDistance = nearestDistance;
1424
+ }
1425
+ // Sanity clamp: keep the reference within an order of magnitude of the
1426
+ // nearest visible tile distance, so a degenerate camera frame cannot
1427
+ // demand levels far outside what visible tiles can plausibly load.
1428
+ if (
1429
+ defined(referenceDistance) &&
1430
+ defined(nearestDistance) &&
1431
+ nearestDistance > 0.0
1432
+ ) {
1433
+ referenceDistance = Math.min(referenceDistance, 8.0 * nearestDistance);
1434
+ }
1435
+ if (!defined(referenceDistance) || referenceDistance <= 0.0) {
1436
+ // No ground along the view ray (or no tracking yet): leave inactive.
1437
+ return;
1438
+ }
1439
+
1440
+ // Smooth the reference distance to avoid level flapping, but snap across
1441
+ // large discontinuities (camera cuts, e.g. per-frame seeks in export).
1442
+ const previous = state.referenceDistance;
1443
+ if (defined(previous) && previous > 0.0) {
1444
+ const ratio = referenceDistance / previous;
1445
+ referenceDistance =
1446
+ ratio < 0.5 || ratio > 2.0
1447
+ ? referenceDistance
1448
+ : previous + 0.25 * (referenceDistance - previous);
1449
+ }
1450
+
1451
+ const maxDeviationLevels = CesiumMath.clamp(
1452
+ mode.maxDeviationLevels ?? 3,
1453
+ 0,
1454
+ 10,
1455
+ );
1456
+ const maxCoarsenLevels = CesiumMath.clamp(mode.maxCoarsenLevels ?? 1, 0, 10);
1457
+
1458
+ state.referenceDistance = referenceDistance;
1459
+ state.engage = engage;
1460
+ state.deviationFactor = Math.pow(2.0, maxDeviationLevels);
1461
+ state.coarsenFactor = Math.pow(2.0, maxCoarsenLevels);
1462
+ state.active = true;
1463
+ }
1464
+
1248
1465
  function screenSpaceError(primitive, frameState, tile) {
1249
1466
  if (
1250
1467
  frameState.mode === SceneMode.SCENE2D ||
@@ -1263,9 +1480,46 @@ function screenSpaceError(primitive, frameState, tile) {
1263
1480
 
1264
1481
  let error = (maxGeometricError * height) / (distance * sseDenominator);
1265
1482
 
1483
+ const uniformLevel = primitive._uniformLevelState;
1484
+ if (uniformLevel.active) {
1485
+ // Uniform-level mode: evaluate this tile as if it sat at the reference
1486
+ // distance, so every tile refines to the level appropriate for one
1487
+ // representative distance instead of its own. The clamp bounds how far
1488
+ // the selection may drift from natural in either direction (each level
1489
+ // is a factor of two in error): far tiles may be pushed at most
1490
+ // maxDeviationLevels levels finer (bounding tile-count growth toward the
1491
+ // horizon), and near tiles held at most maxCoarsenLevels levels coarser.
1492
+ // Without the near-side clamp, the foreground of a tilted far view lands
1493
+ // 2+ levels coarse, and terrain exaggeration (verticalExaggeration > 1)
1494
+ // amplifies coarse-terrain interpolation error (4x per level) into
1495
+ // visible tearing. engage blends between natural and uniform selection.
1496
+ const uniformError =
1497
+ (maxGeometricError * height) /
1498
+ (uniformLevel.referenceDistance * sseDenominator);
1499
+ const cappedUniformError = CesiumMath.clamp(
1500
+ uniformError,
1501
+ error / uniformLevel.coarsenFactor,
1502
+ error * uniformLevel.deviationFactor,
1503
+ );
1504
+ error += (cappedUniformError - error) * uniformLevel.engage;
1505
+ }
1506
+
1266
1507
  if (frameState.fog.enabled) {
1267
- error -=
1268
- CesiumMath.fog(distance, frameState.fog.density) * frameState.fog.sse;
1508
+ let fogAmount = CesiumMath.fog(distance, frameState.fog.density);
1509
+ if (uniformLevel.active) {
1510
+ // Blend the fog SSE relief toward the relief at the reference distance
1511
+ // so every tile gets the same effective threshold. Subtracting each
1512
+ // tile's own (larger, distance-dependent) relief instead would satisfy
1513
+ // the SSE for far tiles on fog alone, vetoing the uniform refinement
1514
+ // and reintroducing cross-level boundaries beyond the fog onset
1515
+ // distance. Fog shading itself is unaffected; only LOD selection.
1516
+ const uniformFogAmount = CesiumMath.fog(
1517
+ uniformLevel.referenceDistance,
1518
+ frameState.fog.density,
1519
+ );
1520
+ fogAmount += (uniformFogAmount - fogAmount) * uniformLevel.engage;
1521
+ }
1522
+ error -= fogAmount * frameState.fog.sse;
1269
1523
  }
1270
1524
 
1271
1525
  error /= frameState.pixelRatio;
@@ -26,6 +26,39 @@ uniform float u_dayTextureBrightness[TEXTURE_UNITS];
26
26
  uniform float u_dayTextureContrast[TEXTURE_UNITS];
27
27
  #endif
28
28
 
29
+ #ifdef APPLY_TONE_MATCH
30
+ uniform vec3 u_dayTextureToneGain[TEXTURE_UNITS];
31
+ uniform vec3 u_dayTextureToneBias[TEXTURE_UNITS];
32
+ #endif
33
+
34
+ #ifdef APPLY_TONE_MATCH_EDGE
35
+ // Per-texture fade targets at the four geographic edges of the surface tile
36
+ // (index order: 0 = south, 1 = east, 2 = north, 3 = west).
37
+ // u_dayTextureToneEdgeBlend holds one band width per geographic edge as a
38
+ // fraction of the tile extent; only edges with a coarser neighbor get a
39
+ // positive width, so same-level/finer edges never participate in the fade
40
+ // (an edge that faded toward the texture's own correction would reset the
41
+ // fade contributed by another edge near their corner).
42
+ uniform vec3 u_dayTextureToneEdgeGain[TEXTURE_UNITS * 4];
43
+ uniform vec3 u_dayTextureToneEdgeBias[TEXTURE_UNITS * 4];
44
+ uniform float u_dayTextureToneEdgeBlend[TEXTURE_UNITS * 4];
45
+ #endif
46
+
47
+ #ifdef APPLY_TONE_MATCH_CROSS
48
+ // Per-texture ancestor imagery for content cross-fading at edges facing a
49
+ // coarser neighbor: near such an edge the tile blends its full rendered result
50
+ // with the ancestor's rendering (ancestor texture sampled with the ancestor
51
+ // level's correction), making both sides of the boundary identical at the
52
+ // shared edge. u_dayTextureCrossBlend holds one band width per geographic edge
53
+ // (0 = south, 1 = east, 2 = north, 3 = west); an edge only fades when its
54
+ // neighbor's imagery level matches the bound ancestor's level.
55
+ uniform sampler2D u_dayTextureCrossTexture[TEXTURE_UNITS];
56
+ uniform vec4 u_dayTextureCrossTranslationAndScale[TEXTURE_UNITS];
57
+ uniform vec3 u_dayTextureCrossGain[TEXTURE_UNITS];
58
+ uniform vec3 u_dayTextureCrossBias[TEXTURE_UNITS];
59
+ uniform float u_dayTextureCrossBlend[TEXTURE_UNITS * 4];
60
+ #endif
61
+
29
62
  #ifdef APPLY_HUE
30
63
  uniform float u_dayTextureHue[TEXTURE_UNITS];
31
64
  #endif
@@ -199,6 +232,8 @@ vec4 sampleAndBlend(
199
232
  float textureHue,
200
233
  float textureSaturation,
201
234
  float textureOneOverGamma,
235
+ vec3 textureToneGain,
236
+ vec3 textureToneBias,
202
237
  float split,
203
238
  vec4 colorToAlpha,
204
239
  float nightBlend)
@@ -227,6 +262,13 @@ vec4 sampleAndBlend(
227
262
  vec3 color = value.rgb;
228
263
  float alpha = value.a;
229
264
 
265
+ #ifdef APPLY_TONE_MATCH
266
+ // Applied directly after sampling so the affine correction, which is fitted
267
+ // in source-image color space, commutes with the identical transforms that
268
+ // follow for both the tile and the imagery it replaces.
269
+ color = color * textureToneGain + textureToneBias;
270
+ #endif
271
+
230
272
  #ifdef APPLY_COLOR_TO_ALPHA
231
273
  vec3 colorDiff = abs(color.rgb - colorToAlpha.rgb);
232
274
  colorDiff.r = czm_maximumComponent(colorDiff);
@@ -690,3 +732,4 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat
690
732
  }
691
733
 
692
734
  #endif // #ifdef SHOW_REFLECTIVE_OCEAN
735
+