bruce-cesium 7.2.9 → 7.3.0

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.
@@ -83,7 +83,7 @@ __exportStar(require("./widgets/widget-info-view"), exports);
83
83
  __exportStar(require("./widgets/widget-left-panel"), exports);
84
84
  __exportStar(require("./widgets/widget-nav-compass"), exports);
85
85
  __exportStar(require("./widgets/widget-view-bar"), exports);
86
- exports.VERSION = "7.2.9";
86
+ exports.VERSION = "7.3.0";
87
87
  /**
88
88
  * Updates the environment instance used by bruce-cesium to one specified.
89
89
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -30,6 +30,17 @@ var DisplacedSurfacePrimitive;
30
30
  // Most-detailed would fetch every tile under a parcel that can be a hundred kilometres across.
31
31
  const GROUND_SAMPLE_LEVEL = 11;
32
32
  // Height window the packed ground texture covers, wide enough for any terrain on Earth.
33
+ // Ground samples across the clearance regions rather than the whole extent.
34
+ const MASK_GROUND_SAMPLES_PER_SIDE = 128;
35
+ // Finest and coarsest terrain level a clearance raster is sampled at, and how many terrain tiles it is allowed to pull.
36
+ const MASK_GROUND_LEVEL_MAX = 13;
37
+ const MASK_GROUND_LEVEL_MIN = 8;
38
+ const MASK_GROUND_TILE_BUDGET = 64;
39
+ // Longest side of the rasterised region mask. The mask decides whether a texel is held up, so it
40
+ // wants to be finer than the value raster it is compared against.
41
+ const MASK_TEXTURE_MAX_SIDE = 1024;
42
+ // Stands in for "no floor" and "no clearance", far below anything the sheet can reach.
43
+ const NO_LIMIT_METRES = -1e6;
33
44
  const GROUND_MIN_METRES = -1000;
34
45
  const GROUND_MAX_METRES = 9000;
35
46
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
@@ -52,6 +63,19 @@ uniform float u_skirtDepth;
52
63
  uniform float u_groundWeight;
53
64
  uniform vec2 u_groundRange;
54
65
  uniform vec2 u_groundSize;
66
+ uniform sampler2D u_maskTexture;
67
+ uniform sampler2D u_maskGroundTexture;
68
+ uniform float u_maskWeight;
69
+ uniform float u_maskGroundWeight;
70
+ uniform float u_maskFill;
71
+ uniform float u_maskFillValue;
72
+ uniform float u_maskClearance;
73
+ uniform float u_maskFloor;
74
+ uniform vec2 u_maskGroundSize;
75
+ // West, East, North, South in degrees, for the surface and for the sampled region box.
76
+ uniform vec4 u_extentDegrees;
77
+ uniform vec4 u_maskGroundDegrees;
78
+ uniform float u_coverageCutoffVertex;
55
79
 
56
80
  out vec2 v_st;
57
81
  out float v_value;
@@ -81,6 +105,46 @@ float sampleGround(vec2 uv) {
81
105
  return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
82
106
  }
83
107
 
108
+ /*
109
+ * Height of the terrain under a point of the surface, read from the region's own ground raster.
110
+ *
111
+ * Sampled over the regions rather than over the whole extent, so the raster is fine enough for a
112
+ * clearance test to mean something.
113
+ */
114
+ float sampleMaskGround(vec2 uv) {
115
+ float longitude = mix(u_extentDegrees.x, u_extentDegrees.y, uv.x);
116
+ float latitude = mix(u_extentDegrees.z, u_extentDegrees.w, uv.y);
117
+ vec2 inBox = vec2(
118
+ (longitude - u_maskGroundDegrees.x) / max(u_maskGroundDegrees.y - u_maskGroundDegrees.x, 1e-9),
119
+ (u_maskGroundDegrees.z - latitude) / max(u_maskGroundDegrees.z - u_maskGroundDegrees.w, 1e-9));
120
+ vec2 clamped = clamp(inBox, vec2(0.0), vec2(1.0));
121
+ vec2 texel = 1.0 / u_maskGroundSize;
122
+ vec2 coord = clamped * u_maskGroundSize - 0.5;
123
+ vec2 base = floor(coord);
124
+ vec2 f = coord - base;
125
+ vec2 origin = (base + 0.5) * texel;
126
+ vec4 t00 = texture(u_maskGroundTexture, origin);
127
+ vec4 t10 = texture(u_maskGroundTexture, origin + vec2(texel.x, 0.0));
128
+ vec4 t01 = texture(u_maskGroundTexture, origin + vec2(0.0, texel.y));
129
+ vec4 t11 = texture(u_maskGroundTexture, origin + texel);
130
+ // One unknown corner makes the interpolation meaningless, so the whole point is treated as
131
+ // unsampled and answers low enough that no clearance built on it can win.
132
+ float known = min(min(t00.a, t10.a), min(t01.a, t11.a));
133
+ if (known < 0.5) {
134
+ return u_groundRange.x;
135
+ }
136
+ vec3 p00 = t00.rgb * 255.0;
137
+ vec3 p10 = t10.rgb * 255.0;
138
+ vec3 p01 = t01.rgb * 255.0;
139
+ vec3 p11 = t11.rgb * 255.0;
140
+ vec3 weights = vec3(65536.0, 256.0, 1.0);
141
+ float h00 = u_groundRange.x + dot(p00, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
142
+ float h10 = u_groundRange.x + dot(p10, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
143
+ float h01 = u_groundRange.x + dot(p01, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
144
+ float h11 = u_groundRange.x + dot(p11, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
145
+ return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
146
+ }
147
+
84
148
  /*
85
149
  * The texel's normalised value, whichever way the archive packed it.
86
150
  *
@@ -100,8 +164,16 @@ float decodeValue(vec4 texel) {
100
164
  void main() {
101
165
  vec4 texel = texture(u_valueTexture, st);
102
166
  v_st = st;
103
- v_value = decodeValue(texel);
104
- v_coverage = texel.a;
167
+
168
+ // Inside a region the scene knows is water, a texel with no value still has to paint, or the
169
+ // hole the region exists to close is still a hole.
170
+ // The value it paints is the shallowest the ramp is not transparent at.
171
+ float inMask = u_maskWeight * texture(u_maskTexture, st).a;
172
+ float hasData = step(u_coverageCutoffVertex, texel.a);
173
+ float raw = decodeValue(texel);
174
+ float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
175
+ v_value = mix(raw, filled, inMask * u_maskFill);
176
+ v_coverage = max(texel.a, inMask * u_maskFill);
105
177
 
106
178
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
107
179
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -114,8 +186,14 @@ void main() {
114
186
 
115
187
  // Coverage scales the displacement only where it is measured from the terrain, because there a
116
188
  // zero means the surface sits on the ground.
117
- float coverageScale = mix(1.0, texel.a, u_groundWeight);
118
- float displacement = u_baseHeight + ground + metres * coverageScale - skirt * u_skirtDepth;
189
+ float coverageScale = mix(1.0, v_coverage, u_groundWeight);
190
+ float placed = u_baseHeight + ground + metres * coverageScale;
191
+
192
+ // Held above whichever is higher, the region's own floor or the terrain under it plus a margin.
193
+ // Only inside the region: lifting the whole sheet would raise it over dry land as well.
194
+ float held = max(u_maskFloor, sampleMaskGround(st) * u_maskGroundWeight + u_maskClearance);
195
+ float lifted = mix(placed, max(placed, held), step(0.5, inMask));
196
+ float displacement = lifted - skirt * u_skirtDepth;
119
197
 
120
198
  // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
121
199
  // would be lost to float32 rounding at an earth radius.
@@ -350,6 +428,147 @@ void main() {
350
428
  Cesium.GeometryPipeline.encodeAttribute(geometry, "position", "position3DHigh", "position3DLow");
351
429
  return geometry;
352
430
  }
431
+ /*
432
+ * Where the ramp first paints something, as a normalised value.
433
+ *
434
+ * A style with a hidden floor is transparent over its lowest band, so filling a gap at zero would
435
+ * fill it with nothing. This is the lowest value that actually shows.
436
+ */
437
+ function firstOpaqueStop(ramp) {
438
+ const stops = ramp.length / 4;
439
+ for (let i = 0; i < stops; i++) {
440
+ if (ramp[i * 4 + 3] > 0) {
441
+ return stops > 1 ? i / (stops - 1) : 0;
442
+ }
443
+ }
444
+ return 0;
445
+ }
446
+ /*
447
+ * Rasterises the clearance regions into a mask over the surface's extent, alpha inside.
448
+ *
449
+ * Drawn north row first, matching the value raster's own row order, so one set of texture
450
+ * coordinates indexes both.
451
+ */
452
+ function rasteriseMask(extent, regions) {
453
+ const spanX = extent.East - extent.West;
454
+ const spanY = extent.North - extent.South;
455
+ if (!(spanX > 0) || !(spanY > 0) || !regions.length) {
456
+ return null;
457
+ }
458
+ // No document at all off the page, and the regions are still worth holding: the mask can be
459
+ // rasterised the next time they are set, and until then the surface simply lifts nothing.
460
+ if (typeof document === "undefined") {
461
+ return null;
462
+ }
463
+ const wide = spanX >= spanY;
464
+ const width = wide ? MASK_TEXTURE_MAX_SIDE : Math.max(1, Math.round(MASK_TEXTURE_MAX_SIDE * spanX / spanY));
465
+ const height = wide ? Math.max(1, Math.round(MASK_TEXTURE_MAX_SIDE * spanY / spanX)) : MASK_TEXTURE_MAX_SIDE;
466
+ const canvas = document.createElement("canvas");
467
+ canvas.width = width;
468
+ canvas.height = height;
469
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
470
+ if (!ctx) {
471
+ return null;
472
+ }
473
+ ctx.clearRect(0, 0, width, height);
474
+ ctx.fillStyle = "#ffffff";
475
+ for (const region of regions) {
476
+ const rings = region.rings || [];
477
+ if (!rings.length) {
478
+ continue;
479
+ }
480
+ ctx.beginPath();
481
+ for (const ring of rings) {
482
+ for (let i = 0; i + 1 < ring.length; i += 2) {
483
+ const x = (ring[i] - extent.West) / spanX * width;
484
+ const y = (extent.North - ring[i + 1]) / spanY * height;
485
+ if (i === 0) {
486
+ ctx.moveTo(x, y);
487
+ }
488
+ else {
489
+ ctx.lineTo(x, y);
490
+ }
491
+ }
492
+ ctx.closePath();
493
+ }
494
+ // Even odd, so a ring inside a ring is a hole without the caller having to wind them.
495
+ ctx.fill("evenodd");
496
+ }
497
+ return { width, height, data: new Uint8Array(ctx.getImageData(0, 0, width, height).data.buffer.slice(0)) };
498
+ }
499
+ /*
500
+ * The box every region fits inside, clipped to the surface, so the ground under them can be
501
+ * sampled without sampling the whole extent.
502
+ */
503
+ function boxOf(extent, regions) {
504
+ let west = Infinity;
505
+ let east = -Infinity;
506
+ let south = Infinity;
507
+ let north = -Infinity;
508
+ for (const region of regions) {
509
+ for (const ring of region.rings || []) {
510
+ for (let i = 0; i + 1 < ring.length; i += 2) {
511
+ west = Math.min(west, ring[i]);
512
+ east = Math.max(east, ring[i]);
513
+ south = Math.min(south, ring[i + 1]);
514
+ north = Math.max(north, ring[i + 1]);
515
+ }
516
+ }
517
+ }
518
+ if (!isFinite(west) || !isFinite(south)) {
519
+ return null;
520
+ }
521
+ return {
522
+ West: Math.max(west, extent.West),
523
+ East: Math.min(east, extent.East),
524
+ South: Math.max(south, extent.South),
525
+ North: Math.min(north, extent.North)
526
+ };
527
+ }
528
+ /*
529
+ * Packs sampled terrain heights into a texture's RGB, three bytes a height.
530
+ *
531
+ * A height needs more than eight bits to be useful and the surface reads it in the vertex shader,
532
+ * where float textures are not guaranteed, so it goes in as a fixed point integer.
533
+ */
534
+ function packHeights(sampled) {
535
+ const pixels = new Uint8Array(sampled.length * 4);
536
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
537
+ for (let i = 0; i < sampled.length; i++) {
538
+ const height = sampled[i] ? sampled[i].height : undefined;
539
+ const known = typeof height === "number" && isFinite(height);
540
+ const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, known ? height : 0));
541
+ const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
542
+ pixels[i * 4] = (packed >> 16) & 255;
543
+ pixels[i * 4 + 1] = (packed >> 8) & 255;
544
+ pixels[i * 4 + 2] = packed & 255;
545
+ pixels[i * 4 + 3] = known ? 255 : 0;
546
+ }
547
+ return pixels;
548
+ }
549
+ /*
550
+ * The finest terrain level a box can be sampled at inside the tile budget.
551
+ *
552
+ * Asked of the provider's own tiling scheme where it offers one, since a terrain set is free to
553
+ * tile the world differently from the default two by one.
554
+ */
555
+ function groundLevelFor(box, provider) {
556
+ const scheme = provider && provider.tilingScheme;
557
+ for (let level = MASK_GROUND_LEVEL_MAX; level > MASK_GROUND_LEVEL_MIN; level--) {
558
+ const across = scheme && typeof scheme.getNumberOfXTilesAtLevel === "function"
559
+ ? scheme.getNumberOfXTilesAtLevel(level)
560
+ : Math.pow(2, level + 1);
561
+ const down = scheme && typeof scheme.getNumberOfYTilesAtLevel === "function"
562
+ ? scheme.getNumberOfYTilesAtLevel(level)
563
+ : Math.pow(2, level);
564
+ const tiles = Math.ceil((box.East - box.West) / (360 / across) + 1)
565
+ * Math.ceil((box.North - box.South) / (180 / down) + 1);
566
+ if (tiles <= MASK_GROUND_TILE_BUDGET) {
567
+ return level;
568
+ }
569
+ }
570
+ return MASK_GROUND_LEVEL_MIN;
571
+ }
353
572
  class Surface {
354
573
  constructor(options, hasCoverage) {
355
574
  var _a, _b, _c, _d;
@@ -368,6 +587,14 @@ void main() {
368
587
  this.groundTexture = null;
369
588
  this.groundPixels = null;
370
589
  this.groundRequested = false;
590
+ this.groundClearance = null;
591
+ this.maskTexture = null;
592
+ this.maskPixels = null;
593
+ this.maskGroundTexture = null;
594
+ this.maskGroundPixels = null;
595
+ this.maskGroundBox = null;
596
+ this.maskGroundRequested = false;
597
+ this.maskGroundLevel = 0;
371
598
  this.followGround = Boolean(options.followGround);
372
599
  this.terrainProvider = options.terrainProvider || null;
373
600
  this.extent = options.extent;
@@ -380,7 +607,9 @@ void main() {
380
607
  this.pixelPlacement = options.pixelPlacement;
381
608
  this.tileSkirts = Boolean(options.tileSkirts);
382
609
  this.rampPixels = (0, image_utils_1.RampLookup)(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
610
+ this.maskFillValue = firstOpaqueStop(this.rampPixels);
383
611
  this.tiles = buildTiles(hasCoverage);
612
+ this.SetGroundClearance(options.groundClearance || null);
384
613
  }
385
614
  GetFollowsGround() {
386
615
  return this.followGround;
@@ -403,6 +632,97 @@ void main() {
403
632
  SetBaseHeight(baseHeight) {
404
633
  this.baseHeight = baseHeight;
405
634
  }
635
+ GetGroundClearance() {
636
+ return this.groundClearance;
637
+ }
638
+ /*
639
+ * The terrain level the clearance regions were sampled at, once they have been.
640
+ *
641
+ * Worth reading rather than assuming: the level is chosen from how big the regions are, so a
642
+ * region covering a whole sound gets a coarser test than a hand drawn harbour does.
643
+ */
644
+ GetGroundClearanceLevel() {
645
+ return this.maskGroundLevel;
646
+ }
647
+ /*
648
+ * What the clearance regions were measured against, once they have been.
649
+ * The heights are read out of whatever the terrain stores, and a terrain referenced to sea
650
+ * level reads a geoid apart from an archive delivering ellipsoidal metres.
651
+ */
652
+ GetGroundClearanceStats() {
653
+ if (!this.maskGroundPixels) {
654
+ return null;
655
+ }
656
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
657
+ let min = Infinity;
658
+ let max = -Infinity;
659
+ let samples = 0;
660
+ for (let i = 0; i < this.maskGroundPixels.length / 4; i++) {
661
+ if (!this.maskGroundPixels[i * 4 + 3]) {
662
+ continue;
663
+ }
664
+ const packed = (this.maskGroundPixels[i * 4] << 16)
665
+ + (this.maskGroundPixels[i * 4 + 1] << 8) + this.maskGroundPixels[i * 4 + 2];
666
+ const height = GROUND_MIN_METRES + (packed / 16777215) * span;
667
+ min = Math.min(min, height);
668
+ max = Math.max(max, height);
669
+ samples++;
670
+ }
671
+ if (!samples) {
672
+ return null;
673
+ }
674
+ return { level: this.maskGroundLevel, min, max, samples };
675
+ }
676
+ /*
677
+ * The real world range the texture's 0 to 1 maps onto, so a caller can compare it against what
678
+ * the terrain says without having kept its own copy.
679
+ */
680
+ GetValueRange() {
681
+ return { min: this.valueMin, max: this.valueMax };
682
+ }
683
+ /*
684
+ * Takes a different terrain to measure against.
685
+ * The clearance raster is sampled once and cached, so a scene that swaps its terrain would
686
+ * otherwise keep holding the sheet above heights read from the terrain it used to have.
687
+ */
688
+ SetTerrainProvider(provider) {
689
+ if (provider === this.terrainProvider) {
690
+ return;
691
+ }
692
+ this.terrainProvider = provider;
693
+ this.groundPixels = null;
694
+ this.groundRequested = false;
695
+ this.maskGroundPixels = null;
696
+ this.maskGroundRequested = false;
697
+ this.maskGroundLevel = 0;
698
+ }
699
+ /*
700
+ * Takes a new set of clearance regions, or drops them.
701
+ * The mask is rasterised here rather than on the render thread, and the ground under the
702
+ * regions is re-requested, so a plugin can hand over regions the moment it has them.
703
+ */
704
+ SetGroundClearance(clearance) {
705
+ this.groundClearance = clearance && clearance.regions && clearance.regions.length
706
+ ? clearance
707
+ : null;
708
+ this.maskPixels = this.groundClearance
709
+ ? rasteriseMask(this.extent, this.groundClearance.regions)
710
+ : null;
711
+ if (this.maskTexture) {
712
+ this.maskTexture.destroy();
713
+ this.maskTexture = null;
714
+ }
715
+ this.maskGroundPixels = null;
716
+ this.maskGroundRequested = false;
717
+ this.maskGroundLevel = 0;
718
+ this.maskGroundBox = this.groundClearance
719
+ ? boxOf(this.extent, this.groundClearance.regions)
720
+ : null;
721
+ if (this.maskGroundTexture) {
722
+ this.maskGroundTexture.destroy();
723
+ this.maskGroundTexture = null;
724
+ }
725
+ }
406
726
  /**
407
727
  * Feeds the surface from an animated frame series.
408
728
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -441,6 +761,8 @@ void main() {
441
761
  this.syncRampTexture(context);
442
762
  this.requestGround();
443
763
  this.syncGroundTexture(context);
764
+ this.requestMaskGround();
765
+ this.syncMaskTextures(context);
444
766
  // A tile needs one mesh before it has a bounding sphere to measure against.
445
767
  for (const tile of this.tiles) {
446
768
  if (!tile.centre) {
@@ -485,6 +807,14 @@ void main() {
485
807
  this.groundTexture.destroy();
486
808
  this.groundTexture = null;
487
809
  }
810
+ if (this.maskTexture) {
811
+ this.maskTexture.destroy();
812
+ this.maskTexture = null;
813
+ }
814
+ if (this.maskGroundTexture) {
815
+ this.maskGroundTexture.destroy();
816
+ this.maskGroundTexture = null;
817
+ }
488
818
  return undefined;
489
819
  }
490
820
  /*
@@ -517,24 +847,123 @@ void main() {
517
847
  if (this.destroyed || !sampled) {
518
848
  return;
519
849
  }
520
- const pixels = new Uint8Array(GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE * 4);
521
- const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
522
- for (let i = 0; i < sampled.length; i++) {
523
- const height = sampled[i] && isFinite(sampled[i].height) ? sampled[i].height : 0;
524
- const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, height));
525
- const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
526
- pixels[i * 4] = (packed >> 16) & 255;
527
- pixels[i * 4 + 1] = (packed >> 8) & 255;
528
- pixels[i * 4 + 2] = packed & 255;
529
- pixels[i * 4 + 3] = 255;
530
- }
531
- this.groundPixels = pixels;
850
+ this.groundPixels = packHeights(sampled);
532
851
  })
533
852
  .catch(() => {
534
853
  // Terrain that cannot be sampled leaves the surface on the ellipsoid, which is
535
854
  // where a clamped polygon sat before this existed.
536
855
  });
537
856
  }
857
+ /*
858
+ * Samples the terrain under the clearance regions.
859
+ *
860
+ * Its own raster rather than the surface's, because the surface extent here is two degrees
861
+ * wide and the region is a few kilometres, and a clearance test needs the finer one.
862
+ */
863
+ requestMaskGround() {
864
+ if (this.maskGroundRequested || !this.groundClearance || !this.maskGroundBox) {
865
+ return;
866
+ }
867
+ if (!(this.groundClearance.clearance > NO_LIMIT_METRES / 2)) {
868
+ return;
869
+ }
870
+ this.maskGroundRequested = true;
871
+ const sampler = Cesium.sampleTerrain;
872
+ if (!this.terrainProvider || typeof sampler !== "function") {
873
+ return;
874
+ }
875
+ const box = this.maskGroundBox;
876
+ const level = groundLevelFor(box, this.terrainProvider);
877
+ this.maskGroundLevel = level;
878
+ const total = MASK_GROUND_SAMPLES_PER_SIDE * MASK_GROUND_SAMPLES_PER_SIDE;
879
+ const scheme = this.terrainProvider.tilingScheme;
880
+ const asked = [];
881
+ // Which slot in the raster each asked-for position belongs to, since the ones the terrain
882
+ // does not cover are never asked for at all.
883
+ const slots = [];
884
+ for (let row = 0; row < MASK_GROUND_SAMPLES_PER_SIDE; row++) {
885
+ const fy = row / (MASK_GROUND_SAMPLES_PER_SIDE - 1);
886
+ const latitude = Cesium.Math.lerp(box.North, box.South, fy);
887
+ for (let col = 0; col < MASK_GROUND_SAMPLES_PER_SIDE; col++) {
888
+ const fx = col / (MASK_GROUND_SAMPLES_PER_SIDE - 1);
889
+ const at = Cesium.Cartographic.fromDegrees(Cesium.Math.lerp(box.West, box.East, fx), latitude);
890
+ // An imported terrain covers a survey area, so asking outside it answers 404 and
891
+ // fills the console with failures for points that were never going to have a height.
892
+ if (scheme && typeof this.terrainProvider.getTileDataAvailable === "function") {
893
+ const tile = scheme.positionToTileXY(at, level);
894
+ if (!tile || this.terrainProvider.getTileDataAvailable(tile.x, tile.y, level) === false) {
895
+ continue;
896
+ }
897
+ }
898
+ asked.push(at);
899
+ slots.push(row * MASK_GROUND_SAMPLES_PER_SIDE + col);
900
+ }
901
+ }
902
+ if (!asked.length) {
903
+ return;
904
+ }
905
+ Promise.resolve(sampler(this.terrainProvider, level, asked))
906
+ .then((sampled) => {
907
+ if (this.destroyed || !sampled) {
908
+ return;
909
+ }
910
+ const spread = new Array(total).fill(undefined);
911
+ for (let i = 0; i < slots.length; i++) {
912
+ spread[slots[i]] = sampled[i];
913
+ }
914
+ this.maskGroundPixels = packHeights(spread);
915
+ })
916
+ .catch(() => {
917
+ // Unsampled terrain leaves only the region's own floor to hold the sheet up,
918
+ // which is the behaviour a region with no clearance already has.
919
+ });
920
+ }
921
+ /*
922
+ * Uploads the region mask and the ground under it, with a one texel stand-in until they land
923
+ * so the shader always has something bound.
924
+ */
925
+ syncMaskTextures(context) {
926
+ if (!this.maskTexture) {
927
+ const held = this.maskPixels;
928
+ this.maskTexture = new Cesium.Texture({
929
+ context,
930
+ pixelFormat: Cesium.PixelFormat.RGBA,
931
+ source: held
932
+ ? { width: held.width, height: held.height, arrayBufferView: held.data }
933
+ : { width: 1, height: 1, arrayBufferView: new Uint8Array([0, 0, 0, 0]) },
934
+ flipY: false,
935
+ sampler: new Cesium.Sampler({
936
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
937
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE
938
+ })
939
+ });
940
+ }
941
+ if (this.maskGroundTexture && (!this.maskGroundPixels
942
+ || this.maskGroundTexture.width === MASK_GROUND_SAMPLES_PER_SIDE)) {
943
+ return;
944
+ }
945
+ if (this.maskGroundTexture) {
946
+ this.maskGroundTexture.destroy();
947
+ }
948
+ const size = this.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1;
949
+ this.maskGroundTexture = new Cesium.Texture({
950
+ context,
951
+ pixelFormat: Cesium.PixelFormat.RGBA,
952
+ source: {
953
+ width: size,
954
+ height: size,
955
+ arrayBufferView: this.maskGroundPixels || packHeights([{ height: 0 }])
956
+ },
957
+ flipY: false,
958
+ sampler: new Cesium.Sampler({
959
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
960
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
961
+ // Nearest, because the shader interpolates decoded heights itself.
962
+ minificationFilter: Cesium.TextureMinificationFilter.NEAREST,
963
+ magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST
964
+ })
965
+ });
966
+ }
538
967
  /*
539
968
  * Uploads whatever ground samples exist, and a flat stand-in until they arrive, so the
540
969
  * shader always has a texture bound whether or not this surface follows the ground.
@@ -546,10 +975,8 @@ void main() {
546
975
  if (this.groundTexture) {
547
976
  this.groundTexture.destroy();
548
977
  }
549
- const zero = Math.round(((0 - GROUND_MIN_METRES) / (GROUND_MAX_METRES - GROUND_MIN_METRES)) * 16777215);
550
978
  const size = this.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1;
551
- const arrayBufferView = this.groundPixels
552
- || new Uint8Array([(zero >> 16) & 255, (zero >> 8) & 255, zero & 255, 255]);
979
+ const arrayBufferView = this.groundPixels || packHeights([{ height: 0 }]);
553
980
  this.groundTexture = new Cesium.Texture({
554
981
  context,
555
982
  pixelFormat: Cesium.PixelFormat.RGBA,
@@ -758,6 +1185,28 @@ void main() {
758
1185
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
759
1186
  u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
760
1187
  u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
1188
+ u_maskTexture: () => self.maskTexture,
1189
+ u_maskGroundTexture: () => self.maskGroundTexture,
1190
+ u_maskWeight: () => (self.groundClearance && self.maskPixels ? 1 : 0),
1191
+ u_maskGroundWeight: () => (self.maskGroundPixels ? 1 : 0),
1192
+ u_maskFill: () => (self.groundClearance && self.groundClearance.fillGaps ? 1 : 0),
1193
+ u_maskFillValue: () => self.maskFillValue,
1194
+ u_maskClearance: () => (self.groundClearance
1195
+ && typeof self.groundClearance.clearance === "number"
1196
+ && self.maskGroundPixels
1197
+ ? self.groundClearance.clearance
1198
+ : NO_LIMIT_METRES),
1199
+ u_maskFloor: () => (self.groundClearance
1200
+ && typeof self.groundClearance.floor === "number"
1201
+ ? self.groundClearance.floor
1202
+ : NO_LIMIT_METRES),
1203
+ u_maskGroundSize: () => new Cesium.Cartesian2(self.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1, self.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1),
1204
+ u_extentDegrees: () => new Cesium.Cartesian4(self.extent.West, self.extent.East, self.extent.North, self.extent.South),
1205
+ u_maskGroundDegrees: () => {
1206
+ const box = self.maskGroundBox || self.extent;
1207
+ return new Cesium.Cartesian4(box.West, box.East, box.North, box.South);
1208
+ },
1209
+ u_coverageCutoffVertex: () => COVERAGE_CUTOFF,
761
1210
  u_metresPerTexel: () => self.metresPerTexel(),
762
1211
  u_valueMin: () => self.valueMin,
763
1212
  u_valueRange: () => self.valueMax - self.valueMin,