@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/cloud-cover.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Seen from above, Clouds draws the clouds over the rain under them, which Precipitation then
|
|
2
|
+
// leaves to them: the scenes where clouds are drawn, by the active Clouds, with their shadow maps,
|
|
3
|
+
// through which Precipitation lights its haze.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A shadow map of the clouds: its texture, undefined until it is drawn, the ground point it is
|
|
7
|
+
* centered on, its side, and the base and the top of the slab it covers.
|
|
8
|
+
* @typedef {{texture: () => any, center: import('@cesium/core').Cartesian3, extent: () => number, base: () => number, top: () => number}} CloudShadow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @type {WeakMap<import('@cesium/engine').Scene, CloudShadow[]>} */
|
|
12
|
+
const covers = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
16
|
+
* @param {CloudShadow} shadow
|
|
17
|
+
*/
|
|
18
|
+
export function acquireCloudCover(scene, shadow) {
|
|
19
|
+
covers.set(scene, [...(covers.get(scene) ?? []), shadow]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
24
|
+
* @param {CloudShadow} shadow
|
|
25
|
+
*/
|
|
26
|
+
export function releaseCloudCover(scene, shadow) {
|
|
27
|
+
const shadows = (covers.get(scene) ?? []).filter((other) => other !== shadow);
|
|
28
|
+
if (shadows.length > 0) {
|
|
29
|
+
covers.set(scene, shadows);
|
|
30
|
+
} else {
|
|
31
|
+
covers.delete(scene);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether clouds are drawn in the scene.
|
|
37
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
38
|
+
*/
|
|
39
|
+
export function hasCloudCover(scene) {
|
|
40
|
+
return covers.has(scene);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The shadow map of the clouds drawn in the scene, the last activated's, or undefined without.
|
|
45
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
46
|
+
*/
|
|
47
|
+
export function cloudShadow(scene) {
|
|
48
|
+
return covers.get(scene)?.at(-1);
|
|
49
|
+
}
|
package/cloud-map.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {imageData} from './weather-map.js';
|
|
2
|
+
|
|
3
|
+
// The clouds' map, from a map of the rain: a radar sees the rain, "not the small cloud droplets and
|
|
4
|
+
// ice crystals that make up the rest of the cloud" (UBC ATSC 201), so the clouds are derived from
|
|
5
|
+
// it rather than drawn where it rains. Red is the rain, as it is; green is the coverage, the rain
|
|
6
|
+
// spread into a cloud shield wider than it; blue is the type of the clouds, from a flat deck over
|
|
7
|
+
// drizzle to towers over heavy rain, the precipitation turning the map "to cumulonimbus clouds"
|
|
8
|
+
// (Schneider 2015).
|
|
9
|
+
|
|
10
|
+
// the cloud shield reaches this far beyond the rain, in meters, its edge soft over about as much: 5
|
|
11
|
+
// to 15 km wide
|
|
12
|
+
const SHIELD_SPREAD = 5000;
|
|
13
|
+
// the rain rate, in mm/h, at intensity 1, the map's intensity being sqrt(R / RAIN_RATE), as
|
|
14
|
+
// RainHaze.glsl's
|
|
15
|
+
const RAIN_RATE = 10;
|
|
16
|
+
// the deck at this rain rate and below, in mm/h, towers at this one and above, about 40 dBZ (UBC
|
|
17
|
+
// ATSC 201): the type is remap(log(R), log(0.5), log(10), 0, 1)
|
|
18
|
+
const DECK_RATE = 0.5;
|
|
19
|
+
const TOWER_RATE = 10;
|
|
20
|
+
// meters, for the size of the map's pixels
|
|
21
|
+
const EARTH_RADIUS = 6371000;
|
|
22
|
+
// a Gaussian is approximated by this many box blurs, each a running sum whatever its width
|
|
23
|
+
const BOXES = 3;
|
|
24
|
+
|
|
25
|
+
/** @type {WeakMap<object, import('./weather-map.js').WeatherMapOptions>} */
|
|
26
|
+
const cloudMaps = new WeakMap();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Blurs values in place along rows (step 1) or columns (step width) by a box of a radius, a running
|
|
30
|
+
* sum, the edges held.
|
|
31
|
+
* @param {Float32Array} values
|
|
32
|
+
* @param {number} width
|
|
33
|
+
* @param {number} height
|
|
34
|
+
* @param {number} radius
|
|
35
|
+
* @param {boolean} rows
|
|
36
|
+
*/
|
|
37
|
+
function boxBlur(values, width, height, radius, rows) {
|
|
38
|
+
const [count, length, step, stride] = rows ? [height, width, 1, width] : [width, height, width, 1];
|
|
39
|
+
const line = new Float32Array(length);
|
|
40
|
+
for (let i = 0; i < count; i++) {
|
|
41
|
+
const start = i * stride;
|
|
42
|
+
for (let j = 0; j < length; j++) {
|
|
43
|
+
line[j] = values[start + j * step];
|
|
44
|
+
}
|
|
45
|
+
let sum = 0;
|
|
46
|
+
for (let j = -radius; j <= radius; j++) {
|
|
47
|
+
sum += line[Math.min(Math.max(j, 0), length - 1)];
|
|
48
|
+
}
|
|
49
|
+
for (let j = 0; j < length; j++) {
|
|
50
|
+
values[start + j * step] = sum / (2 * radius + 1);
|
|
51
|
+
sum += line[Math.min(j + radius + 1, length - 1)] - line[Math.max(j - radius, 0)];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dilates values in place along rows or columns: the highest within a radius.
|
|
58
|
+
* @param {Float32Array} values
|
|
59
|
+
* @param {number} width
|
|
60
|
+
* @param {number} height
|
|
61
|
+
* @param {number} radius
|
|
62
|
+
* @param {boolean} rows
|
|
63
|
+
*/
|
|
64
|
+
function dilate(values, width, height, radius, rows) {
|
|
65
|
+
const [count, length, step, stride] = rows ? [height, width, 1, width] : [width, height, width, 1];
|
|
66
|
+
const line = new Float32Array(length);
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
const start = i * stride;
|
|
69
|
+
for (let j = 0; j < length; j++) {
|
|
70
|
+
line[j] = values[start + j * step];
|
|
71
|
+
}
|
|
72
|
+
for (let j = 0; j < length; j++) {
|
|
73
|
+
let max = 0;
|
|
74
|
+
for (let k = Math.max(j - radius, 0); k <= Math.min(j + radius, length - 1); k++) {
|
|
75
|
+
max = Math.max(max, line[k]);
|
|
76
|
+
}
|
|
77
|
+
values[start + j * step] = max;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The clouds' map of a map of the rain, over the same rectangle, made once for each image.
|
|
84
|
+
* @param {import('./weather-map.js').WeatherMapOptions} map
|
|
85
|
+
* @returns {import('./weather-map.js').WeatherMapOptions}
|
|
86
|
+
*/
|
|
87
|
+
export default function createCloudMap(map) {
|
|
88
|
+
const known = cloudMaps.get(map.image);
|
|
89
|
+
if (known) {
|
|
90
|
+
return known;
|
|
91
|
+
}
|
|
92
|
+
const {width, height, data} = /** @type {{width: number, height: number, data: Uint8ClampedArray}} */ (imageData(map.image));
|
|
93
|
+
const {west, south, east, north} = map.rectangle;
|
|
94
|
+
const pixel = 0.5 * EARTH_RADIUS * (((east - west) * Math.cos(0.5 * (south + north))) / width + (north - south) / height);
|
|
95
|
+
// the rain dilated by the spread, in pixels, then softened by a Gaussian of half of it, the
|
|
96
|
+
// radius of each box for its standard deviation
|
|
97
|
+
const spread = Math.max(Math.round(SHIELD_SPREAD / pixel), 1);
|
|
98
|
+
const sigma = 0.5 * spread;
|
|
99
|
+
const radius = Math.max(Math.round(0.5 * (Math.sqrt((12 * sigma * sigma) / BOXES + 1) - 1)), 1);
|
|
100
|
+
const shield = new Float32Array(width * height);
|
|
101
|
+
for (let i = 0; i < width * height; i++) {
|
|
102
|
+
shield[i] = data[4 * i] > 0 ? 1 : 0;
|
|
103
|
+
}
|
|
104
|
+
dilate(shield, width, height, spread, true);
|
|
105
|
+
dilate(shield, width, height, spread, false);
|
|
106
|
+
for (let box = 0; box < BOXES; box++) {
|
|
107
|
+
boxBlur(shield, width, height, radius, true);
|
|
108
|
+
boxBlur(shield, width, height, radius, false);
|
|
109
|
+
}
|
|
110
|
+
const cloud = new Uint8ClampedArray(width * height * 4);
|
|
111
|
+
for (let i = 0; i < width * height; i++) {
|
|
112
|
+
const intensity = data[4 * i] / 255;
|
|
113
|
+
const rate = RAIN_RATE * intensity * intensity;
|
|
114
|
+
cloud[4 * i] = data[4 * i];
|
|
115
|
+
cloud[4 * i + 1] = Math.round(255 * shield[i]);
|
|
116
|
+
cloud[4 * i + 2] = rate > DECK_RATE ? Math.round((255 * Math.log(rate / DECK_RATE)) / Math.log(TOWER_RATE / DECK_RATE)) : 0;
|
|
117
|
+
cloud[4 * i + 3] = 255;
|
|
118
|
+
}
|
|
119
|
+
const cloudMap = {image: {width, height, data: cloud}, rectangle: map.rectangle};
|
|
120
|
+
cloudMaps.set(map.image, cloudMap);
|
|
121
|
+
return cloudMap;
|
|
122
|
+
}
|
package/cloud-noise.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// The clouds' noise, made once on the CPU: a cube of voxels that tiles in every direction, for
|
|
2
|
+
// CloudsMarch.glsl to read instead of computing noise for each sample. Red is the base shape,
|
|
3
|
+
// Perlin-Worley noise: billows of inverted Worley noise, broken up by gradient noise, as in
|
|
4
|
+
// Schneider's "The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn" (2015); green and blue
|
|
5
|
+
// are inverted Worley noise at twice and four times its frequency, the details that erode the
|
|
6
|
+
// edges. And a square of texels that tiles over the clouds' map, for CloudsDensity.glsl to read
|
|
7
|
+
// once rather than compute noise at each sample: red and green jitter the map's lookup, blue varies
|
|
8
|
+
// its coverage. Each channel is stretched over the bytes' range.
|
|
9
|
+
|
|
10
|
+
// cells of the base shape across the cube
|
|
11
|
+
const BASE_CELLS = 4;
|
|
12
|
+
// the map noise's square spans this many cells of the map, its jitter varying over this many of
|
|
13
|
+
// them, its coverage over this many: whole periods across the square, for it to tile
|
|
14
|
+
const MAP_TILE_CELLS = 64;
|
|
15
|
+
const JITTER_CELLS = 2;
|
|
16
|
+
const COVERAGE_CELLS = 8;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A hash of a lattice point, 0 to 1.
|
|
20
|
+
* @param {number} x
|
|
21
|
+
* @param {number} y
|
|
22
|
+
* @param {number} z
|
|
23
|
+
* @param {number} seed
|
|
24
|
+
*/
|
|
25
|
+
function hash(x, y, z, seed) {
|
|
26
|
+
let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 1440662683) ^ Math.imul(seed, 2246822519);
|
|
27
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
28
|
+
h ^= h >>> 16;
|
|
29
|
+
return (h >>> 0) / 4294967296;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {number} a
|
|
34
|
+
* @param {number} n
|
|
35
|
+
*/
|
|
36
|
+
function wrap(a, n) {
|
|
37
|
+
return ((a % n) + n) % n;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gradient noise that repeats every `period` cells, about -1 to 1, with the random directions of
|
|
42
|
+
* its lattice points computed once.
|
|
43
|
+
* @param {number} period
|
|
44
|
+
* @param {number} seed
|
|
45
|
+
*/
|
|
46
|
+
function perlin(period, seed) {
|
|
47
|
+
const gradients = new Float32Array(period ** 3 * 3);
|
|
48
|
+
for (let c = 0; c < period ** 3; c++) {
|
|
49
|
+
const cx = c % period;
|
|
50
|
+
const cy = Math.floor(c / period) % period;
|
|
51
|
+
const cz = Math.floor(c / period ** 2);
|
|
52
|
+
const theta = 2 * Math.PI * hash(cx, cy, cz, seed);
|
|
53
|
+
const cosPhi = 2 * hash(cx, cy, cz, seed + 1) - 1;
|
|
54
|
+
const sinPhi = Math.sqrt(1 - cosPhi * cosPhi);
|
|
55
|
+
gradients.set([sinPhi * Math.cos(theta), sinPhi * Math.sin(theta), cosPhi], 3 * c);
|
|
56
|
+
}
|
|
57
|
+
const fade = (/** @type {number} */ t) => t * t * t * (t * (t * 6 - 15) + 10);
|
|
58
|
+
// the corners of a voxel's cell are within one cell of it, so their wrapped index is a lookup
|
|
59
|
+
const wrapped = Int32Array.from({length: period + 1}, (_, i) => wrap(i, period));
|
|
60
|
+
/**
|
|
61
|
+
* @param {number} x in cells
|
|
62
|
+
* @param {number} y
|
|
63
|
+
* @param {number} z
|
|
64
|
+
*/
|
|
65
|
+
return (x, y, z) => {
|
|
66
|
+
const ix = Math.floor(x);
|
|
67
|
+
const iy = Math.floor(y);
|
|
68
|
+
const iz = Math.floor(z);
|
|
69
|
+
const fx = x - ix;
|
|
70
|
+
const fy = y - iy;
|
|
71
|
+
const fz = z - iz;
|
|
72
|
+
const x0 = wrapped[ix];
|
|
73
|
+
const x1 = wrapped[ix + 1];
|
|
74
|
+
const y0 = period * wrapped[iy];
|
|
75
|
+
const y1 = period * wrapped[iy + 1];
|
|
76
|
+
const z0 = period * period * wrapped[iz];
|
|
77
|
+
const z1 = period * period * wrapped[iz + 1];
|
|
78
|
+
const dot = (/** @type {number} */ c, /** @type {number} */ dx, /** @type {number} */ dy, /** @type {number} */ dz) =>
|
|
79
|
+
gradients[3 * c] * dx + gradients[3 * c + 1] * dy + gradients[3 * c + 2] * dz;
|
|
80
|
+
const u = fade(fx);
|
|
81
|
+
const v = fade(fy);
|
|
82
|
+
const w = fade(fz);
|
|
83
|
+
const a = dot(x0 + y0 + z0, fx, fy, fz);
|
|
84
|
+
const b = dot(x1 + y0 + z0, fx - 1, fy, fz);
|
|
85
|
+
const c = dot(x0 + y1 + z0, fx, fy - 1, fz);
|
|
86
|
+
const d = dot(x1 + y1 + z0, fx - 1, fy - 1, fz);
|
|
87
|
+
const e = dot(x0 + y0 + z1, fx, fy, fz - 1);
|
|
88
|
+
const f = dot(x1 + y0 + z1, fx - 1, fy, fz - 1);
|
|
89
|
+
const g = dot(x0 + y1 + z1, fx, fy - 1, fz - 1);
|
|
90
|
+
const h = dot(x1 + y1 + z1, fx - 1, fy - 1, fz - 1);
|
|
91
|
+
const lower = a + (b - a) * u + (c + (d - c) * u - a - (b - a) * u) * v;
|
|
92
|
+
const upper = e + (f - e) * u + (g + (h - g) * u - e - (f - e) * u) * v;
|
|
93
|
+
return 2 * (lower + (upper - lower) * w);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The distance to the nearest of one random point in each of `cells` cells across, repeating
|
|
99
|
+
* every `cells`, 0 to about 1, in cells, with the points computed once.
|
|
100
|
+
* @param {number} cells
|
|
101
|
+
* @param {number} seed
|
|
102
|
+
*/
|
|
103
|
+
function worley(cells, seed) {
|
|
104
|
+
const points = new Float32Array(cells ** 3 * 3);
|
|
105
|
+
for (let c = 0; c < cells ** 3; c++) {
|
|
106
|
+
const cx = c % cells;
|
|
107
|
+
const cy = Math.floor(c / cells) % cells;
|
|
108
|
+
const cz = Math.floor(c / cells ** 2);
|
|
109
|
+
points.set([hash(cx, cy, cz, seed), hash(cx, cy, cz, seed + 1), hash(cx, cy, cz, seed + 2)], 3 * c);
|
|
110
|
+
}
|
|
111
|
+
// the cells around a voxel are within one of its cell, so their wrapped index is a lookup
|
|
112
|
+
const wrapped = Int32Array.from({length: cells + 2}, (_, i) => wrap(i - 1, cells));
|
|
113
|
+
/**
|
|
114
|
+
* @param {number} x in cells
|
|
115
|
+
* @param {number} y
|
|
116
|
+
* @param {number} z
|
|
117
|
+
*/
|
|
118
|
+
return (x, y, z) => {
|
|
119
|
+
const ix = Math.floor(x);
|
|
120
|
+
const iy = Math.floor(y);
|
|
121
|
+
const iz = Math.floor(z);
|
|
122
|
+
let nearest = Infinity;
|
|
123
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
124
|
+
const wz = cells * cells * wrapped[iz + dz + 1];
|
|
125
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
126
|
+
const wy = wz + cells * wrapped[iy + dy + 1];
|
|
127
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
128
|
+
const p = 3 * (wy + wrapped[ix + dx + 1]);
|
|
129
|
+
const px = ix + dx + points[p] - x;
|
|
130
|
+
const py = iy + dy + points[p + 1] - y;
|
|
131
|
+
const pz = iz + dz + points[p + 2] - z;
|
|
132
|
+
const d = px * px + py * py + pz * pz;
|
|
133
|
+
if (d < nearest) {
|
|
134
|
+
nearest = d;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return nearest < 1 ? Math.sqrt(nearest) : 1;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The noise as RGBA bytes, x fastest, then y, then z.
|
|
145
|
+
* @param {number} [size] voxels along each side
|
|
146
|
+
*/
|
|
147
|
+
export default function createCloudNoise(size = 64) {
|
|
148
|
+
const channels = [new Float32Array(size ** 3), new Float32Array(size ** 3), new Float32Array(size ** 3)];
|
|
149
|
+
const gradients = [0, 1, 2].map((octave) => perlin(BASE_CELLS * 2 ** octave, 10 + octave));
|
|
150
|
+
const [billowCells, shapeDetail, fineShapeDetail, detail, fineDetail] = [
|
|
151
|
+
worley(BASE_CELLS, 20),
|
|
152
|
+
worley(2 * BASE_CELLS, 30),
|
|
153
|
+
worley(4 * BASE_CELLS, 40),
|
|
154
|
+
worley(4 * BASE_CELLS, 50),
|
|
155
|
+
worley(8 * BASE_CELLS, 60),
|
|
156
|
+
];
|
|
157
|
+
let i = 0;
|
|
158
|
+
for (let z = 0; z < size; z++) {
|
|
159
|
+
for (let y = 0; y < size; y++) {
|
|
160
|
+
for (let x = 0; x < size; x++) {
|
|
161
|
+
// the voxel's center, in the base shape's cells
|
|
162
|
+
const bx = ((x + 0.5) / size) * BASE_CELLS;
|
|
163
|
+
const by = ((y + 0.5) / size) * BASE_CELLS;
|
|
164
|
+
const bz = ((z + 0.5) / size) * BASE_CELLS;
|
|
165
|
+
let gradient = 0;
|
|
166
|
+
for (let octave = 0; octave < 3; octave++) {
|
|
167
|
+
const scale = 2 ** octave;
|
|
168
|
+
gradient += gradients[octave](bx * scale, by * scale, bz * scale) / scale;
|
|
169
|
+
}
|
|
170
|
+
const billows = 1 - billowCells(bx, by, bz);
|
|
171
|
+
// the billows, raised where the gradient noise is high
|
|
172
|
+
channels[0][i] = billows + (0.5 + 0.35 * gradient) * (1 - billows);
|
|
173
|
+
channels[1][i] = 1 - (0.7 * shapeDetail(2 * bx, 2 * by, 2 * bz) + 0.3 * fineShapeDetail(4 * bx, 4 * by, 4 * bz));
|
|
174
|
+
channels[2][i] = 1 - (0.7 * detail(4 * bx, 4 * by, 4 * bz) + 0.3 * fineDetail(8 * bx, 8 * by, 8 * bz));
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return toBytes(channels);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Channels of values as RGBA bytes, each stretched over the bytes' range, alpha opaque.
|
|
184
|
+
* @param {Float32Array[]} channels
|
|
185
|
+
*/
|
|
186
|
+
function toBytes(channels) {
|
|
187
|
+
const bytes = new Uint8Array(channels[0].length * 4);
|
|
188
|
+
channels.forEach((values, channel) => {
|
|
189
|
+
let min = Infinity;
|
|
190
|
+
let max = -Infinity;
|
|
191
|
+
for (const value of values) {
|
|
192
|
+
min = Math.min(min, value);
|
|
193
|
+
max = Math.max(max, value);
|
|
194
|
+
}
|
|
195
|
+
for (let j = 0; j < values.length; j++) {
|
|
196
|
+
bytes[4 * j + channel] = Math.round((255 * (values[j] - min)) / (max - min));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
for (let j = 3; j < bytes.length; j += 4) {
|
|
200
|
+
bytes[j] = 255;
|
|
201
|
+
}
|
|
202
|
+
return bytes;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The noise over the clouds' map as RGBA bytes, x fastest: a square that tiles, MAP_TILE_CELLS
|
|
207
|
+
* cells of the map across, with gradient noise over JITTER_CELLS in red and green and over
|
|
208
|
+
* COVERAGE_CELLS in blue.
|
|
209
|
+
* @param {number} [size] texels along each side
|
|
210
|
+
*/
|
|
211
|
+
export function createMapNoise(size = 256) {
|
|
212
|
+
const noises = [
|
|
213
|
+
[perlin(MAP_TILE_CELLS / JITTER_CELLS, 70), JITTER_CELLS],
|
|
214
|
+
[perlin(MAP_TILE_CELLS / JITTER_CELLS, 80), JITTER_CELLS],
|
|
215
|
+
[perlin(MAP_TILE_CELLS / COVERAGE_CELLS, 90), COVERAGE_CELLS],
|
|
216
|
+
];
|
|
217
|
+
const channels = noises.map(() => new Float32Array(size * size));
|
|
218
|
+
for (let y = 0; y < size; y++) {
|
|
219
|
+
for (let x = 0; x < size; x++) {
|
|
220
|
+
// the texel's center, in the map's cells
|
|
221
|
+
const cx = ((x + 0.5) / size) * MAP_TILE_CELLS;
|
|
222
|
+
const cy = ((y + 0.5) / size) * MAP_TILE_CELLS;
|
|
223
|
+
noises.forEach(([noise, cells], channel) => {
|
|
224
|
+
channels[channel][y * size + x] = /** @type {Function} */ (noise)(cx / /** @type {number} */ (cells), cy / /** @type {number} */ (cells), 0);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return toBytes(channels);
|
|
229
|
+
}
|