@geoblocks/cesium-post-process 0.0.0-stage → 0.1.1

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 (89) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +235 -2
  3. package/analog-video.js +138 -0
  4. package/blur.js +39 -0
  5. package/cesium-post-process.js +23 -0
  6. package/cloud-cover.js +49 -0
  7. package/cloud-map.js +122 -0
  8. package/cloud-noise.js +229 -0
  9. package/clouds.js +684 -0
  10. package/color-isolation.js +69 -0
  11. package/depth-test.js +37 -0
  12. package/digital-video.js +108 -0
  13. package/drone-display.js +170 -0
  14. package/effect.js +64 -0
  15. package/focus.js +25 -0
  16. package/frame-clock.js +61 -0
  17. package/height.js +23 -0
  18. package/infrared.js +40 -0
  19. package/jello.js +146 -0
  20. package/lens-distortion.js +68 -0
  21. package/lightning.js +321 -0
  22. package/motion-blur.js +117 -0
  23. package/package.json +55 -4
  24. package/precipitation.js +493 -0
  25. package/shaders/Alpenglow.js +2 -0
  26. package/shaders/AnalogVideo.js +2 -0
  27. package/shaders/Beam.js +2 -0
  28. package/shaders/CloudsBlur.js +2 -0
  29. package/shaders/CloudsComposite.js +2 -0
  30. package/shaders/CloudsDensity.js +2 -0
  31. package/shaders/CloudsDepth.js +2 -0
  32. package/shaders/CloudsMarch.js +2 -0
  33. package/shaders/CloudsResolve.js +2 -0
  34. package/shaders/CloudsShadow.js +2 -0
  35. package/shaders/CloudsShadowMap.js +2 -0
  36. package/shaders/CloudsSlab.js +2 -0
  37. package/shaders/ColorIsolation.js +2 -0
  38. package/shaders/DepthEdges.js +2 -0
  39. package/shaders/DigitalVideo.js +2 -0
  40. package/shaders/DroneDisplay.js +2 -0
  41. package/shaders/EyeFromDepth.js +2 -0
  42. package/shaders/Filmic.js +2 -0
  43. package/shaders/Flashlight.js +2 -0
  44. package/shaders/Fog.js +2 -0
  45. package/shaders/Frame.js +2 -0
  46. package/shaders/Hash.js +2 -0
  47. package/shaders/Height.js +2 -0
  48. package/shaders/Hsl.js +2 -0
  49. package/shaders/Infrared.js +2 -0
  50. package/shaders/Jello.js +2 -0
  51. package/shaders/LensDistortion.js +2 -0
  52. package/shaders/Lightning.js +2 -0
  53. package/shaders/MotionBlur.js +2 -0
  54. package/shaders/MotionBlurVelocity.js +2 -0
  55. package/shaders/Noise.js +2 -0
  56. package/shaders/Normal.js +2 -0
  57. package/shaders/Phase.js +2 -0
  58. package/shaders/Precipitation.js +2 -0
  59. package/shaders/PrecipitationShaftBlur.js +2 -0
  60. package/shaders/PrecipitationShafts.js +2 -0
  61. package/shaders/PrecipitationWind.js +2 -0
  62. package/shaders/RainHaze.js +2 -0
  63. package/shaders/Shockwave.js +2 -0
  64. package/shaders/SkyTable.js +2 -0
  65. package/shaders/SnowLine.js +2 -0
  66. package/shaders/SpeedLines.js +2 -0
  67. package/shaders/Spotlight.js +2 -0
  68. package/shaders/SpotlightBeam.js +2 -0
  69. package/shaders/SpotlightBeamBlur.js +2 -0
  70. package/shaders/SpotlightCone.js +2 -0
  71. package/shaders/Super8.js +2 -0
  72. package/shaders/Technicolor.js +2 -0
  73. package/shaders/TiltShift.js +2 -0
  74. package/shaders/Toy.js +2 -0
  75. package/shaders/Trails.js +2 -0
  76. package/shaders/ValleyFog.js +2 -0
  77. package/shaders/WeatherMap.js +2 -0
  78. package/shaders/WhiteModel.js +2 -0
  79. package/snow-line.js +108 -0
  80. package/speed-lines.js +71 -0
  81. package/spotlight.js +193 -0
  82. package/strikes.js +30 -0
  83. package/super8.js +164 -0
  84. package/technicolor.js +56 -0
  85. package/tilt-shift.js +124 -0
  86. package/toy.js +192 -0
  87. package/valley-fog.js +105 -0
  88. package/weather-map.js +290 -0
  89. package/white-model.js +105 -0
