@geoblocks/cesium-post-process 0.1.0 → 0.1.2

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.
package/clouds.js ADDED
@@ -0,0 +1,684 @@
1
+ import {Cartesian3, Ellipsoid, Matrix4, PixelDatatype, Rectangle, Math as CesiumMath} from '@cesium/core';
2
+ import {PostProcessStage, PostProcessStageComposite, TextureMagnificationFilter, TextureMinificationFilter} from '@cesium/engine';
3
+ // @ts-expect-error the engine exports its 3D textures and samplers, but does not type them
4
+ import {Sampler, Texture3D, TextureWrap} from '@cesium/engine';
5
+ // @ts-expect-error the engine exports its textures, framebuffers and render states, but does not type them
6
+ import {Framebuffer, RenderState, Texture} from '@cesium/engine';
7
+ import {acquireCloudCover, releaseCloudCover} from './cloud-cover.js';
8
+ import createCloudMap from './cloud-map.js';
9
+ import createCloudNoise, {createMapNoise} from './cloud-noise.js';
10
+ import {acquireTerrainDepth, releaseTerrainDepth} from './depth-test.js';
11
+ import Effect from './effect.js';
12
+ import {acquireFrames, releaseFrames} from './frame-clock.js';
13
+ import {heightUniforms} from './height.js';
14
+ import CloudsBlur from './shaders/CloudsBlur.js';
15
+ import CloudsComposite from './shaders/CloudsComposite.js';
16
+ import CloudsDensity from './shaders/CloudsDensity.js';
17
+ import CloudsDepth from './shaders/CloudsDepth.js';
18
+ import CloudsMarch from './shaders/CloudsMarch.js';
19
+ import CloudsResolve from './shaders/CloudsResolve.js';
20
+ import CloudsShadow from './shaders/CloudsShadow.js';
21
+ import CloudsShadowMap from './shaders/CloudsShadowMap.js';
22
+ import CloudsSlab from './shaders/CloudsSlab.js';
23
+ import EyeFromDepth from './shaders/EyeFromDepth.js';
24
+ import Fog from './shaders/Fog.js';
25
+ import Height from './shaders/Height.js';
26
+ import Phase from './shaders/Phase.js';
27
+ import RainHaze from './shaders/RainHaze.js';
28
+ import WeatherMapShader from './shaders/WeatherMap.js';
29
+ import WeatherMap from './weather-map.js';
30
+
31
+ // the clouds are kilometers wide and soft: a quarter of the resolution, a 16th of the pixels, is
32
+ // enough for their raymarch (CloudsComposite.glsl's CLOUD_SCALE)
33
+ const CLOUD_SCALE = 0.25;
34
+ // voxels along each side of the noise, CloudsDensity.glsl's NOISE_VOXELS
35
+ const NOISE_SIZE = 64;
36
+ // the share of the previous frames in each frame's clouds, about 10 frames' worth (Hillaire 2016,
37
+ // sec. 5.5.3, blends 0.1 of the current one)
38
+ const HISTORY_WEIGHT = 0.9;
39
+ // after the camera, the clock or the map changes, the scene renders this long, in milliseconds, at
40
+ // this frame rate, for the accumulation to settle, also in requestRenderMode
41
+ const SETTLE_TIME = 500;
42
+ const SETTLE_RATE = 30;
43
+ // with a drift, the clouds move all the time: the scene renders at this frame rate while they are
44
+ // active, also in requestRenderMode
45
+ const DRIFT_RATE = 30;
46
+ // the shadow map: its texels across, over a square centered on the ground point under the camera,
47
+ // this many times as wide as the camera is high, between these sides, in meters, so that it covers
48
+ // the view from high above; drawn again once that point moves a 40th of it, its side changes by a
49
+ // quarter, or the clock or the clouds change, rather than at each frame
50
+ const SHADOW_SIZE = 512;
51
+ const SHADOW_SPREAD = 4;
52
+ const SHADOW_EXTENTS = [80000, 800000];
53
+ const SHADOW_MOVE = 1 / 40;
54
+ const SHADOW_RESIZE = 0.25;
55
+ // and at most once in this many milliseconds, while they keep changing, as during playback: a
56
+ // redraw takes as long as a frame of the clouds or longer
57
+ const SHADOW_INTERVAL = 250;
58
+ // the history is a copy of the resolve's output, drawn rather than copied: Cesium does not copy
59
+ // from a framebuffer into a texture of half floats
60
+ const COPY = `precision highp sampler2D;
61
+ uniform sampler2D source;
62
+ in vec2 v_textureCoordinates;
63
+ void main() {
64
+ out_FragColor = texture(source, v_textureCoordinates);
65
+ }
66
+ `;
67
+ // the clouds over the lightest rain are this thick, in meters, CloudsDensity.glsl's THIN_DEPTH
68
+ const THIN_DEPTH = 1500;
69
+ // the wind's offset of the noise wraps at this many meters, a period of its tile, CloudsMarch.glsl's
70
+ // NOISE_TILE, and of its larger lookup, the tile over LARGE_SCALE (0.27): single precision would
71
+ // lose the offset after days of wind
72
+ const WIND_PERIOD = 100 * 16000;
73
+
74
+ /**
75
+ * The noise's voxels, made once for all the clouds of the page: about a third of a second.
76
+ * @type {Uint8Array | undefined}
77
+ */
78
+ let noise;
79
+
80
+ /**
81
+ * The noise over the clouds' map, made once for all the clouds of the page.
82
+ * @type {Uint8Array | undefined}
83
+ */
84
+ let mapNoise;
85
+
86
+ // texels along each side of the map's noise
87
+ const MAP_NOISE_SIZE = 256;
88
+
89
+ /**
90
+ * Clouds from a map of the rain: a slab above the cloud base where the map
91
+ * has rain, a flat layer over drizzle and towers up to the cloud top over the
92
+ * heaviest rain, eroded by noise, lit by the sun with a silver lining toward
93
+ * it and by the sky, brighter at their top. Seen from far above they are
94
+ * cloud tops; the camera flies into them as it comes down. Activated before
95
+ * Precipitation, the rain, its shafts and its haze are drawn over them, so
96
+ * from below the cloud base they are seen through the rain. They are
97
+ * raymarched at a quarter of
98
+ * the resolution, accumulated over the frames, and upscaled by the scene's
99
+ * depth. Their noise drifts with the wind by the scene's clock, and with a
100
+ * drift by the real time; after a change, the scene renders for half a second
101
+ * for them to settle, then rests, unless they drift.
102
+ * Without a map, there are no clouds.
103
+ */
104
+ export default class Clouds extends Effect {
105
+ /**
106
+ * @param {import('@cesium/engine').CesiumWidget} viewer
107
+ * @param {{map?: import('./weather-map.js').WeatherMapOptions, cloudBase?: number, cloudTop?: number, density?: number, wind?: number, drift?: number}} [options]
108
+ */
109
+ constructor(viewer, options = {}) {
110
+ super(viewer);
111
+ this.cloudBase_ = options.cloudBase ?? 2500;
112
+ this.cloudTop_ = options.cloudTop ?? 9000;
113
+ this.density_ = options.density ?? 1;
114
+ this.wind_ = options.wind ?? 10;
115
+ this.drift_ = options.drift ?? 0;
116
+ // without a map, no clouds
117
+ this.weatherMap_ = new WeatherMap(0);
118
+ // the maps of the rain, from which the clouds' maps are made
119
+ /** @type {import('./weather-map.js').WeatherMapOptions | undefined} */
120
+ this.map_ = options.map;
121
+ /** @type {import('./weather-map.js').WeatherMapOptions | undefined} */
122
+ this.nextMap_ = undefined;
123
+ this.weatherMap_.map = options.map && createCloudMap(options.map);
124
+ // the map's intensity at the camera when it is inside the clouds, read each frame
125
+ this.localDensity_ = 0;
126
+ // made when the clouds are first drawn, as the map's texture
127
+ /** @type {any} */
128
+ this.noiseTexture_ = undefined;
129
+ /** @type {any} */
130
+ this.mapNoiseTexture_ = undefined;
131
+ // the accumulation over the frames: the frames drawn, the previous frames' clouds, their share,
132
+ // the previous frame's view and projection, and what makes the scene render until it settles
133
+ this.frame_ = 0;
134
+ /** @type {any} */
135
+ this.history_ = undefined;
136
+ /** @type {any} */
137
+ this.historyFramebuffer_ = undefined;
138
+ /** @type {any} */
139
+ this.copy_ = undefined;
140
+ /** @type {any} */
141
+ this.copySource_ = undefined;
142
+ this.historyWeight_ = 0;
143
+ this.previousViewProjection_ = new Matrix4();
144
+ this.reprojection_ = new Matrix4();
145
+ this.lastView_ = new Matrix4();
146
+ this.lastTime_ = {dayNumber: 0, secondsOfDay: 0};
147
+ this.changed_ = false;
148
+ this.settling_ = false;
149
+ this.settleUntil_ = 0;
150
+ // the shadow map, its ground point and time, and whether the clouds changed since it was drawn
151
+ /** @type {any} */
152
+ this.shadow_ = undefined;
153
+ /** @type {any} */
154
+ this.shadowFramebuffer_ = undefined;
155
+ /** @type {any} */
156
+ this.shadowCommand_ = undefined;
157
+ this.shadowCenter_ = new Cartesian3();
158
+ this.shadowExtent_ = SHADOW_EXTENTS[0];
159
+ this.shadowTime_ = {dayNumber: 0, secondsOfDay: 0};
160
+ this.shadowDrawnAt_ = -Infinity;
161
+ this.shadowChanged_ = true;
162
+ // for Precipitation, which lights its haze through it
163
+ /** @type {import('./cloud-cover.js').CloudShadow} */
164
+ this.shadowSource_ = {texture: () => this.shadow_, center: this.shadowCenter_, extent: () => this.shadowExtent_, base: () => this.cloudBase_, top: () => this.slabTop_()};
165
+ this.onPreRender_ = () => {
166
+ const {camera} = this.viewer.scene;
167
+ this.drawShadow_(this.viewer.scene);
168
+ const {longitude, latitude, height} = camera.positionCartographic;
169
+ const inside = height >= this.cloudBase_ && height <= this.cloudTop_;
170
+ this.localDensity_ = inside ? this.weatherMap_.intensityAt(longitude, latitude) : 0;
171
+ const time = this.viewer.clock.currentTime;
172
+ if (this.changed_ || !Matrix4.equals(camera.viewMatrix, this.lastView_) || time.dayNumber !== this.lastTime_.dayNumber || time.secondsOfDay !== this.lastTime_.secondsOfDay) {
173
+ this.changed_ = false;
174
+ Matrix4.clone(camera.viewMatrix, this.lastView_);
175
+ this.lastTime_ = {dayNumber: time.dayNumber, secondsOfDay: time.secondsOfDay};
176
+ this.settleUntil_ = Date.now() + SETTLE_TIME;
177
+ if (!this.settling_) {
178
+ this.settling_ = true;
179
+ acquireFrames(this.viewer.scene, SETTLE_RATE);
180
+ }
181
+ }
182
+ };
183
+ this.onPostRender_ = () => {
184
+ const {scene} = this.viewer;
185
+ this.keepHistory_(scene);
186
+ Matrix4.multiply(scene.camera.frustum.projectionMatrix, scene.camera.viewMatrix, this.previousViewProjection_);
187
+ this.frame_ = (this.frame_ + 1) % 1024;
188
+ if (this.settling_ && Date.now() > this.settleUntil_) {
189
+ this.settling_ = false;
190
+ releaseFrames(scene, SETTLE_RATE);
191
+ }
192
+ };
193
+ }
194
+
195
+ /**
196
+ * @override
197
+ * @param {import('@cesium/engine').Scene} scene
198
+ */
199
+ createStage_(scene) {
200
+ // the farthest depth of each block of pixels of the march, at its resolution
201
+ const depth = new PostProcessStage({
202
+ fragmentShader: EyeFromDepth + CloudsDepth,
203
+ textureScale: CLOUD_SCALE,
204
+ pixelDatatype: PixelDatatype.FLOAT,
205
+ });
206
+ const march = new PostProcessStage({
207
+ fragmentShader: EyeFromDepth + Height + CloudsSlab + WeatherMapShader + CloudsDensity + Fog + Phase + RainHaze + CloudsMarch,
208
+ uniforms: {
209
+ ...this.slabUniforms_(scene),
210
+ // @ts-expect-error the scene's context is not typed
211
+ ...this.densityUniforms_(scene.context),
212
+ cloudDepth: depth.name,
213
+ // @ts-expect-error the scene's context is not typed
214
+ maxMap: () => this.weatherMap_.maxTexture(scene.context),
215
+ frame: () => this.frame_,
216
+ skyLightIntensity: () => scene.skyAtmosphere?.atmosphereLightIntensity ?? 50,
217
+ },
218
+ textureScale: CLOUD_SCALE,
219
+ });
220
+ // accumulated over the frames
221
+ const resolve = new PostProcessStage({
222
+ fragmentShader: EyeFromDepth + Height + CloudsSlab + CloudsResolve,
223
+ uniforms: {
224
+ ...this.slabUniforms_(scene),
225
+ cloudDepth: depth.name,
226
+ cloudTexture: march.name,
227
+ // @ts-expect-error the scene's context is not typed
228
+ historyTexture: () => this.history_ ?? scene.context.defaultTexture,
229
+ reprojection: () => Matrix4.multiply(this.previousViewProjection_, scene.camera.inverseViewMatrix, this.reprojection_),
230
+ historyWeight: () => (this.history_ ? this.historyWeight_ : 0),
231
+ },
232
+ textureScale: CLOUD_SCALE,
233
+ pixelDatatype: PixelDatatype.HALF_FLOAT,
234
+ });
235
+ // blurred at the same resolution
236
+ const blurred = new PostProcessStage({
237
+ fragmentShader: CloudsBlur,
238
+ uniforms: {cloudTexture: resolve.name, cloudDepth: depth.name},
239
+ textureScale: CLOUD_SCALE,
240
+ });
241
+ return new PostProcessStageComposite({
242
+ stages: [
243
+ depth,
244
+ march,
245
+ resolve,
246
+ blurred,
247
+ new PostProcessStage({
248
+ fragmentShader: EyeFromDepth + CloudsShadow + CloudsComposite,
249
+ uniforms: {
250
+ ...this.shadowUniforms_(scene),
251
+ cloudTexture: blurred.name,
252
+ cloudDepth: depth.name,
253
+ },
254
+ }),
255
+ ],
256
+ // the clouds are drawn over the scene, not over the march
257
+ inputPreviousStageTexture: false,
258
+ });
259
+ }
260
+
261
+ /**
262
+ * The top of the tallest cloud the map makes, as CloudsDensity.glsl's cloudDensity makes the
263
+ * clouds of its tallest type, over its heaviest rain: from above, the march starts there rather than at the cloud
264
+ * top, so that its steps are in the clouds rather than in the empty air over them.
265
+ */
266
+ slabTop_() {
267
+ // the clouds' map's tallest type, in blue
268
+ const type = this.weatherMap_.maxValues[2];
269
+ return this.cloudBase_ + THIN_DEPTH + (this.cloudTop_ - this.cloudBase_ - THIN_DEPTH) * type;
270
+ }
271
+
272
+ /**
273
+ * The direction the wind blows toward, in world coordinates: the east at the map's center, fixed
274
+ * for the map rather than turning with the camera, which would shift the drifted noise.
275
+ */
276
+ windDirection_() {
277
+ const rectangle = this.weatherMap_.map?.rectangle;
278
+ if (!rectangle) {
279
+ return Cartesian3.ZERO;
280
+ }
281
+ const up = Ellipsoid.WGS84.geodeticSurfaceNormalCartographic(Rectangle.center(rectangle));
282
+ return Cartesian3.normalize(Cartesian3.cross(Cartesian3.UNIT_Z, up, new Cartesian3()), new Cartesian3());
283
+ }
284
+
285
+ /**
286
+ * How far the wind has carried the noise by the scene's clock, still while it is paused, and the
287
+ * drift by the real time, in world coordinates, in double precision and wrapped at WIND_PERIOD.
288
+ * @param {number} [seconds] the real time
289
+ */
290
+ windOffset_(seconds = performance.now() / 1000) {
291
+ const {dayNumber, secondsOfDay} = this.viewer.clock.currentTime;
292
+ const distance = this.wind_ * (dayNumber * 86400 + secondsOfDay) + this.drift_ * seconds;
293
+ const offset = Cartesian3.multiplyByScalar(this.windDirection_(), distance, new Cartesian3());
294
+ return new Cartesian3(offset.x % WIND_PERIOD, offset.y % WIND_PERIOD, offset.z % WIND_PERIOD);
295
+ }
296
+
297
+ /**
298
+ * The uniforms of CloudsSlab.glsl, with Height.glsl's it needs.
299
+ * @param {import('@cesium/engine').Scene} scene
300
+ */
301
+ slabUniforms_(scene) {
302
+ return {...heightUniforms(scene), cloudBase: () => this.cloudBase_, slabTop: () => this.slabTop_()};
303
+ }
304
+
305
+ /**
306
+ * The uniforms of CloudsDensity.glsl, with WeatherMap.glsl's it needs.
307
+ * @param {any} context the scene's context
308
+ */
309
+ densityUniforms_(context) {
310
+ return {
311
+ map: () => this.weatherMap_.texture(context),
312
+ nextMap: () => this.weatherMap_.nextTexture(context),
313
+ mapBlend: () => this.weatherMap_.blend,
314
+ mapBounds: () => this.weatherMap_.bounds,
315
+ cloudNoise: () => this.createNoiseTexture_(context),
316
+ mapNoise: () => this.createMapNoiseTexture_(context),
317
+ cloudTop: () => this.cloudTop_,
318
+ density: () => this.density_,
319
+ windDirection: () => this.windDirection_(),
320
+ windOffset: () => this.windOffset_(),
321
+ };
322
+ }
323
+
324
+ /**
325
+ * The uniforms of CloudsShadow.glsl, for the stages that read the shadow map.
326
+ * @param {import('@cesium/engine').Scene} scene
327
+ */
328
+ shadowUniforms_(scene) {
329
+ return {
330
+ // @ts-expect-error the scene's context is not typed
331
+ shadowMap: () => this.shadow_ ?? scene.context.defaultTexture,
332
+ shadowCenter: () => this.shadowCenter_,
333
+ shadowExtent: () => this.shadowExtent_,
334
+ shadowBase: () => this.cloudBase_,
335
+ shadowTop: () => this.slabTop_(),
336
+ };
337
+ }
338
+
339
+ /**
340
+ * The shadow map's side, in meters, for a camera this high.
341
+ * @param {number} height meters
342
+ */
343
+ shadowExtentFor_(height) {
344
+ return CesiumMath.clamp(SHADOW_SPREAD * height, SHADOW_EXTENTS[0], SHADOW_EXTENTS[1]);
345
+ }
346
+
347
+ /**
348
+ * Whether the shadow map, drawn around a ground point at a time, is to be drawn again.
349
+ * @param {Cartesian3} ground the ground point under the camera, in world coordinates
350
+ * @param {{dayNumber: number, secondsOfDay: number}} time the clock's
351
+ * @param {number} [extent] the side it would have now, in meters
352
+ */
353
+ shadowStale_(ground, time, extent = this.shadowExtent_) {
354
+ return (
355
+ this.shadowChanged_ ||
356
+ time.dayNumber !== this.shadowTime_.dayNumber ||
357
+ time.secondsOfDay !== this.shadowTime_.secondsOfDay ||
358
+ Cartesian3.distance(ground, this.shadowCenter_) > SHADOW_MOVE * this.shadowExtent_ ||
359
+ Math.abs(extent - this.shadowExtent_) > SHADOW_RESIZE * this.shadowExtent_ ||
360
+ // the drifting clouds, at every frame
361
+ this.drift_ > 0
362
+ );
363
+ }
364
+
365
+ /**
366
+ * Whether the shadow map is to be drawn again now: stale, and not drawn in the last
367
+ * SHADOW_INTERVAL.
368
+ * @param {Cartesian3} ground
369
+ * @param {{dayNumber: number, secondsOfDay: number}} time
370
+ * @param {number} now milliseconds
371
+ * @param {number} [extent] the side it would have now, in meters
372
+ */
373
+ shadowDue_(ground, time, now, extent) {
374
+ return this.shadowStale_(ground, time, extent) && now - this.shadowDrawnAt_ >= SHADOW_INTERVAL;
375
+ }
376
+
377
+ /**
378
+ * @param {Cartesian3} ground
379
+ * @param {{dayNumber: number, secondsOfDay: number}} time
380
+ * @param {number} [now] milliseconds
381
+ * @param {number} [extent] its side, in meters
382
+ */
383
+ shadowDrawn_(ground, time, now = Date.now(), extent = this.shadowExtent_) {
384
+ Cartesian3.clone(ground, this.shadowCenter_);
385
+ this.shadowExtent_ = extent;
386
+ this.shadowTime_ = {dayNumber: time.dayNumber, secondsOfDay: time.secondsOfDay};
387
+ this.shadowDrawnAt_ = now;
388
+ this.shadowChanged_ = false;
389
+ }
390
+
391
+ /**
392
+ * Draws the shadow map into its own texture before the stages run, when it is due: the
393
+ * stages' textures are sized after the screen. A redraw put off renders after the change
394
+ * settles, in the frames the accumulation asks for.
395
+ * @param {import('@cesium/engine').Scene} scene
396
+ */
397
+ drawShadow_(scene) {
398
+ const ground = Ellipsoid.WGS84.scaleToGeodeticSurface(scene.camera.positionWC, new Cartesian3());
399
+ const time = this.viewer.clock.currentTime;
400
+ const now = Date.now();
401
+ const extent = this.shadowExtentFor_(scene.camera.positionCartographic.height);
402
+ if (!ground || !this.shadowDue_(ground, time, now, extent)) {
403
+ return;
404
+ }
405
+ this.shadowDrawn_(ground, time, now, extent);
406
+ // @ts-expect-error the scene's context is not typed
407
+ const context = scene.context;
408
+ if (!context) {
409
+ return;
410
+ }
411
+ if (!this.shadow_) {
412
+ this.shadow_ = new Texture({context, width: SHADOW_SIZE, height: SHADOW_SIZE, pixelDatatype: PixelDatatype.HALF_FLOAT});
413
+ this.shadowFramebuffer_ = new Framebuffer({context, colorTextures: [this.shadow_], destroyAttachments: false});
414
+ this.shadowCommand_ = context.createViewportQuadCommand(Height + CloudsSlab + WeatherMapShader + CloudsDensity + CloudsShadow + CloudsShadowMap, {
415
+ framebuffer: this.shadowFramebuffer_,
416
+ renderState: RenderState.fromCache({viewport: {x: 0, y: 0, width: SHADOW_SIZE, height: SHADOW_SIZE}}),
417
+ uniformMap: {
418
+ ...this.slabUniforms_(scene),
419
+ ...this.densityUniforms_(context),
420
+ maxMap: () => this.weatherMap_.maxTexture(context),
421
+ shadowCenter: () => this.shadowCenter_,
422
+ shadowExtent: () => this.shadowExtent_,
423
+ },
424
+ });
425
+ }
426
+ this.shadowCommand_.execute(context);
427
+ }
428
+
429
+ destroyShadow_() {
430
+ this.shadowCommand_?.shaderProgram?.destroy();
431
+ this.shadowCommand_ = undefined;
432
+ this.shadowFramebuffer_ = this.shadowFramebuffer_?.destroy();
433
+ this.shadow_ = this.shadow_?.destroy();
434
+ this.shadowChanged_ = true;
435
+ }
436
+
437
+ /**
438
+ * Keeps this frame's accumulated clouds for the next one, drawn into the history at the size of
439
+ * the resolve's output, made again when it changes.
440
+ * @param {import('@cesium/engine').Scene} scene
441
+ */
442
+ keepHistory_(scene) {
443
+ // @ts-expect-error the collection's output textures are not typed
444
+ const output = this.stage_ && scene.postProcessStages.getOutputTexture?.(this.stage_.get(2).name);
445
+ if (!output) {
446
+ return;
447
+ }
448
+ // @ts-expect-error the scene's context is not typed
449
+ const context = scene.context;
450
+ if (!this.history_ || this.history_.width !== output.width || this.history_.height !== output.height) {
451
+ this.destroyHistory_();
452
+ this.history_ = new Texture({context, width: output.width, height: output.height, pixelDatatype: PixelDatatype.HALF_FLOAT});
453
+ this.historyFramebuffer_ = new Framebuffer({context, colorTextures: [this.history_], destroyAttachments: false});
454
+ this.historyWeight_ = 0;
455
+ }
456
+ this.copySource_ = output;
457
+ this.copy_ ??= context.createViewportQuadCommand(COPY, {uniformMap: {source: () => this.copySource_}});
458
+ this.copy_.framebuffer = this.historyFramebuffer_;
459
+ this.copy_.renderState = RenderState.fromCache({viewport: {x: 0, y: 0, width: output.width, height: output.height}});
460
+ this.copy_.execute(context);
461
+ this.historyWeight_ = HISTORY_WEIGHT;
462
+ }
463
+
464
+ destroyHistory_() {
465
+ this.historyFramebuffer_ = this.historyFramebuffer_?.destroy();
466
+ this.history_ = this.history_?.destroy();
467
+ }
468
+
469
+ /**
470
+ * The noise's texture, repeating in every direction, with its mipmaps for the far clouds.
471
+ * @param {any} context the scene's context
472
+ */
473
+ createNoiseTexture_(context) {
474
+ if (!this.noiseTexture_) {
475
+ noise ??= createCloudNoise(NOISE_SIZE);
476
+ this.noiseTexture_ = new Texture3D({
477
+ context,
478
+ flipY: false,
479
+ source: {width: NOISE_SIZE, height: NOISE_SIZE, depth: NOISE_SIZE, arrayBufferView: noise},
480
+ sampler: new Sampler({
481
+ wrapS: TextureWrap.REPEAT,
482
+ wrapT: TextureWrap.REPEAT,
483
+ wrapR: TextureWrap.REPEAT,
484
+ minificationFilter: TextureMinificationFilter.LINEAR_MIPMAP_LINEAR,
485
+ magnificationFilter: TextureMagnificationFilter.LINEAR,
486
+ }),
487
+ });
488
+ this.noiseTexture_.generateMipmap();
489
+ }
490
+ return this.noiseTexture_;
491
+ }
492
+
493
+ /**
494
+ * The noise over the clouds' map, repeating.
495
+ * @param {any} context the scene's context
496
+ */
497
+ createMapNoiseTexture_(context) {
498
+ if (!this.mapNoiseTexture_) {
499
+ mapNoise ??= createMapNoise(MAP_NOISE_SIZE);
500
+ this.mapNoiseTexture_ = new Texture({
501
+ context,
502
+ flipY: false,
503
+ source: {width: MAP_NOISE_SIZE, height: MAP_NOISE_SIZE, arrayBufferView: mapNoise},
504
+ sampler: new Sampler({
505
+ wrapS: TextureWrap.REPEAT,
506
+ wrapT: TextureWrap.REPEAT,
507
+ minificationFilter: TextureMinificationFilter.LINEAR,
508
+ magnificationFilter: TextureMagnificationFilter.LINEAR,
509
+ }),
510
+ });
511
+ }
512
+ return this.mapNoiseTexture_;
513
+ }
514
+
515
+ /**
516
+ * @override
517
+ * @param {import('@cesium/engine').Scene} scene
518
+ */
519
+ activated_(scene) {
520
+ if (this.drift_ > 0) {
521
+ acquireFrames(scene, DRIFT_RATE);
522
+ }
523
+ scene.preRender.addEventListener(this.onPreRender_);
524
+ scene.postRender.addEventListener(this.onPostRender_);
525
+ acquireTerrainDepth(scene);
526
+ acquireCloudCover(scene, this.shadowSource_);
527
+ }
528
+
529
+ /**
530
+ * @override
531
+ * @param {import('@cesium/engine').Scene} scene
532
+ */
533
+ deactivating_(scene) {
534
+ releaseCloudCover(scene, this.shadowSource_);
535
+ releaseTerrainDepth(scene);
536
+ scene.preRender.removeEventListener(this.onPreRender_);
537
+ scene.postRender.removeEventListener(this.onPostRender_);
538
+ if (this.settling_) {
539
+ this.settling_ = false;
540
+ releaseFrames(scene, SETTLE_RATE);
541
+ }
542
+ if (this.drift_ > 0) {
543
+ releaseFrames(scene, DRIFT_RATE);
544
+ }
545
+ this.destroyHistory_();
546
+ this.destroyShadow_();
547
+ this.copy_?.shaderProgram?.destroy();
548
+ this.copy_ = undefined;
549
+ this.weatherMap_.destroy();
550
+ this.noiseTexture_ = this.noiseTexture_?.destroy();
551
+ this.mapNoiseTexture_ = this.mapNoiseTexture_?.destroy();
552
+ }
553
+
554
+ /**
555
+ * Where it rains, or undefined for no clouds: an image whose red channel
556
+ * is the local intensity, 0 to 1, over a rectangle of longitudes and
557
+ * latitudes, north up, as Precipitation's map. Set it again after drawing
558
+ * on the image, which is read then.
559
+ */
560
+ get map() {
561
+ return this.map_;
562
+ }
563
+
564
+ set map(value) {
565
+ this.map_ = value;
566
+ this.weatherMap_.map = value && createCloudMap(value);
567
+ this.shadowChanged_ = true;
568
+ // the previous map's clouds would linger
569
+ this.historyWeight_ = 0;
570
+ this.changed_ = true;
571
+ this.viewer.scene.requestRender();
572
+ }
573
+
574
+ /**
575
+ * The map the clouds crossfade to by the map blend, over the same
576
+ * rectangle, as the radar's next hour, or undefined for none.
577
+ */
578
+ get nextMap() {
579
+ return this.nextMap_;
580
+ }
581
+
582
+ set nextMap(value) {
583
+ this.nextMap_ = value;
584
+ this.weatherMap_.nextMap = value && createCloudMap(value);
585
+ this.shadowChanged_ = true;
586
+ this.viewer.scene.requestRender();
587
+ }
588
+
589
+ /**
590
+ * From the map, 0, to the next map, 1.
591
+ */
592
+ get mapBlend() {
593
+ return this.weatherMap_.blend;
594
+ }
595
+
596
+ set mapBlend(value) {
597
+ this.weatherMap_.blend = value;
598
+ this.shadowChanged_ = true;
599
+ this.viewer.scene.requestRender();
600
+ }
601
+
602
+ /**
603
+ * Altitude of the cloud base, in meters above the ellipsoid, as
604
+ * Precipitation's.
605
+ */
606
+ get cloudBase() {
607
+ return this.cloudBase_;
608
+ }
609
+
610
+ set cloudBase(value) {
611
+ this.cloudBase_ = value;
612
+ this.shadowChanged_ = true;
613
+ this.viewer.scene.requestRender();
614
+ }
615
+
616
+ /**
617
+ * Altitude of the highest cloud tops, over the heaviest rain, in meters
618
+ * above the ellipsoid; over drizzle the clouds are 1500 m thick.
619
+ */
620
+ get cloudTop() {
621
+ return this.cloudTop_;
622
+ }
623
+
624
+ set cloudTop(value) {
625
+ this.cloudTop_ = value;
626
+ this.shadowChanged_ = true;
627
+ this.viewer.scene.requestRender();
628
+ }
629
+
630
+ /**
631
+ * Scale of the clouds' opacity: 1 for solid clouds over heavy rain.
632
+ */
633
+ get density() {
634
+ return this.density_;
635
+ }
636
+
637
+ set density(value) {
638
+ this.density_ = value;
639
+ this.shadowChanged_ = true;
640
+ this.viewer.scene.requestRender();
641
+ }
642
+
643
+ /**
644
+ * Speed of the wind that carries the clouds' noise toward the east, in m/s,
645
+ * by the scene's clock: a time-lapse while it runs faster, still while it
646
+ * is paused.
647
+ */
648
+ get wind() {
649
+ return this.wind_;
650
+ }
651
+
652
+ set wind(value) {
653
+ this.wind_ = value;
654
+ this.shadowChanged_ = true;
655
+ this.viewer.scene.requestRender();
656
+ }
657
+
658
+ /**
659
+ * Speed of the drift of the clouds' noise toward the east, in m/s, by the
660
+ * real time rather than the scene's clock: the clouds move while the clock
661
+ * is paused, and the scene renders all the time while they are active. 0,
662
+ * by default, for none.
663
+ */
664
+ get drift() {
665
+ return this.drift_;
666
+ }
667
+
668
+ set drift(value) {
669
+ const drifting = this.drift_ > 0;
670
+ this.drift_ = value;
671
+ if (this.stage_ && drifting !== value > 0) {
672
+ (value > 0 ? acquireFrames : releaseFrames)(this.viewer.scene, DRIFT_RATE);
673
+ }
674
+ this.viewer.scene.requestRender();
675
+ }
676
+
677
+ /**
678
+ * The map's intensity at the camera when it is inside the clouds, 0 to 1,
679
+ * read each frame while active; 0 outside them.
680
+ */
681
+ get localDensity() {
682
+ return this.localDensity_;
683
+ }
684
+ }
@@ -1,7 +1,6 @@
1
1
  import {PostProcessStage} from '@cesium/engine';
2
2
  import Effect from './effect.js';
3
3
  import ColorIsolationShader from './shaders/ColorIsolation.js';
4
- import Hsl from './shaders/Hsl.js';
5
4
 
6
5
  /**
7
6
  * Color isolation: one hue keeps its color, the rest of the scene turns gray.
@@ -21,7 +20,7 @@ export default class ColorIsolation extends Effect {
21
20
  /** @override */
22
21
  createStage_() {
23
22
  return new PostProcessStage({
24
- fragmentShader: Hsl + ColorIsolationShader,
23
+ fragmentShader: ColorIsolationShader,
25
24
  uniforms: {
26
25
  hue: () => this.hue_ / 360,
27
26
  range: () => this.range_ / 360,