@geoblocks/cesium-post-process 0.0.0-stage → 0.1.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.
Files changed (83) 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/color-isolation.js +69 -0
  7. package/depth-test.js +37 -0
  8. package/digital-video.js +108 -0
  9. package/drone-display.js +170 -0
  10. package/focus.js +25 -0
  11. package/frame-clock.js +61 -0
  12. package/height.js +23 -0
  13. package/infrared.js +40 -0
  14. package/jello.js +146 -0
  15. package/lens-distortion.js +68 -0
  16. package/lightning.js +321 -0
  17. package/motion-blur.js +117 -0
  18. package/package.json +49 -4
  19. package/precipitation.js +493 -0
  20. package/shaders/Alpenglow.js +2 -0
  21. package/shaders/AnalogVideo.js +2 -0
  22. package/shaders/Beam.js +2 -0
  23. package/shaders/CloudsBlur.js +2 -0
  24. package/shaders/CloudsComposite.js +2 -0
  25. package/shaders/CloudsDensity.js +2 -0
  26. package/shaders/CloudsDepth.js +2 -0
  27. package/shaders/CloudsMarch.js +2 -0
  28. package/shaders/CloudsResolve.js +2 -0
  29. package/shaders/CloudsShadow.js +2 -0
  30. package/shaders/CloudsShadowMap.js +2 -0
  31. package/shaders/CloudsSlab.js +2 -0
  32. package/shaders/ColorIsolation.js +2 -0
  33. package/shaders/DepthEdges.js +2 -0
  34. package/shaders/DigitalVideo.js +2 -0
  35. package/shaders/DroneDisplay.js +2 -0
  36. package/shaders/EyeFromDepth.js +2 -0
  37. package/shaders/Filmic.js +2 -0
  38. package/shaders/Flashlight.js +2 -0
  39. package/shaders/Fog.js +2 -0
  40. package/shaders/Frame.js +2 -0
  41. package/shaders/Hash.js +2 -0
  42. package/shaders/Height.js +2 -0
  43. package/shaders/Hsl.js +2 -0
  44. package/shaders/Infrared.js +2 -0
  45. package/shaders/Jello.js +2 -0
  46. package/shaders/LensDistortion.js +2 -0
  47. package/shaders/Lightning.js +2 -0
  48. package/shaders/MotionBlur.js +2 -0
  49. package/shaders/MotionBlurVelocity.js +2 -0
  50. package/shaders/Noise.js +2 -0
  51. package/shaders/Normal.js +2 -0
  52. package/shaders/Phase.js +2 -0
  53. package/shaders/Precipitation.js +2 -0
  54. package/shaders/PrecipitationShaftBlur.js +2 -0
  55. package/shaders/PrecipitationShafts.js +2 -0
  56. package/shaders/PrecipitationWind.js +2 -0
  57. package/shaders/RainHaze.js +2 -0
  58. package/shaders/Shockwave.js +2 -0
  59. package/shaders/SkyTable.js +2 -0
  60. package/shaders/SnowLine.js +2 -0
  61. package/shaders/SpeedLines.js +2 -0
  62. package/shaders/Spotlight.js +2 -0
  63. package/shaders/SpotlightBeam.js +2 -0
  64. package/shaders/SpotlightBeamBlur.js +2 -0
  65. package/shaders/SpotlightCone.js +2 -0
  66. package/shaders/Super8.js +2 -0
  67. package/shaders/Technicolor.js +2 -0
  68. package/shaders/TiltShift.js +2 -0
  69. package/shaders/Toy.js +2 -0
  70. package/shaders/Trails.js +2 -0
  71. package/shaders/ValleyFog.js +2 -0
  72. package/shaders/WeatherMap.js +2 -0
  73. package/shaders/WhiteModel.js +2 -0
  74. package/snow-line.js +108 -0
  75. package/speed-lines.js +71 -0
  76. package/spotlight.js +193 -0
  77. package/strikes.js +30 -0
  78. package/super8.js +164 -0
  79. package/technicolor.js +56 -0
  80. package/tilt-shift.js +124 -0
  81. package/toy.js +192 -0
  82. package/valley-fog.js +105 -0
  83. package/white-model.js +105 -0