package/jello.js ADDED
@@ -0,0 +1,146 @@
1
+ import {Cartesian4} from '@cesium/core';
2
+ import {PostProcessStage} from '@cesium/engine';
3
+ import Effect from './effect.js';
4
+ import {acquireFrames, releaseFrames} from './frame-clock.js';
5
+ import JelloShader from './shaders/Jello.js';
6
+
7
+ // the shake renders at this rate, like a video camera
8
+ const FRAME_RATE = 30;
9
+ // the shake's sinusoids, as multiples of the frequency, and their share of the amount, so that
10
+ // it looks like noise rather than a single wave
11
+ const HARMONICS = [1, 1.37, 2.11, 3.07];
12
+ const WEIGHTS = [1, 0.6, 0.35, 0.2];
13
+ // the most the sinusoids reach together, as multiples of the first one's amplitude
14
+ const PEAK = WEIGHTS.reduce((sum, weight) => sum + weight, 0);
15
+ // at full amount: shift in picture heights, rotation in radians
16
+ const MAX_SHIFT = 0.015;
17
+ const MAX_ROTATION = 0.008;
18
+ // amplitude of the first sinusoid at full amount, per axis: x, y, rotation
19
+ const MAX_AMPLITUDES = [MAX_SHIFT, 0.5 * MAX_SHIFT, MAX_ROTATION];
20
+
21
+ const TAU = 2 * Math.PI;
22
+ /** @type {('x' | 'y' | 'z' | 'w')[]} one sinusoid per component */
23
+ const COMPONENTS = ['x', 'y', 'z', 'w'];
24
+
25
+ /**
26
+ * The jello of a vibrating camera with a rolling shutter: straight edges
27
+ * wobble as each row of the sensor is read at a different moment of the
28
+ * shake. The camera moves, so while active with an amount the scene renders
29
+ * at 30 frames per second, also in requestRenderMode.
30
+ */
31
+ export default class Jello extends Effect {
32
+ /**
33
+ * @param {import('@cesium/engine').CesiumWidget} viewer
34
+ * @param {{amount?: number, frequency?: number}} [options]
35
+ */
36
+ constructor(viewer, options = {}) {
37
+ super(viewer);
38
+ this.amount_ = options.amount ?? 0.1;
39
+ this.frequency_ = options.frequency ?? 30;
40
+ // a random starting phase for each sinusoid and axis
41
+ this.offsets_ = HARMONICS.map(() => [Math.random(), Math.random(), Math.random()].map((r) => r * TAU));
42
+ // the uniforms, updated when the amount or the frequency change (the shape) and
43
+ // once per frame (the phases)
44
+ this.shakeHz_ = new Cartesian4();
45
+ this.amplitudes_ = [new Cartesian4(), new Cartesian4(), new Cartesian4()];
46
+ this.zoom_ = 1;
47
+ this.phases_ = [new Cartesian4(), new Cartesian4(), new Cartesian4()];
48
+ this.updateShape_();
49
+ // the phases of the sinusoids at the frame's time, reduced in double precision:
50
+ // a float would lose the time's fraction of a second
51
+ this.onPreRender_ = () => {
52
+ const seconds = performance.now() / 1000;
53
+ HARMONICS.forEach((h, i) => {
54
+ const phase = TAU * ((h * this.frequency_ * seconds) % 1);
55
+ this.phases_.forEach((phases, axis) => {
56
+ phases[COMPONENTS[i]] = (phase + this.offsets_[i][axis]) % TAU;
57
+ });
58
+ });
59
+ };
60
+ }
61
+
62
+ /** @override */
63
+ createStage_() {
64
+ const stage = new PostProcessStage({
65
+ fragmentShader: JelloShader,
66
+ uniforms: {
67
+ shakeHz: () => this.shakeHz_,
68
+ phaseX: () => this.phases_[0],
69
+ phaseY: () => this.phases_[1],
70
+ phaseRotation: () => this.phases_[2],
71
+ amplitudeX: () => this.amplitudes_[0],
72
+ amplitudeY: () => this.amplitudes_[1],
73
+ amplitudeRotation: () => this.amplitudes_[2],
74
+ zoom: () => this.zoom_,
75
+ },
76
+ });
77
+ // no pass, and no frames, without an amount
78
+ stage.enabled = this.amount_ > 0;
79
+ return stage;
80
+ }
81
+
82
+ /**
83
+ * @override
84
+ * @param {import('@cesium/engine').Scene} scene
85
+ */
86
+ activated_(scene) {
87
+ scene.preRender.addEventListener(this.onPreRender_);
88
+ if (this.amount_ > 0) {
89
+ acquireFrames(scene, FRAME_RATE);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @override
95
+ * @param {import('@cesium/engine').Scene} scene
96
+ */
97
+ deactivating_(scene) {
98
+ if (this.amount_ > 0) {
99
+ releaseFrames(scene, FRAME_RATE);
100
+ }
101
+ scene.preRender.removeEventListener(this.onPreRender_);
102
+ }
103
+
104
+ updateShape_() {
105
+ HARMONICS.forEach((h, i) => {
106
+ this.shakeHz_[COMPONENTS[i]] = h * this.frequency_;
107
+ this.amplitudes_.forEach((amplitudes, axis) => {
108
+ amplitudes[COMPONENTS[i]] = WEIGHTS[i] * MAX_AMPLITUDES[axis] * this.amount_;
109
+ });
110
+ });
111
+ // enough to hide the edges when all the sinusoids peak together: the shift, and the
112
+ // rotation of the corners, about a picture height from the middle
113
+ this.zoom_ = 1 + 2 * PEAK * this.amount_ * (MAX_SHIFT + MAX_ROTATION);
114
+ }
115
+
116
+ /**
117
+ * Strength of the shake, 0 to 1.
118
+ */
119
+ get amount() {
120
+ return this.amount_;
121
+ }
122
+
123
+ set amount(value) {
124
+ const shaking = this.amount_ > 0;
125
+ this.amount_ = value;
126
+ this.updateShape_();
127
+ if (this.stage_ && shaking !== value > 0) {
128
+ this.stage_.enabled = value > 0;
129
+ (value > 0 ? acquireFrames : releaseFrames)(this.viewer.scene, FRAME_RATE);
130
+ }
131
+ this.viewer.scene.requestRender();
132
+ }
133
+
134
+ /**
135
+ * Frequency of the vibration, in Hz: the rows read over the 20 ms readout
136
+ * see a fifth of a wave at 10 Hz, more than a whole one at 60 Hz.
137
+ */
138
+ get frequency() {
139
+ return this.frequency_;
140
+ }
141
+
142
+ set frequency(value) {
143
+ this.frequency_ = value;
144
+ this.updateShape_();
145
+ }
146
+ }
@@ -0,0 +1,68 @@
1
+ import {PostProcessStage} from '@cesium/engine';
2
+ import Effect from './effect.js';
3
+ import LensDistortionShader from './shaders/LensDistortion.js';
4
+
5
+ /**
6
+ * The barrel distortion and dark corners of a wide-angle lens, like those of
7
+ * FPV cameras.
8
+ */
9
+ export default class LensDistortion extends Effect {
10
+ /**
11
+ * @param {import('@cesium/engine').CesiumWidget} viewer
12
+ * @param {{distortion?: number, vignette?: number}} [options]
13
+ */
14
+ constructor(viewer, options = {}) {
15
+ super(viewer);
16
+ this.distortion_ = options.distortion ?? 0.5;
17
+ this.vignette_ = options.vignette ?? 0.5;
18
+ }
19
+
20
+ /** @override */
21
+ createStage_() {
22
+ const stage = new PostProcessStage({
23
+ fragmentShader: LensDistortionShader,
24
+ uniforms: {
25
+ distortion: () => this.distortion_,
26
+ vignette: () => this.vignette_,
27
+ },
28
+ });
29
+ // no pass at all without distortion or vignette
30
+ stage.enabled = this.enabled_();
31
+ return stage;
32
+ }
33
+
34
+ enabled_() {
35
+ return this.distortion_ > 0 || this.vignette_ > 0;
36
+ }
37
+
38
+ /**
39
+ * Bending of straight lines toward the edges, 0 to 1.
40
+ */
41
+ get distortion() {
42
+ return this.distortion_;
43
+ }
44
+
45
+ set distortion(value) {
46
+ this.distortion_ = value;
47
+ this.update_();
48
+ }
49
+
50
+ /**
51
+ * Darkening of the corners, 0 to 1.
52
+ */
53
+ get vignette() {
54
+ return this.vignette_;
55
+ }
56
+
57
+ set vignette(value) {
58
+ this.vignette_ = value;
59
+ this.update_();
60
+ }
61
+
62
+ update_() {
63
+ if (this.stage_) {
64
+ this.stage_.enabled = this.enabled_();
65
+ }
66
+ this.viewer.scene.requestRender();
67
+ }
68
+ }
package/lightning.js ADDED
@@ -0,0 +1,321 @@
1
+ import {Cartesian3, Cartesian4, Cartographic, Event, Matrix4} from '@cesium/core';
2
+ import {PostProcessStage} from '@cesium/engine';
3
+ import {acquireTerrainDepth, releaseTerrainDepth} from './depth-test.js';
4
+ import Effect from './effect.js';
5
+ import {acquireFrames, releaseFrames} from './frame-clock.js';
6
+ import {advanceStrikes, MAX_STRIKES} from './strikes.js';
7
+ import WeatherMap from './weather-map.js';
8
+ import EyeFromDepth from './shaders/EyeFromDepth.js';
9
+ import Hash from './shaders/Hash.js';
10
+ import LightningShader from './shaders/Lightning.js';
11
+
12
+ // the flickering of the strokes is tens of milliseconds long: a render for each frame of a video
13
+ const FRAME_RATE = 30;
14
+ // strikes per second at intensity 1
15
+ const MAX_RATE = 0.4;
16
+ // the nearest a strike lands, in meters, and the least height of its channel
17
+ const MIN_DISTANCE = 300;
18
+ const MIN_CHANNEL = 500;
19
+ // longest time between two frames, in seconds, after a pause in requestRenderMode
20
+ const MAX_STEP = 0.1;
21
+ const TWO_PI = 2 * Math.PI;
22
+ // the branches of a channel (Lightning.glsl's BRANCHES), the share of them that exist, and how far down
23
+ // the channel they leave it and at what angle from it, in radians, at most
24
+ const BRANCHES = 5;
25
+ const BRANCH_ODDS = 0.7;
26
+ const BRANCH_ANGLE = [0.25, 0.75];
27
+ // how far from the camera's heading a strike lands in front of it, in radians
28
+ const AHEAD = Math.PI / 6;
29
+ // the radar's intensity, 0 to 1, over which a point goes from never to always struck: from about
30
+ // 2.5 to 8 mm/h, the map being the square root of the rate over 10 mm/h
31
+ const STORM = [0.5, 0.9];
32
+ // the farthest the strikes land from a camera high above, in meters, and the most the chance of a
33
+ // strike grows with the area they land in
34
+ const MAX_RADIUS = 150000;
35
+ const MAX_AREA = 16;
36
+ // the points tried for a strike before giving it up
37
+ const ATTEMPTS = 8;
38
+ // the glow of a strike in the clouds is this high above the cloud base, in meters
39
+ const GLOW_HEIGHT = 1500;
40
+ // the glow and the channel cross-fade over this height on either side of the cloud base, in meters
41
+ const ABOVE_FADE = 200;
42
+
43
+ /**
44
+ * @param {number} edge0
45
+ * @param {number} edge1
46
+ * @param {number} x
47
+ */
48
+ function smoothstep(edge0, edge1, x) {
49
+ const t = Math.min(Math.max((x - edge0) / (edge1 - edge0), 0), 1);
50
+ return t * t * (3 - 2 * t);
51
+ }
52
+
53
+ /**
54
+ * @typedef {{start: number, seed: number, ground: Cartesian3, top: Cartesian3, glow: Cartesian3, glowIntensity: number, branches: number[][]}} Strike
55
+ */
56
+
57
+ const eyeScratch = new Cartesian3();
58
+
59
+ /**
60
+ * Lightning: cloud-to-ground strikes at random around the camera, each a
61
+ * branching channel from the cloud base to the ground with 2 to 4 return
62
+ * strokes tens of milliseconds apart, a hot core in a halo that flickers with
63
+ * them, hidden behind nearer terrain, and a flash that lights the land around
64
+ * the strike and the clouds above it. The strikes are placed on the ground,
65
+ * so they stay where they are as the camera moves. With a map, they land on
66
+ * heavy rain, around what the camera sees: within the radius, or as far as
67
+ * the camera is high, up to 150 km. The strokes flicker, so while it is active
68
+ * with an intensity the scene renders at 30 frames per second, also in
69
+ * requestRenderMode.
70
+ */
71
+ export default class Lightning extends Effect {
72
+ /**
73
+ * @param {import('@cesium/engine').CesiumWidget} viewer
74
+ * @param {{intensity?: number, cloudBase?: number, radius?: number, inFront?: boolean, map?: import('./weather-map.js').WeatherMapOptions}} [options]
75
+ */
76
+ constructor(viewer, options = {}) {
77
+ super(viewer);
78
+ this.intensity_ = options.intensity ?? 0.5;
79
+ this.cloudBase_ = options.cloudBase ?? 2500;
80
+ this.radius_ = options.radius ?? 5000;
81
+ this.inFront_ = options.inFront ?? false;
82
+ // without a map, it strikes anywhere in the radius
83
+ this.weatherMap_ = new WeatherMap(1);
84
+ this.weatherMap_.map = options.map;
85
+ /** @type {Strike[]} */
86
+ this.strikes_ = [];
87
+ // what the shader gets of each strike, in eye coordinates: the top of its channel and its seed,
88
+ // its foot and its age, negative for a slot without a strike
89
+ this.strikeTop_ = Array.from({length: MAX_STRIKES}, () => new Cartesian4());
90
+ this.strikeBottom_ = Array.from({length: MAX_STRIKES}, () => new Cartesian4(0, 0, 0, -1));
91
+ // and each one's branches: where along the channel they leave it, the cosine and the sine of their
92
+ // angle from it, and their length as a share of the channel's, 0 for a branch that does not exist
93
+ this.strikeBranches_ = Array.from({length: MAX_STRIKES * BRANCHES}, () => new Cartesian4());
94
+ // the glow of each strike in the clouds, in eye coordinates, and its intensity, 0 for none
95
+ this.strikeGlow_ = Array.from({length: MAX_STRIKES}, () => new Cartesian4());
96
+ // 0 below the cloud base to 1 above it, where the glow replaces the channel
97
+ this.above_ = 0;
98
+ this.lastTime_ = 0;
99
+ /**
100
+ * Raised with the foot of each new strike on the ground, as it starts.
101
+ * @type {Event<(ground: Cartesian3) => void>}
102
+ */
103
+ this.strikeEvent = new Event();
104
+ this.onPreRender_ = () => {
105
+ const scene = this.viewer.scene;
106
+ const now = performance.now() / 1000;
107
+ const dt = Math.min(now - this.lastTime_, MAX_STEP);
108
+ this.lastTime_ = now;
109
+ const area = Math.min((this.usedRadius_() / this.radius_) ** 2, MAX_AREA);
110
+ advanceStrikes(this.strikes_, now, dt, this.intensity_ * MAX_RATE * area, Math.random, (start) => {
111
+ const strike = this.createStrike_(start);
112
+ if (strike) {
113
+ this.strikeEvent.raiseEvent(strike.ground);
114
+ }
115
+ return strike;
116
+ });
117
+ this.above_ = Math.min(Math.max((scene.camera.positionCartographic.height - this.cloudBase_ + ABOVE_FADE) / (2 * ABOVE_FADE), 0), 1);
118
+ const view = scene.camera.viewMatrix;
119
+ for (let i = 0; i < MAX_STRIKES; i++) {
120
+ const strike = this.strikes_[i];
121
+ if (!strike) {
122
+ this.strikeBottom_[i].w = -1;
123
+ this.strikeGlow_[i].w = 0;
124
+ continue;
125
+ }
126
+ Matrix4.multiplyByPoint(view, strike.top, eyeScratch);
127
+ Cartesian4.fromElements(eyeScratch.x, eyeScratch.y, eyeScratch.z, strike.seed, this.strikeTop_[i]);
128
+ Matrix4.multiplyByPoint(view, strike.ground, eyeScratch);
129
+ Cartesian4.fromElements(eyeScratch.x, eyeScratch.y, eyeScratch.z, now - strike.start, this.strikeBottom_[i]);
130
+ for (let j = 0; j < BRANCHES; j++) {
131
+ const [at, angle, length] = strike.branches[j];
132
+ Cartesian4.fromElements(at, Math.cos(angle), Math.sin(angle), length, this.strikeBranches_[i * BRANCHES + j]);
133
+ }
134
+ Matrix4.multiplyByPoint(view, strike.glow, eyeScratch);
135
+ Cartesian4.fromElements(eyeScratch.x, eyeScratch.y, eyeScratch.z, strike.glowIntensity, this.strikeGlow_[i]);
136
+ }
137
+ };
138
+ }
139
+
140
+ /**
141
+ * How far from the camera the strikes land: the radius, or the camera's
142
+ * height when it is higher, so that they land around what it sees.
143
+ */
144
+ usedRadius_() {
145
+ const height = this.viewer.scene.camera.positionCartographic.height;
146
+ return Math.min(Math.max(this.radius_, height), MAX_RADIUS);
147
+ }
148
+
149
+ /**
150
+ * A strike at a random place within the radius, uniformly over the area,
151
+ * kept with a chance of the map's intensity there.
152
+ * @param {number} start
153
+ * @return {Strike | undefined}
154
+ */
155
+ createStrike_(start) {
156
+ const scene = this.viewer.scene;
157
+ const {longitude, latitude} = scene.camera.positionCartographic;
158
+ const radius = scene.ellipsoid.maximumRadius;
159
+ // a few points tried, so that the strikes find the heavy rain within the radius rather than
160
+ // mostly missing it
161
+ for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
162
+ const azimuth = this.inFront_ ? scene.camera.heading + (Math.random() * 2 - 1) * AHEAD : Math.random() * TWO_PI;
163
+ const distance = MIN_DISTANCE + Math.max(this.usedRadius_() - MIN_DISTANCE, 0) * Math.sqrt(Math.random());
164
+ const strikeLongitude = longitude + (distance * Math.sin(azimuth)) / (radius * Math.cos(latitude));
165
+ const strikeLatitude = latitude + (distance * Math.cos(azimuth)) / radius;
166
+ const glowIntensity = this.weatherMap_.intensityAt(strikeLongitude, strikeLatitude);
167
+ if (!this.weatherMap_.map || Math.random() < smoothstep(STORM[0], STORM[1], glowIntensity)) {
168
+ return this.strikeAt_(start, strikeLongitude, strikeLatitude, glowIntensity);
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ /**
175
+ * A strike at a place, with its random branches.
176
+ * @param {number} start
177
+ * @param {number} strikeLongitude radians
178
+ * @param {number} strikeLatitude radians
179
+ * @param {number} glowIntensity the map's intensity there
180
+ * @return {Strike}
181
+ */
182
+ strikeAt_(start, strikeLongitude, strikeLatitude, glowIntensity) {
183
+ const scene = this.viewer.scene;
184
+ const branches = Array.from({length: BRANCHES}, () => {
185
+ const at = 0.1 + 0.7 * Math.random();
186
+ const side = Math.random() < 0.5 ? -1 : 1;
187
+ const angle = side * (BRANCH_ANGLE[0] + (BRANCH_ANGLE[1] - BRANCH_ANGLE[0]) * Math.random());
188
+ return [at, angle, Math.random() < BRANCH_ODDS ? (1 - at) * (0.25 + 0.4 * Math.random()) : 0];
189
+ });
190
+ const height = scene.globe?.getHeight(new Cartographic(strikeLongitude, strikeLatitude)) ?? 0;
191
+ return {
192
+ start,
193
+ seed: Math.random() * 100,
194
+ branches,
195
+ ground: Cartesian3.fromRadians(strikeLongitude, strikeLatitude, height, scene.ellipsoid),
196
+ top: Cartesian3.fromRadians(strikeLongitude, strikeLatitude, Math.max(this.cloudBase_, height + MIN_CHANNEL), scene.ellipsoid),
197
+ glow: Cartesian3.fromRadians(strikeLongitude, strikeLatitude, this.cloudBase_ + GLOW_HEIGHT, scene.ellipsoid),
198
+ glowIntensity,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * @override
204
+ * @param {import('@cesium/engine').Scene} _scene
205
+ */
206
+ createStage_(_scene) {
207
+ const stage = new PostProcessStage({
208
+ fragmentShader: Hash + EyeFromDepth + LightningShader,
209
+ uniforms: {
210
+ strikeTop: () => this.strikeTop_,
211
+ strikeBottom: () => this.strikeBottom_,
212
+ strikeBranches: () => this.strikeBranches_,
213
+ strikeGlow: () => this.strikeGlow_,
214
+ above: () => this.above_,
215
+ },
216
+ });
217
+ // no pass, and no frames, without an intensity
218
+ stage.enabled = this.intensity_ > 0;
219
+ return stage;
220
+ }
221
+
222
+ /**
223
+ * @override
224
+ * @param {import('@cesium/engine').Scene} scene
225
+ */
226
+ activated_(scene) {
227
+ this.lastTime_ = performance.now() / 1000;
228
+ scene.preRender.addEventListener(this.onPreRender_);
229
+ acquireTerrainDepth(scene);
230
+ if (this.intensity_ > 0) {
231
+ acquireFrames(scene, FRAME_RATE);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * @override
237
+ * @param {import('@cesium/engine').Scene} scene
238
+ */
239
+ deactivating_(scene) {
240
+ if (this.intensity_ > 0) {
241
+ releaseFrames(scene, FRAME_RATE);
242
+ }
243
+ releaseTerrainDepth(scene);
244
+ scene.preRender.removeEventListener(this.onPreRender_);
245
+ this.strikes_.length = 0;
246
+ for (const bottom of this.strikeBottom_) {
247
+ bottom.w = -1;
248
+ }
249
+ for (const glow of this.strikeGlow_) {
250
+ glow.w = 0;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * How active the storm is, 0 to 1: the strikes start at random, up to one
256
+ * every 2.5 seconds on average.
257
+ */
258
+ get intensity() {
259
+ return this.intensity_;
260
+ }
261
+
262
+ set intensity(value) {
263
+ const striking = this.intensity_ > 0;
264
+ this.intensity_ = value;
265
+ if (this.stage_ && striking !== value > 0) {
266
+ this.stage_.enabled = value > 0;
267
+ (value > 0 ? acquireFrames : releaseFrames)(this.viewer.scene, FRAME_RATE);
268
+ }
269
+ this.viewer.scene.requestRender();
270
+ }
271
+
272
+ /**
273
+ * Altitude of the cloud base, in meters above the ellipsoid: the channels
274
+ * start there, and none strike around a camera above it.
275
+ */
276
+ get cloudBase() {
277
+ return this.cloudBase_;
278
+ }
279
+
280
+ set cloudBase(value) {
281
+ this.cloudBase_ = value;
282
+ this.viewer.scene.requestRender();
283
+ }
284
+
285
+ /**
286
+ * Whether the strikes land in front of the camera, within 30 degrees of
287
+ * where it looks, rather than all around it.
288
+ */
289
+ get inFront() {
290
+ return this.inFront_;
291
+ }
292
+
293
+ set inFront(value) {
294
+ this.inFront_ = value;
295
+ }
296
+
297
+ /**
298
+ * How far from the camera the strikes land, in meters.
299
+ */
300
+ get radius() {
301
+ return this.radius_;
302
+ }
303
+
304
+ set radius(value) {
305
+ this.radius_ = value;
306
+ this.viewer.scene.requestRender();
307
+ }
308
+
309
+ /**
310
+ * Where it rains, or undefined to strike anywhere: an image whose red
311
+ * channel is the local intensity, as Precipitation's map. With a map, a
312
+ * strike lands only on heavy rain, more surely as it is heavier.
313
+ */
314
+ get map() {
315
+ return this.weatherMap_.map;
316
+ }
317
+
318
+ set map(value) {
319
+ this.weatherMap_.map = value;
320
+ }
321
+ }
package/motion-blur.js ADDED
@@ -0,0 +1,117 @@
1
+ import {Matrix4, PixelDatatype} from '@cesium/core';
2
+ import {PostProcessStage, PostProcessStageComposite} from '@cesium/engine';
3
+ import {acquireTerrainDepth, releaseTerrainDepth} from './depth-test.js';
4
+ import Effect from './effect.js';
5
+ import EyeFromDepth from './shaders/EyeFromDepth.js';
6
+ import Hash from './shaders/Hash.js';
7
+ import MotionBlurShader from './shaders/MotionBlur.js';
8
+ import MotionBlurVelocity from './shaders/MotionBlurVelocity.js';
9
+
10
+ /**
11
+ * Camera motion blur: each pixel is blurred along its motion on the screen
12
+ * since the previous frame, over the exposure time.
13
+ */
14
+ export default class MotionBlur extends Effect {
15
+ /**
16
+ * @param {import('@cesium/engine').CesiumWidget} viewer
17
+ * @param {{strength?: number, exposure?: number}} [options]
18
+ */
19
+ constructor(viewer, options = {}) {
20
+ super(viewer);
21
+ this.strength_ = options.strength ?? 1;
22
+ this.exposure_ = options.exposure ?? 1 / 24;
23
+ this.previousViewProjection_ = new Matrix4();
24
+ this.reprojection_ = new Matrix4();
25
+ this.previousTime_ = 0;
26
+ this.onPostRender_ = () => {
27
+ const camera = this.viewer.scene.camera;
28
+ Matrix4.multiply(camera.frustum.projectionMatrix, camera.viewMatrix, this.previousViewProjection_);
29
+ this.previousTime_ = performance.now();
30
+ };
31
+ }
32
+
33
+ /**
34
+ * @override
35
+ * @param {import('@cesium/engine').Scene} scene
36
+ */
37
+ createStage_(scene) {
38
+ // the velocity of each pixel is computed once, at half resolution, rather than once per
39
+ // sample of the gather; the distances need the range of floats
40
+ const velocity = new PostProcessStage({
41
+ name: 'czm_motion_blur_velocity',
42
+ fragmentShader: EyeFromDepth + MotionBlurVelocity,
43
+ uniforms: {
44
+ reprojection: () =>
45
+ Matrix4.multiply(this.previousViewProjection_, scene.camera.inverseViewMatrix, this.reprojection_),
46
+ // the blur spans the motion during the exposure, whatever the frame rate; after a pause
47
+ // in requestRenderMode, the time since the previous frame is not the time of the motion
48
+ exposureScale: () =>
49
+ (this.strength_ * this.exposure_) /
50
+ Math.min(Math.max((performance.now() - this.previousTime_) / 1000, 1 / 240), 1 / 10),
51
+ },
52
+ textureScale: 0.5,
53
+ pixelDatatype: PixelDatatype.FLOAT,
54
+ });
55
+ const stage = new PostProcessStageComposite({
56
+ stages: [
57
+ velocity,
58
+ new PostProcessStage({
59
+ fragmentShader: Hash + MotionBlurShader,
60
+ uniforms: {velocityTexture: velocity.name},
61
+ }),
62
+ ],
63
+ // the gather reads the scene, not the velocities
64
+ inputPreviousStageTexture: false,
65
+ });
66
+ // no pass at all without strength
67
+ stage.enabled = this.strength_ > 0;
68
+ return stage;
69
+ }
70
+
71
+ /**
72
+ * @override
73
+ * @param {import('@cesium/engine').Scene} scene
74
+ */
75
+ activated_(scene) {
76
+ acquireTerrainDepth(scene);
77
+ this.onPostRender_();
78
+ scene.postRender.addEventListener(this.onPostRender_);
79
+ }
80
+
81
+ /**
82
+ * @override
83
+ * @param {import('@cesium/engine').Scene} scene
84
+ */
85
+ deactivating_(scene) {
86
+ scene.postRender.removeEventListener(this.onPostRender_);
87
+ releaseTerrainDepth(scene);
88
+ }
89
+
90
+ /**
91
+ * Strength of the blur, 0 to 1: a fraction of the exposure.
92
+ */
93
+ get strength() {
94
+ return this.strength_;
95
+ }
96
+
97
+ set strength(value) {
98
+ this.strength_ = value;
99
+ if (this.stage_) {
100
+ this.stage_.enabled = value > 0;
101
+ }
102
+ this.viewer.scene.requestRender();
103
+ }
104
+
105
+ /**
106
+ * Exposure time, in seconds: 1/48 is the 180 degree shutter of film at 24
107
+ * frames per second, longer blurs more.
108
+ */
109
+ get exposure() {
110
+ return this.exposure_;
111
+ }
112
+
113
+ set exposure(value) {
114
+ this.exposure_ = value;
115
+ this.viewer.scene.requestRender();
116
+ }
117
+ }