@geoblocks/cesium-post-process 0.1.0 → 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.
- package/cloud-cover.js +49 -0
- package/cloud-map.js +122 -0
- package/cloud-noise.js +229 -0
- package/clouds.js +684 -0
- package/effect.js +64 -0
- package/package.json +8 -2
- package/weather-map.js +290 -0
package/effect.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A screen-space effect: a post-process stage on a scene while active.
|
|
3
|
+
* Subclasses create their stage in `createStage_` and take their side effects
|
|
4
|
+
* in `activated_` and `deactivating_`.
|
|
5
|
+
*/
|
|
6
|
+
export default class Effect {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('@cesium/engine').CesiumWidget} viewer
|
|
9
|
+
*/
|
|
10
|
+
constructor(viewer) {
|
|
11
|
+
this.viewer = viewer;
|
|
12
|
+
/** @type {import('@cesium/engine').PostProcessStage | import('@cesium/engine').PostProcessStageComposite | undefined} */
|
|
13
|
+
this.stage_ = undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get active() {
|
|
17
|
+
return this.stage_ !== undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set active(active) {
|
|
21
|
+
const scene = this.viewer.scene;
|
|
22
|
+
if (active) {
|
|
23
|
+
if (this.stage_) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.stage_ = this.createStage_(scene);
|
|
27
|
+
scene.postProcessStages.add(this.stage_);
|
|
28
|
+
this.activated_(scene);
|
|
29
|
+
} else {
|
|
30
|
+
if (!this.stage_) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.deactivating_(scene);
|
|
34
|
+
// removing a stage also destroys it
|
|
35
|
+
scene.postProcessStages.remove(this.stage_);
|
|
36
|
+
this.stage_ = undefined;
|
|
37
|
+
}
|
|
38
|
+
scene.requestRender();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('@cesium/engine').Scene} _scene
|
|
43
|
+
* @return {import('@cesium/engine').PostProcessStage | import('@cesium/engine').PostProcessStageComposite}
|
|
44
|
+
*/
|
|
45
|
+
createStage_(_scene) {
|
|
46
|
+
throw new Error(`${this.constructor.name} does not implement createStage_`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Called once the stage is added to the scene.
|
|
51
|
+
* @param {import('@cesium/engine').Scene} _scene
|
|
52
|
+
*/
|
|
53
|
+
activated_(_scene) {}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Called before the stage is removed from the scene.
|
|
57
|
+
* @param {import('@cesium/engine').Scene} _scene
|
|
58
|
+
*/
|
|
59
|
+
deactivating_(_scene) {}
|
|
60
|
+
|
|
61
|
+
destroy() {
|
|
62
|
+
this.active = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geoblocks/cesium-post-process",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
"analog-video.js",
|
|
16
16
|
"blur.js",
|
|
17
17
|
"cesium-post-process.js",
|
|
18
|
+
"cloud-cover.js",
|
|
19
|
+
"cloud-map.js",
|
|
20
|
+
"cloud-noise.js",
|
|
21
|
+
"clouds.js",
|
|
18
22
|
"color-isolation.js",
|
|
19
23
|
"depth-test.js",
|
|
20
24
|
"digital-video.js",
|
|
21
25
|
"drone-display.js",
|
|
26
|
+
"effect.js",
|
|
22
27
|
"focus.js",
|
|
23
28
|
"frame-clock.js",
|
|
24
29
|
"height.js",
|
|
@@ -37,6 +42,7 @@
|
|
|
37
42
|
"tilt-shift.js",
|
|
38
43
|
"toy.js",
|
|
39
44
|
"valley-fog.js",
|
|
45
|
+
"weather-map.js",
|
|
40
46
|
"white-model.js",
|
|
41
47
|
"shaders/*.js"
|
|
42
48
|
],
|
|
@@ -47,5 +53,5 @@
|
|
|
47
53
|
"@cesium/core": ">=0.1.0",
|
|
48
54
|
"@cesium/engine": ">=10"
|
|
49
55
|
},
|
|
50
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "95e74403b31593a8d770f787bff6ae41e4e19dc9"
|
|
51
57
|
}
|
package/weather-map.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {Cartesian4} from '@cesium/core';
|
|
2
|
+
// @ts-expect-error the engine exports its textures, but does not type them
|
|
3
|
+
import {Texture} from '@cesium/engine';
|
|
4
|
+
|
|
5
|
+
const TWO_PI = 2 * Math.PI;
|
|
6
|
+
// the max map's texels are blocks of this many pixels of the map across
|
|
7
|
+
const POOL = 4;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where it rains: an image whose red channel is the local intensity, 0 to 1,
|
|
11
|
+
* over a rectangle of longitudes and latitudes, north up, not crossing the
|
|
12
|
+
* antimeridian. Outside the rectangle there is none, so the image should
|
|
13
|
+
* fade to 0 at its edges.
|
|
14
|
+
* @typedef {{image: HTMLCanvasElement | ImageData | {width: number, height: number, data: Uint8ClampedArray}, rectangle: import('@cesium/core').Rectangle}} WeatherMapOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The pixels of an image, read once.
|
|
19
|
+
* @param {WeatherMapOptions['image']} image
|
|
20
|
+
*/
|
|
21
|
+
export function imageData(image) {
|
|
22
|
+
return 'data' in image ? image : image.getContext('2d', {willReadFrequently: true})?.getImageData(0, 0, image.width, image.height);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A texture's source from an image: a canvas or an ImageData as it is, other pixels as an array,
|
|
27
|
+
* which Cesium flips as it flips an image.
|
|
28
|
+
* @param {HTMLCanvasElement | ImageData | {width: number, height: number, data: Uint8ClampedArray}} image
|
|
29
|
+
*/
|
|
30
|
+
function textureSource(image) {
|
|
31
|
+
if (!('data' in image) || (typeof ImageData !== 'undefined' && image instanceof ImageData)) {
|
|
32
|
+
return image;
|
|
33
|
+
}
|
|
34
|
+
const {width, height, data} = image;
|
|
35
|
+
return {width, height, arrayBufferView: new Uint8Array(data.buffer, data.byteOffset, data.length)};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The highest value of each channel of an image, 0 to 1.
|
|
40
|
+
* @param {{data: Uint8ClampedArray} | undefined} data
|
|
41
|
+
*/
|
|
42
|
+
function highest(data) {
|
|
43
|
+
const max = [0, 0, 0, 0];
|
|
44
|
+
const values = data?.data ?? [];
|
|
45
|
+
for (let i = 0; i < values.length; i += 4) {
|
|
46
|
+
for (let channel = 0; channel < 4; channel++) {
|
|
47
|
+
max[channel] = Math.max(max[channel], values[i + channel]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return max.map((value) => value / 255);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The highest value of each channel of images of the same size around each block of POOL x POOL
|
|
55
|
+
* pixels: in the block and its 8 neighbors, as RGBA bytes. A ray sampled less than a block apart
|
|
56
|
+
* then finds every cell of rain it passes, however small.
|
|
57
|
+
* @param {Array<{width: number, height: number, data: Uint8ClampedArray} | undefined>} images
|
|
58
|
+
*/
|
|
59
|
+
function maxPool(images) {
|
|
60
|
+
const {width, height} = /** @type {{width: number, height: number}} */ (images[0]);
|
|
61
|
+
const w = Math.ceil(width / POOL);
|
|
62
|
+
const h = Math.ceil(height / POOL);
|
|
63
|
+
const pooled = new Uint8Array(w * h * 4);
|
|
64
|
+
for (const image of images) {
|
|
65
|
+
if (!image) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (let y = 0; y < height; y++) {
|
|
69
|
+
for (let x = 0; x < width; x++) {
|
|
70
|
+
const i = 4 * (Math.floor(y / POOL) * w + Math.floor(x / POOL));
|
|
71
|
+
for (let channel = 0; channel < 4; channel++) {
|
|
72
|
+
pooled[i + channel] = Math.max(pooled[i + channel], image.data[4 * (y * width + x) + channel]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const data = new Uint8Array(w * h * 4);
|
|
78
|
+
for (let y = 0; y < h; y++) {
|
|
79
|
+
for (let x = 0; x < w; x++) {
|
|
80
|
+
for (let channel = 0; channel < 4; channel++) {
|
|
81
|
+
let max = 0;
|
|
82
|
+
for (let dy = Math.max(y - 1, 0); dy <= Math.min(y + 1, h - 1); dy++) {
|
|
83
|
+
for (let dx = Math.max(x - 1, 0); dx <= Math.min(x + 1, w - 1); dx++) {
|
|
84
|
+
max = Math.max(max, pooled[4 * (dy * w + dx) + channel]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
data[4 * (y * w + x) + channel] = max;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {width: w, height: h, data};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A map of the rain for the shaders, WeatherMap.glsl's uniforms, and its
|
|
96
|
+
* intensity at a point on the CPU, bilinear as the shaders sample it. Without
|
|
97
|
+
* a map, the intensity everywhere is the empty value: 1 for rain everywhere,
|
|
98
|
+
* 0 for none. A next map, over the same rectangle and of the same size, crossfades from the map to
|
|
99
|
+
* it by the blend, as the radar from one hour to the next.
|
|
100
|
+
*/
|
|
101
|
+
export default class WeatherMap {
|
|
102
|
+
/**
|
|
103
|
+
* @param {number} empty the intensity everywhere without a map
|
|
104
|
+
*/
|
|
105
|
+
constructor(empty) {
|
|
106
|
+
this.empty_ = empty;
|
|
107
|
+
/** @type {WeatherMapOptions | undefined} */
|
|
108
|
+
this.map_ = undefined;
|
|
109
|
+
/** @type {{width: number, height: number, data: Uint8ClampedArray} | undefined} */
|
|
110
|
+
this.data_ = undefined;
|
|
111
|
+
/** @type {WeatherMapOptions | undefined} */
|
|
112
|
+
this.nextMap_ = undefined;
|
|
113
|
+
/** @type {{width: number, height: number, data: Uint8ClampedArray} | undefined} */
|
|
114
|
+
this.nextData_ = undefined;
|
|
115
|
+
/**
|
|
116
|
+
* From the map, 0, to the next one, 1.
|
|
117
|
+
*/
|
|
118
|
+
this.blend = 0;
|
|
119
|
+
/**
|
|
120
|
+
* The rectangle in the ellipsoid's texture coordinates: its west and
|
|
121
|
+
* south, and one over its width and height; the whole globe without a map.
|
|
122
|
+
*/
|
|
123
|
+
this.bounds = new Cartesian4(0, 0, 1, 1);
|
|
124
|
+
/**
|
|
125
|
+
* The highest intensity on the map, 0 to 1: the empty value without a map; and the highest
|
|
126
|
+
* value of each of its channels.
|
|
127
|
+
*/
|
|
128
|
+
this.maxIntensity = empty;
|
|
129
|
+
this.maxValues = [empty, empty, empty, empty];
|
|
130
|
+
// made when the shaders first need it: a stage given an image makes its texture a frame after
|
|
131
|
+
// it is set, and draws without one in between
|
|
132
|
+
/** @type {any} */
|
|
133
|
+
this.texture_ = undefined;
|
|
134
|
+
/** @type {any} */
|
|
135
|
+
this.nextTexture_ = undefined;
|
|
136
|
+
/** @type {{width: number, height: number, data: Uint8Array} | undefined} */
|
|
137
|
+
this.maxImage_ = undefined;
|
|
138
|
+
/** @type {any} */
|
|
139
|
+
this.maxTexture_ = undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get map() {
|
|
143
|
+
return this.map_;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set map(value) {
|
|
147
|
+
this.map_ = value;
|
|
148
|
+
if (!value) {
|
|
149
|
+
this.data_ = undefined;
|
|
150
|
+
Cartesian4.fromElements(0, 0, 1, 1, this.bounds);
|
|
151
|
+
this.maxIntensity = this.empty_;
|
|
152
|
+
} else {
|
|
153
|
+
const {image, rectangle} = value;
|
|
154
|
+
this.data_ = imageData(image);
|
|
155
|
+
Cartesian4.fromElements(
|
|
156
|
+
rectangle.west / TWO_PI + 0.5,
|
|
157
|
+
rectangle.south / Math.PI + 0.5,
|
|
158
|
+
TWO_PI / (rectangle.east - rectangle.west),
|
|
159
|
+
Math.PI / (rectangle.north - rectangle.south),
|
|
160
|
+
this.bounds,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
this.updateMaxIntensity_();
|
|
164
|
+
// made again from the new image when the shaders next need it
|
|
165
|
+
this.texture_ = this.texture_?.destroy();
|
|
166
|
+
this.clearMaxImage_();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get nextMap() {
|
|
170
|
+
return this.nextMap_;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
set nextMap(value) {
|
|
174
|
+
this.nextMap_ = value;
|
|
175
|
+
this.nextData_ = value && imageData(value.image);
|
|
176
|
+
this.updateMaxIntensity_();
|
|
177
|
+
this.nextTexture_ = this.nextTexture_?.destroy();
|
|
178
|
+
this.clearMaxImage_();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
clearMaxImage_() {
|
|
182
|
+
this.maxImage_ = undefined;
|
|
183
|
+
this.maxTexture_ = this.maxTexture_?.destroy();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The highest intensity around each block of the map's pixels, over the map and the next one,
|
|
188
|
+
* made when first needed: undefined without a map.
|
|
189
|
+
*/
|
|
190
|
+
maxImage() {
|
|
191
|
+
if (this.map_ && !this.maxImage_) {
|
|
192
|
+
this.maxImage_ = maxPool([this.data_, this.nextData_]);
|
|
193
|
+
}
|
|
194
|
+
return this.maxImage_;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
updateMaxIntensity_() {
|
|
198
|
+
const map = highest(this.data_);
|
|
199
|
+
const next = highest(this.nextMap_ ? this.nextData_ : undefined);
|
|
200
|
+
this.maxValues = this.map_ ? map.map((value, channel) => Math.max(value, next[channel])) : [this.empty_, this.empty_, this.empty_, this.empty_];
|
|
201
|
+
this.maxIntensity = this.maxValues[0];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The intensity at a point, 0 to 1, bilinear as the shaders sample it: the
|
|
206
|
+
* empty value without a map, 0 outside its rectangle.
|
|
207
|
+
* @param {number} longitude radians
|
|
208
|
+
* @param {number} latitude radians
|
|
209
|
+
*/
|
|
210
|
+
intensityAt(longitude, latitude) {
|
|
211
|
+
if (!this.map_ || !this.data_) {
|
|
212
|
+
return this.empty_;
|
|
213
|
+
}
|
|
214
|
+
const {west, south, east, north} = this.map_.rectangle;
|
|
215
|
+
const u = (longitude - west) / (east - west);
|
|
216
|
+
const v = (latitude - south) / (north - south);
|
|
217
|
+
if (u < 0 || u > 1 || v < 0 || v > 1) {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
const intensity = this.sample_(this.data_, u, v);
|
|
221
|
+
if (!this.nextData_) {
|
|
222
|
+
return intensity;
|
|
223
|
+
}
|
|
224
|
+
return intensity + (this.sample_(this.nextData_, u, v) - intensity) * this.blend;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* An image's intensity, bilinear, at a point of its rectangle, 0 to 1 from its west and south.
|
|
229
|
+
* @param {{width: number, height: number, data: Uint8ClampedArray}} image
|
|
230
|
+
* @param {number} u
|
|
231
|
+
* @param {number} v
|
|
232
|
+
*/
|
|
233
|
+
sample_(image, u, v) {
|
|
234
|
+
const {width, height, data} = image;
|
|
235
|
+
// the image's rows go down from the north
|
|
236
|
+
const x = Math.min(Math.max(u * width - 0.5, 0), width - 1);
|
|
237
|
+
const y = Math.min(Math.max((1 - v) * height - 0.5, 0), height - 1);
|
|
238
|
+
const x0 = Math.floor(x);
|
|
239
|
+
const y0 = Math.floor(y);
|
|
240
|
+
const x1 = Math.min(x0 + 1, width - 1);
|
|
241
|
+
const y1 = Math.min(y0 + 1, height - 1);
|
|
242
|
+
const red = (/** @type {number} */ column, /** @type {number} */ row) => data[4 * (row * width + column)] / 255;
|
|
243
|
+
const upper = red(x0, y0) + (red(x1, y0) - red(x0, y0)) * (x - x0);
|
|
244
|
+
const lower = red(x0, y1) + (red(x1, y1) - red(x0, y1)) * (x - x0);
|
|
245
|
+
return upper + (lower - upper) * (y - y0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The map's texture, a pixel of the empty value without a map.
|
|
250
|
+
* @param {any} context the scene's context
|
|
251
|
+
*/
|
|
252
|
+
texture(context) {
|
|
253
|
+
if (!this.texture_) {
|
|
254
|
+
const value = 255 * this.empty_;
|
|
255
|
+
this.texture_ = new Texture({context, source: textureSource(this.map_?.image ?? new ImageData(new Uint8ClampedArray([value, value, value, 255]), 1, 1))});
|
|
256
|
+
}
|
|
257
|
+
return this.texture_;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The next map's texture, the map's without one.
|
|
262
|
+
* @param {any} context the scene's context
|
|
263
|
+
*/
|
|
264
|
+
nextTexture(context) {
|
|
265
|
+
if (!this.nextMap_) {
|
|
266
|
+
return this.texture(context);
|
|
267
|
+
}
|
|
268
|
+
this.nextTexture_ ??= new Texture({context, source: textureSource(this.nextMap_.image)});
|
|
269
|
+
return this.nextTexture_;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The max map's texture, a pixel of the empty value without a map.
|
|
274
|
+
* @param {any} context the scene's context
|
|
275
|
+
*/
|
|
276
|
+
maxTexture(context) {
|
|
277
|
+
if (!this.maxTexture_) {
|
|
278
|
+
const value = 255 * this.empty_;
|
|
279
|
+
const {width, height, data} = this.maxImage() ?? {width: 1, height: 1, data: new Uint8Array([value, value, value, 255])};
|
|
280
|
+
this.maxTexture_ = new Texture({context, source: {width, height, arrayBufferView: data}});
|
|
281
|
+
}
|
|
282
|
+
return this.maxTexture_;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
destroy() {
|
|
286
|
+
this.texture_ = this.texture_?.destroy();
|
|
287
|
+
this.nextTexture_ = this.nextTexture_?.destroy();
|
|
288
|
+
this.maxTexture_ = this.maxTexture_?.destroy();
|
|
289
|
+
}
|
|
290
|
+
}
|