bruce-cesium 7.1.2 → 7.1.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.
@@ -0,0 +1,723 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DisplacedSurfacePrimitive = void 0;
4
+ const Cesium = require("cesium");
5
+ /**
6
+ * Renders a value texture as a displaced, coloured surface.
7
+ * The source canvas is expected as RGBA: value in R, coverage in A.
8
+ */
9
+ var DisplacedSurfacePrimitive;
10
+ (function (DisplacedSurfacePrimitive) {
11
+ // Grid densities a tile can draw at, coarsest first.
12
+ const TIERS = [32, 64, 128, 192];
13
+ // Tiles per side the extent is cut into.
14
+ const TILES_PER_SIDE = 6;
15
+ // Screen pixels one grid cell should cover. Lower means finer meshes sooner.
16
+ const TARGET_CELL_PIXELS = 12;
17
+ // Fraction of its distance to the surface the camera must move before densities are reconsidered.
18
+ const MOVEMENT_THRESHOLD = 0.12;
19
+ const RESELECT_INTERVAL_MS = 400;
20
+ // Skirt depth as a fraction of the displaced range. The sheet is translucent, so an oversized
21
+ // skirt blends through it and paints a seam down every tile boundary.
22
+ const SKIRT_FRACTION = 0.015;
23
+ // Alpha below which a texel counts as no-data and is not drawn at all.
24
+ const COVERAGE_CUTOFF = 0.35;
25
+ // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
26
+ const AUTO_RELIEF_FRACTION = 0.02;
27
+ // Terrain is sampled on this grid and interpolated between samples.
28
+ // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
29
+ const GROUND_SAMPLES_PER_SIDE = 64;
30
+ // Terrain detail level the samples are taken at.
31
+ // Most-detailed would fetch every tile under a parcel that can be a hundred kilometres across.
32
+ const GROUND_SAMPLE_LEVEL = 11;
33
+ // Height window the packed ground texture covers, wide enough for any terrain on Earth.
34
+ const GROUND_MIN_METRES = -1000;
35
+ const GROUND_MAX_METRES = 9000;
36
+ const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
37
+ const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
38
+ const VERTEX_SHADER_GLSL300 = `
39
+ in vec3 position3DHigh;
40
+ in vec3 position3DLow;
41
+ in vec3 normal;
42
+ in vec2 st;
43
+ in float skirt;
44
+
45
+ uniform sampler2D u_valueTexture;
46
+ uniform sampler2D u_groundTexture;
47
+ uniform float u_valueMin;
48
+ uniform float u_valueRange;
49
+ uniform float u_exaggeration;
50
+ uniform float u_baseHeight;
51
+ uniform float u_skirtDepth;
52
+ uniform float u_groundWeight;
53
+ uniform vec2 u_groundRange;
54
+ uniform vec2 u_groundSize;
55
+
56
+ out vec2 v_st;
57
+ out float v_value;
58
+ out float v_coverage;
59
+
60
+ float decodeGround(vec2 uv) {
61
+ // Channels arrive normalised to 0..1, so they go back to bytes before being reassembled.
62
+ vec3 packed = texture(u_groundTexture, uv).rgb * 255.0;
63
+ float normalised = dot(packed, vec3(65536.0, 256.0, 1.0)) / 16777215.0;
64
+ return u_groundRange.x + normalised * (u_groundRange.y - u_groundRange.x);
65
+ }
66
+
67
+ /*
68
+ * Bilinear by hand, because the height is packed across three channels and hardware filtering would
69
+ * blend the packing rather than the heights, turning a smooth slope into noise at every texel edge.
70
+ */
71
+ float sampleGround(vec2 uv) {
72
+ vec2 texel = 1.0 / u_groundSize;
73
+ vec2 coord = uv * u_groundSize - 0.5;
74
+ vec2 base = floor(coord);
75
+ vec2 f = coord - base;
76
+ vec2 origin = (base + 0.5) * texel;
77
+ float h00 = decodeGround(origin);
78
+ float h10 = decodeGround(origin + vec2(texel.x, 0.0));
79
+ float h01 = decodeGround(origin + vec2(0.0, texel.y));
80
+ float h11 = decodeGround(origin + texel);
81
+ return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
82
+ }
83
+
84
+ void main() {
85
+ vec4 texel = texture(u_valueTexture, st);
86
+ v_st = st;
87
+ v_value = texel.r;
88
+ v_coverage = texel.a;
89
+
90
+ float metres = u_valueMin + texel.r * u_valueRange;
91
+
92
+ // Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
93
+ // is following the ground rather than absolutely positioned.
94
+ float ground = sampleGround(st) * u_groundWeight;
95
+ float displacement = u_baseHeight + ground + metres * u_exaggeration * texel.a - skirt * u_skirtDepth;
96
+
97
+ // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
98
+ // would be lost to float32 rounding at an earth radius.
99
+ vec4 p = czm_translateRelativeToEye(position3DHigh, position3DLow + normal * displacement);
100
+ gl_Position = czm_modelViewProjectionRelativeToEye * p;
101
+ }
102
+ `;
103
+ const FRAGMENT_SHADER_GLSL300 = `
104
+ uniform sampler2D u_valueTexture;
105
+ uniform vec4 u_lowColor;
106
+ uniform vec4 u_highColor;
107
+ uniform float u_coverageCutoff;
108
+ uniform vec2 u_texelSize;
109
+ uniform float u_valueRange;
110
+ uniform float u_exaggeration;
111
+ uniform float u_metresPerTexel;
112
+
113
+ in vec2 v_st;
114
+ in float v_value;
115
+ in float v_coverage;
116
+
117
+ void main() {
118
+ if (v_coverage < u_coverageCutoff) {
119
+ discard;
120
+ }
121
+ vec4 color = mix(u_lowColor, u_highColor, v_value);
122
+
123
+ // Relief shading from the value gradient, so the displacement reads at a distance without
124
+ // recomputing vertex normals every time the texture changes.
125
+ float left = texture(u_valueTexture, v_st - vec2(u_texelSize.x, 0.0)).r;
126
+ float right = texture(u_valueTexture, v_st + vec2(u_texelSize.x, 0.0)).r;
127
+ float down = texture(u_valueTexture, v_st - vec2(0.0, u_texelSize.y)).r;
128
+ float up = texture(u_valueTexture, v_st + vec2(0.0, u_texelSize.y)).r;
129
+ float scale = u_valueRange * u_exaggeration;
130
+ vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
131
+ float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
132
+ color.rgb *= 0.45 + 0.9 * lambert;
133
+
134
+ out_FragColor = vec4(color.rgb, color.a * v_coverage);
135
+ }
136
+ `;
137
+ /*
138
+ * Rewrites a GLSL 300 es source into the GLSL 100 dialect older Cesium builds author in.
139
+ */
140
+ function toGlsl100(source, isFragment) {
141
+ let result = source
142
+ .replace(/\btexture\s*\(/g, "texture2D(")
143
+ .replace(/^in\s+/gm, isFragment ? "varying " : "attribute ")
144
+ .replace(/^out\s+/gm, "varying ");
145
+ if (isFragment) {
146
+ result = result.replace(/\bout_FragColor\b/g, "gl_FragColor");
147
+ }
148
+ return result;
149
+ }
150
+ let _glsl300Support = null;
151
+ /*
152
+ * Works out which GLSL dialect this host's Cesium authors shaders in.
153
+ */
154
+ function usesGlsl300(context) {
155
+ if (_glsl300Support !== null) {
156
+ return _glsl300Support;
157
+ }
158
+ let modernizes = false;
159
+ try {
160
+ const probe = new Cesium.ShaderSource({
161
+ sources: ["varying float v_probe;\nvoid main() { gl_Position = vec4(0.0); }"],
162
+ includeBuiltIns: false
163
+ });
164
+ const combined = probe.createCombinedVertexShader(context);
165
+ modernizes = combined.indexOf("varying") < 0;
166
+ }
167
+ catch (e) {
168
+ // No probe means no evidence the host rewrites anything, so trust the context alone.
169
+ modernizes = false;
170
+ }
171
+ _glsl300Support = Boolean(context.webgl2) && !modernizes;
172
+ return _glsl300Support;
173
+ }
174
+ /*
175
+ * Places grid node (fx, fy) on the ellipsoid, walking equal steps of east/north metres.
176
+ * Cesium maps a polygon's material across a tangent-plane rectangle and the generated raster is
177
+ * linear in a projected CRS, so stepping in degrees instead would skew the surface against the flat-textured polygon it replaces.
178
+ */
179
+ function makePlacer(extent) {
180
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
181
+ const centre = Cesium.Cartesian3.fromDegrees((extent.West + extent.East) / 2, (extent.South + extent.North) / 2, 0);
182
+ const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centre);
183
+ const toLocal = Cesium.Matrix4.inverse(frame, new Cesium.Matrix4());
184
+ const southWest = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0), new Cesium.Cartesian3());
185
+ const northEast = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.East, extent.North, 0), new Cesium.Cartesian3());
186
+ return (fx, fy) => {
187
+ const local = new Cesium.Cartesian3(Cesium.Math.lerp(southWest.x, northEast.x, fx), Cesium.Math.lerp(northEast.y, southWest.y, fy), 0);
188
+ const world = Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3());
189
+ return ellipsoid.scaleToGeodeticSurface(world, world);
190
+ };
191
+ }
192
+ /*
193
+ * Builds one tile's grid, plus a duplicated perimeter ring the shader pushes downwards to cover
194
+ * the crack where a neighbouring tile at a different density interpolates the shared edge.
195
+ */
196
+ function buildGeometry(extent, resolution, patch) {
197
+ const cols = resolution;
198
+ const rows = resolution;
199
+ const gridCount = (cols + 1) * (rows + 1);
200
+ const perimeterCount = 2 * (cols + 1) + 2 * (rows + 1) - 4;
201
+ const count = gridCount + perimeterCount;
202
+ const positions = new Float64Array(count * 3);
203
+ const normals = new Float32Array(count * 3);
204
+ const sts = new Float32Array(count * 2);
205
+ const skirts = new Float32Array(count);
206
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
207
+ const place = makePlacer(extent);
208
+ let v = 0;
209
+ for (let row = 0; row <= rows; row++) {
210
+ // Texture row 0 is the north edge, so t runs opposite to northing.
211
+ const fy = Cesium.Math.lerp(patch.v0, patch.v1, row / rows);
212
+ for (let col = 0; col <= cols; col++) {
213
+ const fx = Cesium.Math.lerp(patch.u0, patch.u1, col / cols);
214
+ const position = place(fx, fy);
215
+ positions[v * 3] = position.x;
216
+ positions[v * 3 + 1] = position.y;
217
+ positions[v * 3 + 2] = position.z;
218
+ const up = ellipsoid.geodeticSurfaceNormal(position, new Cesium.Cartesian3());
219
+ normals[v * 3] = up.x;
220
+ normals[v * 3 + 1] = up.y;
221
+ normals[v * 3 + 2] = up.z;
222
+ sts[v * 2] = fx;
223
+ sts[v * 2 + 1] = fy;
224
+ v++;
225
+ }
226
+ }
227
+ const indices = new Uint32Array(cols * rows * 6 + perimeterCount * 6);
228
+ let i = 0;
229
+ for (let row = 0; row < rows; row++) {
230
+ for (let col = 0; col < cols; col++) {
231
+ const topLeft = row * (cols + 1) + col;
232
+ const topRight = topLeft + 1;
233
+ const bottomLeft = topLeft + (cols + 1);
234
+ const bottomRight = bottomLeft + 1;
235
+ indices[i++] = topLeft;
236
+ indices[i++] = bottomLeft;
237
+ indices[i++] = topRight;
238
+ indices[i++] = topRight;
239
+ indices[i++] = bottomLeft;
240
+ indices[i++] = bottomRight;
241
+ }
242
+ }
243
+ // Perimeter walked as one closed loop, so consecutive entries are always neighbours.
244
+ const loop = [];
245
+ for (let col = 0; col <= cols; col++) {
246
+ loop.push(col);
247
+ }
248
+ for (let row = 1; row <= rows; row++) {
249
+ loop.push(row * (cols + 1) + cols);
250
+ }
251
+ for (let col = cols - 1; col >= 0; col--) {
252
+ loop.push(rows * (cols + 1) + col);
253
+ }
254
+ for (let row = rows - 1; row >= 1; row--) {
255
+ loop.push(row * (cols + 1));
256
+ }
257
+ const skirtStart = gridCount;
258
+ loop.forEach((gridIndex, k) => {
259
+ const target = skirtStart + k;
260
+ positions[target * 3] = positions[gridIndex * 3];
261
+ positions[target * 3 + 1] = positions[gridIndex * 3 + 1];
262
+ positions[target * 3 + 2] = positions[gridIndex * 3 + 2];
263
+ normals[target * 3] = normals[gridIndex * 3];
264
+ normals[target * 3 + 1] = normals[gridIndex * 3 + 1];
265
+ normals[target * 3 + 2] = normals[gridIndex * 3 + 2];
266
+ sts[target * 2] = sts[gridIndex * 2];
267
+ sts[target * 2 + 1] = sts[gridIndex * 2 + 1];
268
+ skirts[target] = 1;
269
+ });
270
+ for (let k = 0; k < loop.length; k++) {
271
+ const nextK = (k + 1) % loop.length;
272
+ indices[i++] = loop[k];
273
+ indices[i++] = skirtStart + k;
274
+ indices[i++] = loop[nextK];
275
+ indices[i++] = loop[nextK];
276
+ indices[i++] = skirtStart + k;
277
+ indices[i++] = skirtStart + nextK;
278
+ }
279
+ const geometry = new Cesium.Geometry({
280
+ attributes: {
281
+ position: new Cesium.GeometryAttribute({
282
+ componentDatatype: Cesium.ComponentDatatype.DOUBLE,
283
+ componentsPerAttribute: 3,
284
+ values: positions
285
+ }),
286
+ normal: new Cesium.GeometryAttribute({
287
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
288
+ componentsPerAttribute: 3,
289
+ values: normals
290
+ }),
291
+ st: new Cesium.GeometryAttribute({
292
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
293
+ componentsPerAttribute: 2,
294
+ values: sts
295
+ }),
296
+ skirt: new Cesium.GeometryAttribute({
297
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
298
+ componentsPerAttribute: 1,
299
+ values: skirts
300
+ })
301
+ },
302
+ indices: indices,
303
+ primitiveType: Cesium.PrimitiveType.TRIANGLES,
304
+ boundingSphere: Cesium.BoundingSphere.fromVertices(Array.prototype.slice.call(positions))
305
+ });
306
+ // Splits position into the high/low pair Cesium's relative-to-eye shaders expect.
307
+ Cesium.GeometryPipeline.encodeAttribute(geometry, "position", "position3DHigh", "position3DLow");
308
+ return geometry;
309
+ }
310
+ class Surface {
311
+ constructor(options, hasCoverage) {
312
+ var _a, _b, _c, _d;
313
+ this.show = true;
314
+ this.valueSource = null;
315
+ this.lastPresentVersion = -1;
316
+ this.texture = null;
317
+ this.textureDirty = true;
318
+ this.shaderProgram = null;
319
+ this.renderState = null;
320
+ this.destroyed = false;
321
+ this.lastEyePosition = null;
322
+ this.lastSelectionMs = -Infinity;
323
+ this.selectionDirty = true;
324
+ this.groundTexture = null;
325
+ this.groundPixels = null;
326
+ this.groundRequested = false;
327
+ this.followGround = Boolean(options.followGround);
328
+ this.terrainProvider = options.terrainProvider || null;
329
+ this.extent = options.extent;
330
+ this.source = options.source || null;
331
+ this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
332
+ this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
333
+ this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
334
+ this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
335
+ this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
336
+ this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
337
+ this.tiles = buildTiles(hasCoverage);
338
+ }
339
+ GetFollowsGround() {
340
+ return this.followGround;
341
+ }
342
+ GetBaseHeight() {
343
+ return this.baseHeight;
344
+ }
345
+ GetExaggeration() {
346
+ return this.exaggeration;
347
+ }
348
+ GetExtent() {
349
+ return this.extent;
350
+ }
351
+ SetExaggeration(exaggeration) {
352
+ this.exaggeration = exaggeration;
353
+ }
354
+ SetBaseHeight(baseHeight) {
355
+ this.baseHeight = baseHeight;
356
+ }
357
+ /**
358
+ * Feeds the surface from an animated frame series.
359
+ * Height and colour advance with the clock without the caller having to push each frame in.
360
+ */
361
+ BindAnimator(source) {
362
+ this.valueSource = source;
363
+ }
364
+ /**
365
+ * Tells the surface its source canvas has been redrawn and the texture needs re-uploading.
366
+ */
367
+ MarkSourceDirty() {
368
+ this.textureDirty = true;
369
+ }
370
+ update(frameState) {
371
+ if (!this.show || this.destroyed || this.tiles.length === 0) {
372
+ return;
373
+ }
374
+ const context = frameState.context;
375
+ if (this.valueSource) {
376
+ const canvas = this.valueSource.GetValueCanvas();
377
+ if (!canvas) {
378
+ // No frame decoded yet, so there is nothing to displace against.
379
+ return;
380
+ }
381
+ const version = this.valueSource.GetPresentVersion();
382
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
383
+ this.source = canvas;
384
+ this.lastPresentVersion = version;
385
+ this.textureDirty = true;
386
+ }
387
+ }
388
+ if (!this.source || !this.source.width || !this.source.height) {
389
+ return;
390
+ }
391
+ this.syncTexture(context);
392
+ this.requestGround();
393
+ this.syncGroundTexture(context);
394
+ // A tile needs one mesh before it has a bounding sphere to measure against.
395
+ for (const tile of this.tiles) {
396
+ if (!tile.centre) {
397
+ this.ensureMesh(context, tile, TIERS[0]);
398
+ }
399
+ }
400
+ const nowMs = now();
401
+ if (this.shouldReselect(frameState, nowMs)) {
402
+ this.selectTiers(frameState);
403
+ this.lastSelectionMs = nowMs;
404
+ this.lastEyePosition = Cesium.Cartesian3.clone(frameState.camera.positionWC, this.lastEyePosition);
405
+ this.selectionDirty = false;
406
+ }
407
+ for (const tile of this.tiles) {
408
+ const mesh = this.ensureMesh(context, tile, tile.tier);
409
+ frameState.commandList.push(this.commandFor(context, tile, mesh));
410
+ }
411
+ }
412
+ isDestroyed() {
413
+ return this.destroyed;
414
+ }
415
+ destroy() {
416
+ this.destroyed = true;
417
+ for (const tile of this.tiles) {
418
+ tile.meshes.forEach((mesh) => mesh.vertexArray.destroy());
419
+ tile.meshes.clear();
420
+ tile.command = null;
421
+ }
422
+ if (this.shaderProgram) {
423
+ this.shaderProgram.destroy();
424
+ this.shaderProgram = null;
425
+ }
426
+ if (this.texture) {
427
+ this.texture.destroy();
428
+ this.texture = null;
429
+ }
430
+ if (this.groundTexture) {
431
+ this.groundTexture.destroy();
432
+ this.groundTexture = null;
433
+ }
434
+ return undefined;
435
+ }
436
+ /*
437
+ * Samples terrain across the extent once and packs it into a texture the vertex shader reads.
438
+ *
439
+ * A texture rather than heights baked into the meshes, because the samples arrive
440
+ * asynchronously and every cached tile mesh would otherwise have to be rebuilt when they land.
441
+ */
442
+ requestGround() {
443
+ if (this.groundRequested || !this.followGround) {
444
+ return;
445
+ }
446
+ this.groundRequested = true;
447
+ const sampler = Cesium.sampleTerrain;
448
+ if (!this.terrainProvider || typeof sampler !== "function") {
449
+ return;
450
+ }
451
+ const positions = [];
452
+ for (let row = 0; row < GROUND_SAMPLES_PER_SIDE; row++) {
453
+ const fy = row / (GROUND_SAMPLES_PER_SIDE - 1);
454
+ const latitude = Cesium.Math.lerp(this.extent.North, this.extent.South, fy);
455
+ for (let col = 0; col < GROUND_SAMPLES_PER_SIDE; col++) {
456
+ const fx = col / (GROUND_SAMPLES_PER_SIDE - 1);
457
+ const longitude = Cesium.Math.lerp(this.extent.West, this.extent.East, fx);
458
+ positions.push(Cesium.Cartographic.fromDegrees(longitude, latitude));
459
+ }
460
+ }
461
+ Promise.resolve(sampler(this.terrainProvider, GROUND_SAMPLE_LEVEL, positions))
462
+ .then((sampled) => {
463
+ if (this.destroyed || !sampled) {
464
+ return;
465
+ }
466
+ const pixels = new Uint8Array(GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE * 4);
467
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
468
+ for (let i = 0; i < sampled.length; i++) {
469
+ const height = sampled[i] && isFinite(sampled[i].height) ? sampled[i].height : 0;
470
+ const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, height));
471
+ const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
472
+ pixels[i * 4] = (packed >> 16) & 255;
473
+ pixels[i * 4 + 1] = (packed >> 8) & 255;
474
+ pixels[i * 4 + 2] = packed & 255;
475
+ pixels[i * 4 + 3] = 255;
476
+ }
477
+ this.groundPixels = pixels;
478
+ })
479
+ .catch(() => {
480
+ // Terrain that cannot be sampled leaves the surface on the ellipsoid, which is
481
+ // where a clamped polygon sat before this existed.
482
+ });
483
+ }
484
+ /*
485
+ * Uploads whatever ground samples exist, and a flat stand-in until they arrive, so the
486
+ * shader always has a texture bound whether or not this surface follows the ground.
487
+ */
488
+ syncGroundTexture(context) {
489
+ if (this.groundTexture && (!this.groundPixels || this.groundTexture.width === GROUND_SAMPLES_PER_SIDE)) {
490
+ return;
491
+ }
492
+ if (this.groundTexture) {
493
+ this.groundTexture.destroy();
494
+ }
495
+ const zero = Math.round(((0 - GROUND_MIN_METRES) / (GROUND_MAX_METRES - GROUND_MIN_METRES)) * 16777215);
496
+ const size = this.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1;
497
+ const arrayBufferView = this.groundPixels
498
+ || new Uint8Array([(zero >> 16) & 255, (zero >> 8) & 255, zero & 255, 255]);
499
+ this.groundTexture = new Cesium.Texture({
500
+ context,
501
+ pixelFormat: Cesium.PixelFormat.RGBA,
502
+ source: { width: size, height: size, arrayBufferView },
503
+ flipY: false,
504
+ sampler: new Cesium.Sampler({
505
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
506
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
507
+ // Nearest, because the shader interpolates decoded heights itself.
508
+ minificationFilter: Cesium.TextureMinificationFilter.NEAREST,
509
+ magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST
510
+ })
511
+ });
512
+ }
513
+ /*
514
+ * Ground distance between neighbouring texels, so the shading normal is built from a real
515
+ * slope rather than a made up one.
516
+ */
517
+ metresPerTexel() {
518
+ const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
519
+ const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
520
+ const width = Cesium.Cartesian3.distance(southWest, southEast);
521
+ return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
522
+ }
523
+ syncTexture(context) {
524
+ const sizeChanged = this.texture
525
+ && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
526
+ if (!this.texture || sizeChanged) {
527
+ if (this.texture) {
528
+ this.texture.destroy();
529
+ }
530
+ this.texture = new Cesium.Texture({
531
+ context,
532
+ source: this.source,
533
+ // Cesium flips canvas rows on upload by default, which would put the archive's
534
+ // row 0 (its north edge) at the south edge and mirror the whole surface.
535
+ flipY: false,
536
+ sampler: new Cesium.Sampler({
537
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
538
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
539
+ minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
540
+ magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
541
+ })
542
+ });
543
+ this.textureDirty = false;
544
+ return;
545
+ }
546
+ if (this.textureDirty) {
547
+ this.texture.copyFrom({ source: this.source });
548
+ this.textureDirty = false;
549
+ }
550
+ }
551
+ ensureMesh(context, tile, tier) {
552
+ const cached = tile.meshes.get(tier);
553
+ if (cached) {
554
+ return cached;
555
+ }
556
+ const geometry = buildGeometry(this.extent, tier, tile.patch);
557
+ const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
558
+ const mesh = {
559
+ attributeLocations,
560
+ vertexArray: Cesium.VertexArray.fromGeometry({
561
+ context,
562
+ geometry,
563
+ attributeLocations,
564
+ bufferUsage: Cesium.BufferUsage.STATIC_DRAW
565
+ }),
566
+ boundingSphere: geometry.boundingSphere
567
+ };
568
+ tile.meshes.set(tier, mesh);
569
+ tile.centre = geometry.boundingSphere.center;
570
+ tile.radius = geometry.boundingSphere.radius;
571
+ return mesh;
572
+ }
573
+ /*
574
+ * Picks each tile's density from how much screen it covers.
575
+ */
576
+ selectTiers(frameState) {
577
+ const camera = frameState.camera;
578
+ const fovy = camera.frustum && camera.frustum.fovy ? camera.frustum.fovy : Cesium.Math.PI_OVER_THREE;
579
+ const pixelsPerRadian = frameState.context.drawingBufferHeight / fovy;
580
+ for (const tile of this.tiles) {
581
+ if (!tile.centre) {
582
+ tile.tier = TIERS[0];
583
+ continue;
584
+ }
585
+ const distance = Math.max(1, Cesium.Cartesian3.distance(camera.positionWC, tile.centre) - tile.radius);
586
+ const screenPixels = (2 * tile.radius / distance) * pixelsPerRadian;
587
+ const wanted = screenPixels / TARGET_CELL_PIXELS;
588
+ let tier = TIERS[0];
589
+ for (const candidate of TIERS) {
590
+ if (candidate <= wanted) {
591
+ tier = candidate;
592
+ }
593
+ }
594
+ tile.tier = tier;
595
+ }
596
+ }
597
+ shouldReselect(frameState, nowMs) {
598
+ if (this.selectionDirty) {
599
+ return true;
600
+ }
601
+ if (nowMs - this.lastSelectionMs < RESELECT_INTERVAL_MS) {
602
+ return false;
603
+ }
604
+ const eye = frameState.camera.positionWC;
605
+ if (!this.lastEyePosition) {
606
+ return true;
607
+ }
608
+ const moved = Cesium.Cartesian3.distance(eye, this.lastEyePosition);
609
+ const anchor = this.tiles[0].centre;
610
+ const reference = anchor ? Math.max(1, Cesium.Cartesian3.distance(eye, anchor)) : 1;
611
+ return moved / reference > MOVEMENT_THRESHOLD;
612
+ }
613
+ commandFor(context, tile, mesh) {
614
+ if (!this.shaderProgram) {
615
+ const glsl300 = usesGlsl300(context);
616
+ this.shaderProgram = Cesium.ShaderProgram.fromCache({
617
+ context,
618
+ vertexShaderSource: glsl300 ? VERTEX_SHADER_GLSL300 : toGlsl100(VERTEX_SHADER_GLSL300, false),
619
+ fragmentShaderSource: glsl300 ? FRAGMENT_SHADER_GLSL300 : toGlsl100(FRAGMENT_SHADER_GLSL300, true),
620
+ attributeLocations: mesh.attributeLocations
621
+ });
622
+ this.renderState = Cesium.RenderState.fromCache({
623
+ depthTest: { enabled: true },
624
+ // Translucent ramp: blend, and leave the depth buffer to the opaque world.
625
+ depthMask: false,
626
+ blending: Cesium.BlendingState.ALPHA_BLEND,
627
+ cull: { enabled: false }
628
+ });
629
+ }
630
+ if (!tile.command) {
631
+ const self = this;
632
+ tile.command = new Cesium.DrawCommand({
633
+ shaderProgram: this.shaderProgram,
634
+ renderState: this.renderState,
635
+ pass: Cesium.Pass.TRANSLUCENT,
636
+ modelMatrix: Cesium.Matrix4.IDENTITY,
637
+ owner: this,
638
+ uniformMap: {
639
+ u_valueTexture: () => self.texture,
640
+ u_groundTexture: () => self.groundTexture,
641
+ u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
642
+ u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
643
+ u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
644
+ u_metresPerTexel: () => self.metresPerTexel(),
645
+ u_valueMin: () => self.valueMin,
646
+ u_valueRange: () => self.valueMax - self.valueMin,
647
+ u_exaggeration: () => self.exaggeration,
648
+ u_baseHeight: () => self.baseHeight,
649
+ u_skirtDepth: () => Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION),
650
+ u_coverageCutoff: () => COVERAGE_CUTOFF,
651
+ u_lowColor: () => self.lowColor,
652
+ u_highColor: () => self.highColor,
653
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
654
+ }
655
+ });
656
+ }
657
+ // Bounding volume belongs to the mesh, so it follows whichever tier is drawing.
658
+ tile.command.vertexArray = mesh.vertexArray;
659
+ tile.command.boundingVolume = mesh.boundingSphere;
660
+ return tile.command;
661
+ }
662
+ }
663
+ DisplacedSurfacePrimitive.Surface = Surface;
664
+ /*
665
+ * Cuts the extent into tiles, dropping any the caller says carry no data.
666
+ */
667
+ function buildTiles(hasCoverage) {
668
+ const tiles = [];
669
+ for (let row = 0; row < TILES_PER_SIDE; row++) {
670
+ for (let col = 0; col < TILES_PER_SIDE; col++) {
671
+ const patch = {
672
+ u0: col / TILES_PER_SIDE,
673
+ u1: (col + 1) / TILES_PER_SIDE,
674
+ v0: row / TILES_PER_SIDE,
675
+ v1: (row + 1) / TILES_PER_SIDE
676
+ };
677
+ if (hasCoverage && !hasCoverage(patch)) {
678
+ continue;
679
+ }
680
+ tiles.push({
681
+ patch,
682
+ meshes: new Map(),
683
+ tier: TIERS[0],
684
+ command: null,
685
+ centre: null,
686
+ radius: 0
687
+ });
688
+ }
689
+ }
690
+ return tiles;
691
+ }
692
+ /**
693
+ * Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
694
+ *
695
+ * Derived from the whole extent rather than per tile: scaling each tile to its own size would
696
+ * make neighbours disagree along their shared edge and tear the surface apart.
697
+ * @param extent
698
+ * @param valueMin
699
+ * @param valueMax
700
+ */
701
+ function autoExaggeration(extent, valueMin, valueMax) {
702
+ const range = Math.abs(valueMax - valueMin);
703
+ if (!(range > 0)) {
704
+ return 1;
705
+ }
706
+ const southWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0);
707
+ const southEast = Cesium.Cartesian3.fromDegrees(extent.East, extent.South, 0);
708
+ const northWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.North, 0);
709
+ const shorterSide = Math.min(Cesium.Cartesian3.distance(southWest, southEast), Cesium.Cartesian3.distance(southWest, northWest));
710
+ if (!(shorterSide > 0)) {
711
+ return 1;
712
+ }
713
+ return (shorterSide * AUTO_RELIEF_FRACTION) / range;
714
+ }
715
+ DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
716
+ function toCesiumColor(color) {
717
+ return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
718
+ }
719
+ function now() {
720
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
721
+ }
722
+ })(DisplacedSurfacePrimitive = exports.DisplacedSurfacePrimitive || (exports.DisplacedSurfacePrimitive = {}));
723
+ //# sourceMappingURL=displaced-surface-primitive.js.map