@@ -0,0 +1,108 @@
1
+ import {PostProcessStage} from '@cesium/engine';
2
+ import Effect from './effect.js';
3
+ import {acquireFrames, releaseFrames} from './frame-clock.js';
4
+ import DigitalVideoShader from './shaders/DigitalVideo.js';
5
+ import Hash from './shaders/Hash.js';
6
+
7
+ // digital FPV video runs at this rate
8
+ const FRAME_RATE = 30;
9
+ // at full breakup, a breakup starts on average every this many seconds
10
+ const MIN_INTERVAL = 2;
11
+ // length of a breakup, in seconds, and of its black screen
12
+ const DURATION = [0.4, 1.5];
13
+ const BLACKOUT = 0.25;
14
+
15
+ /**
16
+ * A digital FPV video feed: clean, then at times broken by a weak signal
17
+ * into macroblocks, displaced blocks and strips and split colors, sometimes
18
+ * with a short black screen. While active the scene renders at 30 frames per
19
+ * second, also in requestRenderMode.
20
+ */
21
+ export default class DigitalVideo extends Effect {
22
+ /**
23
+ * @param {import('@cesium/engine').CesiumWidget} viewer
24
+ * @param {{breakup?: number}} [options]
25
+ */
26
+ constructor(viewer, options = {}) {
27
+ super(viewer);
28
+ this.breakup_ = options.breakup ?? 0.5;
29
+ // the current breakup: start and length in seconds, and whether it goes black
30
+ /** @type {{start: number, duration: number, blackout: boolean} | undefined} */
31
+ this.episode_ = undefined;
32
+ // when the breakups were last updated, in seconds
33
+ this.updated_ = 0;
34
+ // the uniforms, updated once per frame
35
+ this.severity_ = 0;
36
+ this.blackout_ = 0;
37
+ this.onPreRender_ = () => this.update_();
38
+ }
39
+
40
+ /** @override */
41
+ createStage_() {
42
+ return new PostProcessStage({
43
+ fragmentShader: Hash + DigitalVideoShader,
44
+ uniforms: {
45
+ severity: () => this.severity_,
46
+ blackout: () => this.blackout_,
47
+ frame: () => Math.floor((performance.now() / 1000) * FRAME_RATE) % 100000,
48
+ },
49
+ });
50
+ }
51
+
52
+ /**
53
+ * @override
54
+ * @param {import('@cesium/engine').Scene} scene
55
+ */
56
+ activated_(scene) {
57
+ this.updated_ = performance.now() / 1000;
58
+ scene.preRender.addEventListener(this.onPreRender_);
59
+ acquireFrames(scene, FRAME_RATE);
60
+ }
61
+
62
+ /**
63
+ * @override
64
+ * @param {import('@cesium/engine').Scene} scene
65
+ */
66
+ deactivating_(scene) {
67
+ releaseFrames(scene, FRAME_RATE);
68
+ scene.preRender.removeEventListener(this.onPreRender_);
69
+ this.episode_ = undefined;
70
+ }
71
+
72
+ // before each render: end the finished breakup, start one with the chance of the time since the
73
+ // last update, so that breakups start at the same rate whatever the frame rate (at most a second,
74
+ // as a hidden page does not render), then how broken the picture is now: rising and falling over
75
+ // the breakup, black around its middle
76
+ update_() {
77
+ const now = performance.now() / 1000;
78
+ const elapsed = Math.min(now - this.updated_, 1);
79
+ this.updated_ = now;
80
+ if (this.episode_ && now > this.episode_.start + this.episode_.duration) {
81
+ this.episode_ = undefined;
82
+ }
83
+ if (!this.episode_ && Math.random() < 1 - Math.exp((-this.breakup_ / MIN_INTERVAL) * elapsed)) {
84
+ const duration = DURATION[0] + Math.random() * (DURATION[1] - DURATION[0]);
85
+ this.episode_ = {start: now, duration, blackout: Math.random() < 0.5 * this.breakup_};
86
+ }
87
+ const episode = this.episode_;
88
+ if (!episode) {
89
+ this.severity_ = 0;
90
+ this.blackout_ = 0;
91
+ return;
92
+ }
93
+ const t = (now - episode.start) / episode.duration;
94
+ this.severity_ = Math.max(0, Math.sin(Math.PI * Math.min(t, 1))) ** 0.7 * (0.4 + 0.6 * this.breakup_);
95
+ this.blackout_ = episode.blackout && Math.abs(t - 0.5) * episode.duration < BLACKOUT / 2 ? 1 : 0;
96
+ }
97
+
98
+ /**
99
+ * How often and how badly the signal breaks up, 0 to 1.
100
+ */
101
+ get breakup() {
102
+ return this.breakup_;
103
+ }
104
+
105
+ set breakup(value) {
106
+ this.breakup_ = value;
107
+ }
108
+ }
@@ -0,0 +1,170 @@
1
+ import {Cartesian3, Math as CesiumMath} from '@cesium/core';
2
+ import {PostProcessStage} from '@cesium/engine';
3
+ import Effect from './effect.js';
4
+ import DroneDisplayShader from './shaders/DroneDisplay.js';
5
+
6
+ // a 3S battery, full, empty, the seconds it takes to drain between them at the cruise throttle,
7
+ // and how far its voltage sags at full throttle
8
+ const FULL_VOLTS = 12.6;
9
+ const EMPTY_VOLTS = 10.5;
10
+ const DRAIN_SECONDS = 2400;
11
+ const SAG_VOLTS = 0.4;
12
+ // the throttle in percent at cruise, and more of it per m/s of climb
13
+ const CRUISE_THROTTLE = 70;
14
+ const CLIMB_THROTTLE = 5;
15
+ // seconds for the speeds to get two thirds of the way to the camera's; after a pause longer than
16
+ // the gap, as in requestRenderMode, the motion is not measured across it
17
+ const SMOOTHING = 0.5;
18
+ const GAP = 0.5;
19
+
20
+ /**
21
+ * A fixed-wing plane's OSD as ArduPilot draws it on an HD video system, in the
22
+ * blocky white glyphs of an analog chip with some in color: an artificial
23
+ * horizon of dots following the camera's pitch and roll as ArduPilot's does,
24
+ * the flight mode, the heading, the altitude since the display turned on, the
25
+ * vertical speed and the air speed of the camera, the throttle following its
26
+ * climb, the battery voltage draining with the throttle, and a made-up link
27
+ * quality; and in failsafe while the control link is lost, the RTL mode and the
28
+ * FAILSAFE warning.
29
+ */
30
+ export default class DroneDisplay extends Effect {
31
+ /**
32
+ * @param {import('@cesium/engine').CesiumWidget} viewer
33
+ * @param {{opacity?: number}} [options]
34
+ */
35
+ constructor(viewer, options = {}) {
36
+ super(viewer);
37
+ this.opacity_ = options.opacity ?? 1;
38
+ this.rxLoss_ = false;
39
+ // when the display turned on, and the camera's height then
40
+ this.startTime_ = 0;
41
+ this.startHeight_ = 0;
42
+ // the camera's speed and vertical speed in m/s, smoothed, from its position and height at the
43
+ // previous frame
44
+ this.speed_ = 0;
45
+ this.verticalSpeed_ = 0;
46
+ this.previousPosition_ = new Cartesian3();
47
+ this.previousHeight_ = 0;
48
+ this.previousTime_ = 0;
49
+ // the share of the battery drained
50
+ this.drained_ = 0;
51
+ this.onPostRender_ = () => this.measure_(false);
52
+ }
53
+
54
+ /**
55
+ * @override
56
+ * @param {import('@cesium/engine').Scene} scene
57
+ */
58
+ createStage_(scene) {
59
+ return new PostProcessStage({
60
+ fragmentShader: DroneDisplayShader,
61
+ uniforms: {
62
+ opacity: () => this.opacity_,
63
+ pitch: () => CesiumMath.toDegrees(scene.camera.pitch),
64
+ roll: () => CesiumMath.toDegrees(CesiumMath.negativePiToPi(scene.camera.roll)),
65
+ heading: () => CesiumMath.toDegrees(scene.camera.heading),
66
+ altitude: () => scene.camera.positionCartographic.height - this.startHeight_,
67
+ verticalSpeed: () => this.verticalSpeed_,
68
+ speed: () => this.speed_ * 3.6,
69
+ voltage: () => this.voltage_(),
70
+ linkQuality: () => this.linkQuality_(),
71
+ throttle: () => this.throttle_(),
72
+ failsafe: () => (this.rxLoss_ ? 1 : 0),
73
+ // twice per second, as ArduPilot's blinking
74
+ blink: () => (Math.floor(performance.now() / 250) % 2 === 0 ? 1 : 0),
75
+ },
76
+ });
77
+ }
78
+
79
+ /**
80
+ * @override
81
+ * @param {import('@cesium/engine').Scene} scene
82
+ */
83
+ activated_(scene) {
84
+ this.startTime_ = performance.now() / 1000;
85
+ this.startHeight_ = scene.camera.positionCartographic.height;
86
+ this.speed_ = 0;
87
+ this.verticalSpeed_ = 0;
88
+ this.drained_ = 0;
89
+ this.measure_(true);
90
+ scene.postRender.addEventListener(this.onPostRender_);
91
+ }
92
+
93
+ /**
94
+ * @override
95
+ * @param {import('@cesium/engine').Scene} scene
96
+ */
97
+ deactivating_(scene) {
98
+ scene.postRender.removeEventListener(this.onPostRender_);
99
+ }
100
+
101
+ /**
102
+ * Eases the speeds toward the camera's motion since the previous frame, and
103
+ * drains the battery at the throttle.
104
+ * @param {boolean} first whether there is no previous frame
105
+ */
106
+ measure_(first) {
107
+ const camera = this.viewer.scene.camera;
108
+ const time = performance.now() / 1000;
109
+ const height = camera.positionCartographic.height;
110
+ const dt = time - this.previousTime_;
111
+ if (!first && dt > 0 && dt < GAP) {
112
+ const ease = 1 - Math.exp(-dt / SMOOTHING);
113
+ this.speed_ += (Cartesian3.distance(camera.positionWC, this.previousPosition_) / dt - this.speed_) * ease;
114
+ this.verticalSpeed_ += ((height - this.previousHeight_) / dt - this.verticalSpeed_) * ease;
115
+ }
116
+ if (!first) {
117
+ this.drained_ += (dt / DRAIN_SECONDS) * (this.throttle_() / CRUISE_THROTTLE);
118
+ }
119
+ Cartesian3.clone(camera.positionWC, this.previousPosition_);
120
+ this.previousHeight_ = height;
121
+ this.previousTime_ = time;
122
+ }
123
+
124
+ // the battery drains over the flight, and sags under the throttle's load
125
+ voltage_() {
126
+ const drained = (FULL_VOLTS - EMPTY_VOLTS) * Math.min(this.drained_, 1);
127
+ return FULL_VOLTS - drained - (SAG_VOLTS * this.throttle_()) / 100;
128
+ }
129
+
130
+ // the link hovers in the nineties, with a deep dip once in a while
131
+ linkQuality_() {
132
+ const seconds = performance.now() / 1000 - this.startTime_;
133
+ const wander = 4 * Math.abs(Math.sin(seconds * 0.9) * Math.sin(seconds * 0.37));
134
+ const dip = Math.sin(seconds * 0.11) > 0.97 ? 35 : 0;
135
+ return Math.round(99 - wander - dip);
136
+ }
137
+
138
+ // the throttle holds the cruise, more of it climbing and less diving, wandering a little
139
+ throttle_() {
140
+ const seconds = performance.now() / 1000 - this.startTime_;
141
+ const wander = 2 * Math.sin(seconds * 0.3) * Math.sin(seconds * 0.13 + 1);
142
+ return Math.round(Math.min(Math.max(CRUISE_THROTTLE + CLIMB_THROTTLE * this.verticalSpeed_ + wander, 0), 100));
143
+ }
144
+
145
+ /**
146
+ * Opacity of the display, 0 to 1.
147
+ */
148
+ get opacity() {
149
+ return this.opacity_;
150
+ }
151
+
152
+ set opacity(value) {
153
+ this.opacity_ = value;
154
+ this.viewer.scene.requestRender();
155
+ }
156
+
157
+ /**
158
+ * Whether the control link is lost: the mode turns to RTL, the link bar to
159
+ * red, and the FAILSAFE warning blinks below the middle. It blinks only while
160
+ * the scene renders.
161
+ */
162
+ get rxLoss() {
163
+ return this.rxLoss_;
164
+ }
165
+
166
+ set rxLoss(value) {
167
+ this.rxLoss_ = value;
168
+ this.viewer.scene.requestRender();
169
+ }
170
+ }
package/focus.js ADDED
@@ -0,0 +1,25 @@
1
+ import {Cartesian3, Cartesian4, Matrix4} from '@cesium/core';
2
+
3
+ /**
4
+ * What an effect focuses on: a position, a function called every frame for a moving target, or
5
+ * undefined for what is in the middle of the screen.
6
+ * @typedef {Cartesian3 | (() => Cartesian3 | undefined) | undefined} Focus
7
+ */
8
+
9
+ const eyeScratch = new Cartesian3();
10
+
11
+ /**
12
+ * The focus in eye coordinates, computed in double precision; w is 0 for the middle of the
13
+ * screen, which the shaders read from the depth texture.
14
+ * @param {import('@cesium/engine').Scene} scene
15
+ * @param {Focus} focus
16
+ * @param {Cartesian4} result
17
+ */
18
+ export function eyeFocus(scene, focus, result) {
19
+ const position = typeof focus === 'function' ? focus() : focus;
20
+ if (!position) {
21
+ return Cartesian4.fromElements(0, 0, 0, 0, result);
22
+ }
23
+ const eye = Matrix4.multiplyByPoint(scene.camera.viewMatrix, position, eyeScratch);
24
+ return Cartesian4.fromElements(eye.x, eye.y, eye.z, 1, result);
25
+ }
package/frame-clock.js ADDED
@@ -0,0 +1,61 @@
1
+ // Animated effects need the scene to render at their frame rate, also in requestRenderMode. With a
2
+ // timer each, their render requests would not line up and would add up: one timer per scene
3
+ // requests renders at the highest rate the active effects need.
4
+
5
+ /** @type {WeakMap<import('@cesium/engine').Scene, {rates: number[], interval: ReturnType<typeof setInterval> | undefined}>} */
6
+ const clocks = new WeakMap();
7
+
8
+ /**
9
+ * @param {import('@cesium/engine').Scene} scene
10
+ * @param {{rates: number[], interval: ReturnType<typeof setInterval> | undefined}} clock
11
+ */
12
+ function restart(scene, clock) {
13
+ clearInterval(clock.interval);
14
+ clock.interval = clock.rates.length > 0 ? setInterval(() => tick(scene, clock), 1000 / Math.max(...clock.rates)) : undefined;
15
+ }
16
+
17
+ /**
18
+ * A render request; once the scene is destroyed, with effects left active, the clock stops: a
19
+ * destroyed scene throws on every call.
20
+ * @param {import('@cesium/engine').Scene} scene
21
+ * @param {{rates: number[], interval: ReturnType<typeof setInterval> | undefined}} clock
22
+ */
23
+ function tick(scene, clock) {
24
+ if (scene.isDestroyed()) {
25
+ clearInterval(clock.interval);
26
+ clocks.delete(scene);
27
+ return;
28
+ }
29
+ scene.requestRender();
30
+ }
31
+
32
+ /**
33
+ * @param {import('@cesium/engine').Scene} scene
34
+ * @param {number} rate frames per second
35
+ */
36
+ export function acquireFrames(scene, rate) {
37
+ let clock = clocks.get(scene);
38
+ if (!clock) {
39
+ clock = {rates: [], interval: undefined};
40
+ clocks.set(scene, clock);
41
+ }
42
+ clock.rates.push(rate);
43
+ restart(scene, clock);
44
+ }
45
+
46
+ /**
47
+ * @param {import('@cesium/engine').Scene} scene
48
+ * @param {number} rate the rate given to acquireFrames
49
+ */
50
+ export function releaseFrames(scene, rate) {
51
+ const clock = clocks.get(scene);
52
+ const index = clock ? clock.rates.indexOf(rate) : -1;
53
+ if (!clock || index < 0) {
54
+ return;
55
+ }
56
+ clock.rates.splice(index, 1);
57
+ restart(scene, clock);
58
+ if (clock.rates.length === 0) {
59
+ clocks.delete(scene);
60
+ }
61
+ }
package/height.js ADDED
@@ -0,0 +1,23 @@
1
+ import {Cartesian3, Matrix4} from '@cesium/core';
2
+
3
+ const upScratch = new Cartesian3();
4
+ const surfaceScratch = new Cartesian3();
5
+
6
+ /**
7
+ * The uniforms of Height.glsl: the camera's height, the local up direction in
8
+ * eye coordinates and the Earth's radius below the camera, in double precision.
9
+ * @param {import('@cesium/engine').Scene} scene
10
+ */
11
+ export function heightUniforms(scene) {
12
+ return {
13
+ cameraHeight: () => scene.camera.positionCartographic.height,
14
+ up: () => {
15
+ const up = scene.ellipsoid.geodeticSurfaceNormal(scene.camera.positionWC, upScratch);
16
+ return Matrix4.multiplyByPointAsVector(scene.camera.viewMatrix, up, up);
17
+ },
18
+ radius: () => {
19
+ const surface = scene.ellipsoid.scaleToGeodeticSurface(scene.camera.positionWC, surfaceScratch);
20
+ return surface ? Cartesian3.magnitude(surface) : scene.ellipsoid.maximumRadius;
21
+ },
22
+ };
23
+ }
package/infrared.js ADDED
@@ -0,0 +1,40 @@
1
+ import {PostProcessStage} from '@cesium/engine';
2
+ import Effect from './effect.js';
3
+ import Hsl from './shaders/Hsl.js';
4
+ import InfraredShader from './shaders/Infrared.js';
5
+
6
+ /**
7
+ * Black and white infrared film: bright foliage, dark sky and water.
8
+ */
9
+ export default class Infrared extends Effect {
10
+ /**
11
+ * @param {import('@cesium/engine').CesiumWidget} viewer
12
+ * @param {{strength?: number}} [options]
13
+ */
14
+ constructor(viewer, options = {}) {
15
+ super(viewer);
16
+ this.strength_ = options.strength ?? 1;
17
+ }
18
+
19
+ /** @override */
20
+ createStage_() {
21
+ return new PostProcessStage({
22
+ fragmentShader: Hsl + InfraredShader,
23
+ uniforms: {
24
+ strength: () => this.strength_,
25
+ },
26
+ });
27
+ }
28
+
29
+ /**
30
+ * How far from the original colors, 0 to 1.
31
+ */
32
+ get strength() {
33
+ return this.strength_;
34
+ }
35
+
36
+ set strength(value) {
37
+ this.strength_ = value;
38
+ this.viewer.scene.requestRender();
39
+ }
40
+ }
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
+ }