@geoblocks/cesium-post-process 0.1.3 → 0.3.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.
- package/README.md +49 -5
- package/cesium-post-process.js +7 -1
- package/dufour.js +225 -0
- package/local-frame.js +15 -0
- package/package.json +5 -2
- package/rain-rings.js +215 -0
- package/shaders/Dufour.js +2 -0
- package/shaders/RainRings.js +2 -0
- package/shaders/SnowLine.js +1 -1
- package/snow-line.js +54 -2
package/README.md
CHANGED
|
@@ -18,13 +18,16 @@ Each effect is inactive after construction. Every option is also a property, app
|
|
|
18
18
|
|
|
19
19
|
```javascript
|
|
20
20
|
import {Color, Rectangle} from '@cesium/engine';
|
|
21
|
-
import {AnalogVideo, Clouds, ColorIsolation, DigitalVideo, DroneDisplay, Infrared, Jello, LensDistortion, Lightning, MotionBlur, Precipitation, SnowLine, SpeedLines, Spotlight, Super8, Technicolor, TiltShift, Toy, ValleyFog, WhiteModel} from '@geoblocks/cesium-post-process';
|
|
21
|
+
import {AnalogVideo, Clouds, ColorIsolation, DigitalVideo, DroneDisplay, Dufour, Infrared, Jello, LensDistortion, Lightning, MotionBlur, Precipitation, RainRings, SnowLine, SpeedLines, Spotlight, Super8, Technicolor, TiltShift, Toy, ValleyFog, WhiteModel} from '@geoblocks/cesium-post-process';
|
|
22
22
|
|
|
23
23
|
const snowLine = new SnowLine(viewer, {
|
|
24
24
|
altitude: 2000, // meters above the ellipsoid where the snow is half covering
|
|
25
25
|
transition: 200, // height of the band over which the snow thins out, in meters
|
|
26
26
|
maxSlope: 50, // steepest slope that holds snow, in degrees
|
|
27
27
|
coverage: 1, // opacity of the snow, 0 to 1
|
|
28
|
+
patchiness: 0, // 0 to 1: at 1 the coverage is the share of the ground the snow covers, the noise leaving the rest bare
|
|
29
|
+
waterLevel: 564, // meters above the ellipsoid: what lies within a quarter meter of it holds no snow, so a flat lake stays water
|
|
30
|
+
sunDirection: sun, // the direction toward the sun the snow is shaded by, in world coordinates; Cesium's own sun if none
|
|
28
31
|
});
|
|
29
32
|
snowLine.active = true;
|
|
30
33
|
|
|
@@ -66,6 +69,17 @@ precipitation.active = true;
|
|
|
66
69
|
// the rain turns to snow, animatable
|
|
67
70
|
precipitation.snow = 1;
|
|
68
71
|
|
|
72
|
+
const rainRings = new RainRings(viewer, {
|
|
73
|
+
intensity: 0.5, // the share of the cells that have a drop, 0 to 1
|
|
74
|
+
cell: 2, // size of a cell, in meters: one drop in each
|
|
75
|
+
reach: 0.35, // how far a ring grows on the ground, in meters
|
|
76
|
+
waterLevel: 372, // height of the water above the ellipsoid, in meters, where the rings are wider and slower; undefined for none
|
|
77
|
+
maxSlope: 30, // steepest slope that takes a ring, in degrees
|
|
78
|
+
color: new Color(0.31, 0.39, 0.5, 0.6), // slate on a light ground, pale on a dark one; the alpha is how strongly it shows
|
|
79
|
+
origin: position, // the point of the world the cells are laid about; the camera's position when activated if none
|
|
80
|
+
});
|
|
81
|
+
rainRings.active = true;
|
|
82
|
+
|
|
69
83
|
const clouds = new Clouds(viewer, {
|
|
70
84
|
map: precipitation.map, // where it rains, as Precipitation's map; undefined for no clouds
|
|
71
85
|
cloudBase: 2500, // altitude of the cloud base, in meters above the ellipsoid
|
|
@@ -112,6 +126,17 @@ const whiteModel = new WhiteModel(viewer, {
|
|
|
112
126
|
});
|
|
113
127
|
whiteModel.active = true;
|
|
114
128
|
|
|
129
|
+
// or, in Toy's place, the Dufour map's hachures
|
|
130
|
+
const dufour = new Dufour(viewer, {
|
|
131
|
+
spacing: 3.5, // the strokes' spacing on the screen, in pixels
|
|
132
|
+
strokeLength: 14, // a stroke's length between its breaks, in spacings, on average
|
|
133
|
+
weight: 1, // 1 for Lehmann's scale stretched for the Alps: the ink's share grows with the slope, all of it at 60 degrees
|
|
134
|
+
oblique: 0.8, // how much thinner the strokes facing the north-west light, and thicker away from it, 0 to 1
|
|
135
|
+
flatSlope: 4, // the slope under which the ground is left blank, in degrees
|
|
136
|
+
outline: 0.8, // how dark the silhouettes' outline; 0 for none
|
|
137
|
+
});
|
|
138
|
+
dufour.active = true;
|
|
139
|
+
|
|
115
140
|
const tiltShift = new TiltShift(viewer, {
|
|
116
141
|
focus,
|
|
117
142
|
range: 0.3, // width of the sharp band, in factors of two of the focal distance (an orthographic camera: shares of the view's height)
|
|
@@ -197,18 +222,35 @@ super8.grain = 0.3;
|
|
|
197
222
|
super8.destroy();
|
|
198
223
|
```
|
|
199
224
|
|
|
200
|
-
### Shader
|
|
225
|
+
### Shader chunks
|
|
201
226
|
|
|
202
227
|
`EyeFromDepth` is the GLSL chunk the depth-based effects prepend to their fragment shaders: `vec4 eyeAt(sampler2D depthTexture, vec2 uv)` gives a pixel's eye coordinates from the depth texture, with `w` 0 for the sky, with or without a logarithmic depth buffer. It is exported for effects of your own, and for `@geoblocks/cesium-flyto`'s depth of field.
|
|
203
228
|
|
|
229
|
+
`Height`, prepended after it, gives heights above the ellipsoid: `float heightAt(vec3 eye)` of a point in eye coordinates, relative to the camera so that single precision keeps it to the centimeter, and `vec2 belowHeight(vec3 rayEnd, float height)`, the part of the ray from the camera to a point below a height, as shares of it. Its uniforms, the camera's height, the local up direction and the Earth's radius, come from `heightUniforms(scene)` in double precision:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import {EyeFromDepth, Height, heightUniforms} from '@geoblocks/cesium-post-process';
|
|
233
|
+
|
|
234
|
+
const stage = new PostProcessStage({
|
|
235
|
+
fragmentShader: EyeFromDepth + Height + myShader, // myShader calls eyeAt and heightAt
|
|
236
|
+
uniforms: {...heightUniforms(scene), myUniform: () => value},
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`Normal`, also prepended after `EyeFromDepth`, gives a pixel's normal in eye coordinates from the depth of its neighbors, facing the camera: `vec3 normalAt(sampler2D depthTexture, vec2 uv, vec3 center, float distance)`, with `center` the pixel's eye coordinates and the neighbors `distance` pixels away, on each axis the nearer side so that the scene's edges do not bend it; and `vec3 smoothNormalAt(sampler2D depthTexture, vec2 uv, vec3 center)`, averaged over two distances so that the terrain's flat triangles blend into a smoother surface. It needs no uniforms.
|
|
241
|
+
|
|
242
|
+
`localFromEye(frame, viewMatrix)` gives the matrix from the eye's coordinates to a local frame (such as `FixedFrameTransforms.eastNorthUpToFixedFrame(origin)`), made in double precision, for a shader that lays a pattern over the ground: `(localFromEye * vec4(eye.xyz, 1.0)).xy` is the pixel's place in meters, to the millimeter, where the world coordinates would step by half a meter.
|
|
243
|
+
|
|
204
244
|
### Order of the effects
|
|
205
245
|
|
|
206
|
-
Cesium runs the stages in the order they are added and cannot insert one elsewhere. Activate the effects in this order: `SnowLine`, `ValleyFog`, `Spotlight`, `Clouds`, `Precipitation`, `Lightning`, `Toy` or `
|
|
246
|
+
Cesium runs the stages in the order they are added and cannot insert one elsewhere. Activate the effects in this order: `SnowLine`, `ValleyFog`, `Spotlight`, `Clouds`, `Precipitation`, `RainRings`, `Lightning`, `Toy`, `WhiteModel` or `Dufour`, `TiltShift`, `MotionBlur`, `SpeedLines`, `ColorIsolation`, `Infrared`, `Technicolor`, `Super8`, `LensDistortion`, `Jello`, `DroneDisplay`, `AnalogVideo` or `DigitalVideo`. The snow is then under the fog, the clouds behind the rain and the lightning in front of them, the rain or the falling snow and the rings where the rain lands in the scene, the lens effects apply to the whole scene, and the film looks apply to everything, their black bars included. The drone camera comes last, in the order of a real one: its lens bends the picture, its sensor reads it with the jello of its shake, its display is drawn over the picture and stays straight, and the video signal, analog or digital, disturbs both. To activate an effect later, deactivate and activate again the active effects that come after it.
|
|
207
247
|
|
|
208
248
|
### Notes
|
|
209
249
|
|
|
210
250
|
`WhiteModel` is the look of an architect's plaster model or white render: the colors turned to their value and pressed into a narrow range of whites, so the light and the shapes alone tell the surfaces apart; lower `values` for more contrast, a `saturation` above 0 to keep an accent (the project in a color, the context white). The darkest colors, under `poche` (a tenth by default), keep their value, fading into the whites about it: a section's poché stays solid black; give 0 if deep shadows or a dusk scene should stay gray. An accent's hue is scaled down rather than clipped where a channel would pass 1. It is a `Toy` with another grading and shares its edges, found in the depth (`DepthEdges`, a shader chunk both prepend): the creases lit and shaded, the outline on the near side of a jump. Where surfaces meet it adds an occlusion, the Alchemy ambient obscurance ("The Alchemy Screen-Space Ambient Obscurance Algorithm", McGuire et al., HPG 2011): each pixel's normal from the depth's slopes, then the share of 12 samples on a spiral in a disc of `radius` meters that rise above its surface, each the more as it rises steeper and none beyond the radius, the spiral turned from pixel to pixel. A corner's foot gets about a half, a flat floor none. Zoomed out, when the disc is under a few pixels wide, the occlusion fades out: its samples would fall within the depth's noise. Each pixel then reads the depth about 20 times. For the whitest model, give the scene's materials light colors too: the grading keeps their differences in value.
|
|
211
251
|
|
|
252
|
+
`Dufour` draws the terrain as the hachures of the Dufour map (1845-1864), black ink on white paper: strokes down the steepest slope, each a straight line broken every `strokeLength` spacings or so, its breaks staggered from the next stroke's, so that no line runs across them. A stroke covers more of the spacing as the slope is steeper, on Lehmann's scale (1799) stretched for the Alps, the ink's share growing from none on the flat to all of it at 60 degrees rather than his 45, but never more than 45 % of the spacing, so that the paper shows between the strokes, and more on the slopes away from a light from the north-west, as Dufour's: the steep ground and the slopes in the shade are darker, the flat ground under `flatSlope` is blank, the strokes fading in over 2 degrees above it, so that the terrain's slightly tilted triangles do not show as patches. The slope comes from the depth's normals, averaged over 4, 12 and 24 pixels, and the way down from one averaged wider, over 32 pixels, snapped to one of 12 directions, the two nearest blended over a narrow band: the strokes are straight and parallel on a slope rather than turning with each pixel's normal, which drew swirls close up. The strokes are fixed to the ground, in a frame of east and north on the ground below the camera, which follows it once it has moved 20 km, where the strokes jump. Their spacing is a power of two of meters, chosen for each direction from the ground a pixel covers across its strokes, far more than its width where a slope is seen at a grazing angle, the next power faded in as they grow closer than `spacing` pixels: they stay about as far apart on the screen at any distance and angle; where they still grow too close to tell apart, they fade to the share of the paper they cover. The lakes, flat, are left blank.
|
|
253
|
+
|
|
212
254
|
`ColorIsolation`, `Infrared`, `Technicolor` and `Super8` work on display colors, after Cesium's tone mapping. `ColorIsolation`, `Infrared` and `Technicolor` follow prod80's [ReShade shaders](https://github.com/prod80/prod80-ReShade-Repository) (MIT). `Technicolor` imitates the three-strip process: the scene is split into red, green and blue records, each printed with its complementary dye, which gives its dense, saturated primaries. `Super8` blurs the highlights into a red-orange halation, as light scattered back through the film base, draws its grain as clumps a few pixels wide that differ slightly between the dye layers, and moves at 18 frames per second (grain, weave and flicker change with each film frame, light leaks drift slowly), so while it is active the scene renders 18 times per second, also with `requestRenderMode`.
|
|
213
255
|
|
|
214
256
|
`MotionBlur` blurs each pixel along its motion on the screen since the previous frame, over the exposure time, with the depth-weighted gathering of "A Reconstruction Filter for Plausible Motion Blur" (McGuire et al., I3D 2012). Only the camera moves: each pixel's motion follows from its depth and the previous frame's view, written once per pixel into a velocity buffer at half resolution rather than recomputed for each sample, and the samples are weighted by depth so that near terrain and far ridges or sky do not smear into each other. Without the paper's neighborhood velocity pass, a pixel only gathers along its own motion: the blur of moving terrain does not spill over a background that does not move, such as the sky, and its outline stays sharp there. The exposure is scaled by the time between frames, so the blur is the same whatever the frame rate; after a pause in `requestRenderMode`, that time is taken as at most a tenth of a second. At strength 0 its stage is disabled.
|
|
@@ -223,15 +265,17 @@ Cesium runs the stages in the order they are added and cannot insert one elsewhe
|
|
|
223
265
|
|
|
224
266
|
Its `snow`, from 0 to 1, turns the rain to snow in one population, so that nothing in between looks like two kinds of precipitation falling together: the drops slow down from 9 m/s to 1 m/s (geometrically, so 3 m/s at 0.5, as in sleet), their streak, the motion over the exposure, shortens until they are round flakes, their thin sharp line grows into a soft flake that flutters across its cell at its own rate, the nearest layer out of focus as in Andrew Baldwin's "Just snow" (Shadertoy), and there are more of them, as there are in a snowfall; the haze goes from the rain's to the snow's, thicker, from 20 km of visibility to 500 m, with lighter shafts and a brighter light the flakes scatter. Each drop is a capsule from a point in its cell to the end of its trail. The cells of the grid are taller than wide in the rain and square in the snow, so that a trail stays within the cell above the pixel's: animating `snow` stretches the whole field smoothly along the fall, and the scroll along the fall is added up from frame to frame, as the speed changes with it. The cost does not depend on `snow`: about 4.3 to 4.8 ms at 1920 x 1024 on an Intel UHD 630, mostly the four layers; the layers' constants are computed once per frame on the CPU, and each pixel first rejects the drops that are nowhere near it. Wang and Wade's precipitation for Flight Simulator 2004 ("Rendering Falling Rain and Snow", 2004) also draws both with one technique. Several may be active at once: the first one dims the light and turns the shadows off, the last one restores them.
|
|
225
267
|
|
|
268
|
+
`RainRings` lays the ground out in cells of a local frame, east-north-up about `origin`, and in each lands a drop, as often as `intensity` says, where the cell's hash puts it: a thin ring grows from it over about 0.6 s and fades, on what is flatter than `maxSlope`, wider and slower where the surface is within a quarter meter of `waterLevel`. It finds the surfaces from the depth, so it rings the ground, the roofs and the water alike and nothing in the air, and tells the slope from the normal rebuilt from the neighboring pixels. The cells are in a local frame with the globe's offset cancelled in double precision on the CPU (`localFromEye`), so they stay where they are as the camera moves and do not step by half a meter with the single-precision world coordinates a shader gets. A ring never grows past 0.3 cell, so that its cell's edge does not clip it, and fades out where a pixel is wider than 8 to 20 cm on the ground, where it would only be a speckle. It renders the scene 30 times a second while active with an intensity, also in `requestRenderMode`. It has no sense of what is under a roof: rain does not fall there, and the rings do.
|
|
269
|
+
|
|
226
270
|
`Clouds` fills a slab above the cloud base from a map of the rain, from which it derives a map of the clouds on the CPU, about 60 ms for the 1024 x 768 radar map, once for each map: a radar sees the rain rather than the clouds, so the rain is dilated by 5 km and softened into a cloud shield wider than it, broken over dry ground and full over the rain, and the rain rate sets the clouds' type, from a flat deck 1500 m thick over drizzle to towers up to `cloudTop` from 10 mm/h, `remap(log(R), log(0.5), log(10), 0, 1)`, the precipitation turning the map "to cumulonimbus clouds" as in Schneider's "The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn" (2015). Their bases are flat, the deck full up to two thirds of its height, the towers thinning out upward in the convective envelope of Schneider's "Nubis, Evolved" (2022) and spreading an anvil near their top, as in his "Nubis: Authoring Real-Time Volumetric Cloudscapes" (2017); the map is read jittered by a third of a cell of noise, so its 1 km squares do not show, and the coverage varies by 40 % with an 8 km noise that is not the billows' (Högfeldt 2016), both from a 256 x 256 square of noise made once that tiles every 64 cells of the map, read once rather than computed at each sample. Their shape is a noise made once on the CPU, about a third of a second for the page: a 64³ cube that repeats every 16 km, with billows of Perlin-Worley noise for the base shape and Worley noise at two finer scales for the details, which erode the edges of the cloud but not its core, as wisps at its base and billows above. The height profile times the coverage carves the noise rather than scaling it, `saturate(noise - (1 - profile))` (Schneider 2022), so the core stays solid and only the highest billows reach the top, ramping up over a softness that grows with the distance rather than sharpened by a power as in "Nubis Cubed" (2023), which with voxels 250 m wide would show their grid. It is a 3D texture read with its mipmaps at the footprint of the pixels, and a second, four times larger read of it breaks up the repetition of the tile in far views; the details fade out and the edges soften with the distance, so the far clouds do not alias at the pass's resolution. The clouds are raymarched in 32 steps along each view ray, short near the camera and longer with the distance, in a pass at a quarter of the resolution: each ray first probes a map of the highest rain around 4 km blocks at 8 points, so the march starts at the tallest cloud it can meet, rather than in the empty air above the clouds, and is skipped where it meets none; each step's light is integrated over it, and jittered anew at each frame, then accumulated over the frames, about 10 frames' worth, reprojected through where the ray enters the slab and clamped to each frame's neighborhood, as in Hillaire's "Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite" (2016); it is blurred over 3 x 3 texels of about the same depth, and upscaled bilinearly from the texels at the pixel's depth, the farthest of each texel's block of pixels, or from the nearest in depth, so ridges in front of a cloud keep a sharp outline without a halo (Pesce 2016). The sun lights them through two samples toward it, with the light scattered more than once as two octaves (Hillaire 2016) and a Henyey-Greenstein phase function with a forward lobe for the silver lining; the sky lights them in the color of Cesium's sky overhead and the ground from below, less in their core than at their edges and least at their base, and the clouds over heavy rain absorb a fifth of their light, so their bases are dark gray from below, their tops white from above, and in a faintly cool moonlight at night. The far clouds fade into the sky behind them by Cesium's fog factor, as the terrain does. The shadow map holds, across a square around the camera seen from the sun, 80 km wide or four times as wide as the camera is high up to 800 km, fading out toward its edges, the clouds' optical depth from the top of the slab down to each quarter of its height, 512 x 512 texels marched 16 steps each, but for those whose rays meet no clouds, drawn again only when the camera moves a 40th of its width, the clock changes or the clouds change, at most 4 times a second: the clouds cast their shadows on the terrain and light `Precipitation`'s haze through their gaps. It is not read for the clouds' own light: with four heights, those of a thin deck would take in the deck itself, darkening its top. Seen from far above they are cloud tops; coming down, the camera flies into them and the view turns to fog; activated before `Precipitation`, its drops, shafts and haze are drawn over the clouds, so from below the cloud base they are seen through the rain; from above, `Precipitation` leaves the rain under the cloud base to them, its shafts and haze hidden while clouds are active in the scene. Use the same `map` and `cloudBase` as `Precipitation`. Both crossfade from their `map` to a `nextMap` over the same rectangle and of the same size by `mapBlend`, 0 to 1, as the radar from one hour to the next: one more read of the map for each of its lookups. Without a map there are no clouds. Their noise drifts toward the east with the `wind`, 10 m/s by default, leaning 500 m downwind from the base to the top of the slab (Schneider 2017), while the map stays put: the clouds stay where it rains and their billows pass through them. It goes by the scene's clock, a time-lapse while it runs faster and still while it is paused; with a `drift`, in m/s, 0 by default, the noise also drifts by the real time, so the clouds move while the clock is paused, and the scene renders 30 times per second while they are active, also with `requestRenderMode`. After the camera, the clock or the map changes, the scene renders at 30 frames per second for half a second, for the accumulation to settle, also with `requestRenderMode`, then rests. Each ray is marched at most 40 km into the slab: farther, a grazing ray's steps would be longer than the billows and the clouds on the horizon would turn to streaks. On an Intel UHD 630 at 1341 x 660, they add about 6.5 ms to the frame seen from 30 km above, 5.1 ms at an angle from 12 km, 4.8 ms inside the clouds and 3.9 ms toward clouds on the horizon; drawing the shadow map again, while the clock runs, adds no more than a few milliseconds to those frames.
|
|
227
271
|
|
|
228
272
|
`Lightning` draws cloud-to-ground strikes at random around the camera, or in front of it with `inFront`, on the ground, so that they stay where they are as the camera moves, and, with a map, only on heavy rain, more surely as it is heavier, from about 2.5 to 8 mm/h, within the radius or as far from the camera as it is high, up to 150 km. Each is a channel from the cloud base to the ground that wanders to either side, by noise of the position along it in four octaves, linear between random values so that it is straight segments with sharp turns, with branches that leave it downward at an angle, wander the same way and fade toward their ends. It is drawn over the picture in screen space, with a hot core in a halo, at least a pixel and a half wide: the channel is centimeters wide, and would vanish at its true width a thousand meters away. The distance to it is a function of the position along the line, a few noise lookups for each channel, not a polyline for each pixel. It is hidden behind nearer terrain. Seen from above the cloud base, the channel is hidden in the cloud, and the flash lights the cloud over the strike from inside: a glow 4 km wide, 1500 m above the cloud base, following the flicker of the strokes. It follows the lightning flash as it is described in Rakov and Uman's "Lightning: Physics and Effects" (2003): 2 to 4 return strokes tens of milliseconds apart down one channel, the first the brightest, with a dimmer current between them keeping it lit, and a flash that follows their flicker: it lights each pixel in proportion to its own color, less with its distance to the foot of the strike, and the sky toward the cloud above it. The core is white and the halo and the flash are blue with a violet cast: the channel is a plasma of about 30000 K, whose light is mostly the lines of nitrogen, with the hydrogen line of the water in a storm's air adding red (Kieu et al., "High-speed spectroscopy of lightning-like discharges", 2021). A far strike turns yellow, then red, as the scattering of the air takes the blue out of its light (an extinction that goes with the wavelength to the minus 4), and its flash tints the sky warm. Up to four strikes are active at once, each over after a second. The strokes flicker, so while it is active the scene renders 30 times per second, also with `requestRenderMode`; at intensity 0 its stage is disabled and the scene is not rendered for it. Without a strike the pass costs nothing measurable, and the flash alone as little: the cost is the channel, each pixel near it computing its distance to the line and to the branches, whose constants come from the CPU once for each strike, and pixels farther than the channel's reach skip it. On an Intel UHD 630 at 1070 x 611, a strike on the screen adds about 2.3 ms to the frame while it lasts, a few tenths of a second, and four at once about 3.8 ms; the cost goes with the area around the channel, so about three times that at 1920 x 1024. It does not dim the sky itself, so it goes with `Precipitation`, which does.
|
|
229
273
|
|
|
230
274
|
`Spotlight`'s beam in the air is raymarched, as Killzone Shadow Fall's volumetrics (Valient, SIGGRAPH 2014), in a pass at half the resolution: 8 samples along the part of each view ray inside the cone, found analytically, dithered per pixel, each weighted by the cone's penumbra, the dust, the inverse square from the light softened around it and the extinction of the haze so far, then scattered toward the camera with a Henyey-Greenstein phase of asymmetry `beamAnisotropy`. A 3 x 3 blur at that resolution, weighted by the scene distance, smooths the dither out, and the full resolution takes the four texels around each pixel, interpolated where they are at its distance, else the nearest in distance, so that the haze stops at the ridges in front of it rather than spilling onto them; the half-float texture holds the log2 of the distance. On an Intel UHD 630 at 1920 x 1080, the beam costs about 1.9 ms, from 5 ms at full resolution with 16 samples. It holds from any viewpoint, inside the cone included, where the former analytic beam, which took its penumbra where the ray passes closest to the axis, went dark; its dust streaks are noise sampled along the beam, radiating from the light. The light is added to a picture Cesium has already tone mapped, so the output goes through a filmic roll-off (ACES) rather than clipping to flat white.
|
|
231
275
|
|
|
232
|
-
`SnowLine` and `ValleyFog` compute each pixel's height relative to the camera, in double precision on the CPU for the camera, so they stay accurate far from the origin. The snow's slope comes from the normal rebuilt from the neighboring pixels. The fog is lit by the sun like Cesium's own fog: its color during the day, a moonlit blue at night, as bright as `scene.fog.minimumBrightness`. Both work in 3D only.
|
|
276
|
+
`SnowLine` and `ValleyFog` compute each pixel's height relative to the camera, in double precision on the CPU for the camera, so they stay accurate far from the origin. The snow's slope comes from the normal rebuilt from the neighboring pixels. The fog is lit by the sun like Cesium's own fog: its color during the day, a moonlit blue at night, as bright as `scene.fog.minimumBrightness`. Both work in 3D only. `SnowLine`'s `patchiness` turns the coverage from the snow's opacity into the share of the ground it covers: the same noise that breaks up the edge of the snow line thins it into patches. Its `sunDirection` is for a scene lit by a light of its own, where Cesium's sun would shade the snow the wrong way: `scene.light.direction` is the way a light travels, so give its negation.
|
|
233
277
|
|
|
234
|
-
`SnowLine`, `ValleyFog`, `Precipitation`, `Clouds`, `Lightning`, `Toy`, `WhiteModel`, `TiltShift`, `Spotlight` and `MotionBlur` read the depth buffer. The post-process stages only get the terrain's depth when it is depth tested, so `globe.depthTestAgainstTerrain` is on while any of them is active and restored when the last one is deactivated: billboards and labels that show through the terrain can be hidden by it meanwhile.
|
|
278
|
+
`SnowLine`, `ValleyFog`, `Precipitation`, `RainRings`, `Clouds`, `Lightning`, `Toy`, `WhiteModel`, `Dufour`, `TiltShift`, `Spotlight` and `MotionBlur` read the depth buffer. The post-process stages only get the terrain's depth when it is depth tested, so `globe.depthTestAgainstTerrain` is on while any of them is active and restored when the last one is deactivated: billboards and labels that show through the terrain can be hidden by it meanwhile.
|
|
235
279
|
|
|
236
280
|
The depth they read is the nearest of Cesium's frustums': with `scene.logarithmicDepthBuffer` off, Cesium splits the view into frustums of `farToNearRatio` (1000 by default) from the near plane, and only the nearest one's depth reaches the stages. What lies past the split has no depth there: `Toy` and `WhiteModel` paint their sky over it and draw no edge on it, `TiltShift` blurs it as the far background, `SnowLine` and `ValleyFog` take it for sky. Keep the logarithmic depth buffer on, or set `camera.frustum.near` so that one frustum holds the scene (a near plane of 0.3 m reaches 300 m).
|
|
237
281
|
|
package/cesium-post-process.js
CHANGED
|
@@ -2,6 +2,7 @@ export {default as AnalogVideo} from './analog-video.js';
|
|
|
2
2
|
export {default as Clouds} from './clouds.js';
|
|
3
3
|
export {default as ColorIsolation} from './color-isolation.js';
|
|
4
4
|
export {default as DigitalVideo} from './digital-video.js';
|
|
5
|
+
export {default as Dufour} from './dufour.js';
|
|
5
6
|
export {default as DroneDisplay} from './drone-display.js';
|
|
6
7
|
export {default as Infrared} from './infrared.js';
|
|
7
8
|
export {default as Jello} from './jello.js';
|
|
@@ -9,6 +10,7 @@ export {default as LensDistortion} from './lens-distortion.js';
|
|
|
9
10
|
export {default as Lightning} from './lightning.js';
|
|
10
11
|
export {default as MotionBlur} from './motion-blur.js';
|
|
11
12
|
export {default as Precipitation} from './precipitation.js';
|
|
13
|
+
export {default as RainRings} from './rain-rings.js';
|
|
12
14
|
export {default as SnowLine} from './snow-line.js';
|
|
13
15
|
export {default as SpeedLines} from './speed-lines.js';
|
|
14
16
|
export {default as Spotlight} from './spotlight.js';
|
|
@@ -19,5 +21,9 @@ export {default as Toy} from './toy.js';
|
|
|
19
21
|
export {default as ValleyFog} from './valley-fog.js';
|
|
20
22
|
export {default as WhiteModel} from './white-model.js';
|
|
21
23
|
|
|
22
|
-
// the shader
|
|
24
|
+
// the shader chunks the depth-based effects share, and the uniforms of Height, for effects of your own
|
|
23
25
|
export {default as EyeFromDepth} from './shaders/EyeFromDepth.js';
|
|
26
|
+
export {default as Height} from './shaders/Height.js';
|
|
27
|
+
export {heightUniforms} from './height.js';
|
|
28
|
+
export {localFromEye} from './local-frame.js';
|
|
29
|
+
export {default as Normal} from './shaders/Normal.js';
|
package/dufour.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {Cartesian3, Color, FixedFrameTransforms, Math as CesiumMath, Matrix3, 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 {heightUniforms} from './height.js';
|
|
6
|
+
import DepthEdges from './shaders/DepthEdges.js';
|
|
7
|
+
import DufourShader from './shaders/Dufour.js';
|
|
8
|
+
import EyeFromDepth from './shaders/EyeFromDepth.js';
|
|
9
|
+
import Hash from './shaders/Hash.js';
|
|
10
|
+
import Height from './shaders/Height.js';
|
|
11
|
+
import Normal from './shaders/Normal.js';
|
|
12
|
+
|
|
13
|
+
// how far the camera moves over the ground, in meters, before the hachures' frame follows it: they jump then
|
|
14
|
+
const REBASE = 20000;
|
|
15
|
+
|
|
16
|
+
const originECScratch = new Cartesian3();
|
|
17
|
+
const eastScratch = new Cartesian3();
|
|
18
|
+
const northScratch = new Cartesian3();
|
|
19
|
+
const surfaceScratch = new Cartesian3();
|
|
20
|
+
const frameScratch = new Matrix4();
|
|
21
|
+
const paperScratch = new Cartesian3();
|
|
22
|
+
const inkScratch = new Cartesian3();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The look of the Dufour map, the Swiss topographic map of the 1850s: the slopes drawn in hachures, ink strokes down
|
|
26
|
+
* the steepest slope, straight and broken along their length, thicker where it is steeper and on the slopes away from a light from
|
|
27
|
+
* the north-west, the flat ground left blank, black ink on white paper.
|
|
28
|
+
* The strokes are fixed to the ground: they stay put as the camera moves, a power of two of meters apart, the next
|
|
29
|
+
* power faded in as they grow closer than `spacing` pixels.
|
|
30
|
+
*/
|
|
31
|
+
export default class Dufour extends Effect {
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('@cesium/engine').CesiumWidget} viewer
|
|
34
|
+
* @param {{spacing?: number, strokeLength?: number, weight?: number, oblique?: number, flatSlope?: number, outline?: number, jump?: number, paper?: Color, ink?: Color}} [options]
|
|
35
|
+
*/
|
|
36
|
+
constructor(viewer, options = {}) {
|
|
37
|
+
super(viewer);
|
|
38
|
+
this.spacing_ = options.spacing ?? 3.5;
|
|
39
|
+
this.strokeLength_ = options.strokeLength ?? 14;
|
|
40
|
+
this.weight_ = options.weight ?? 1;
|
|
41
|
+
this.oblique_ = options.oblique ?? 0.8;
|
|
42
|
+
this.flatSlope_ = options.flatSlope ?? 4;
|
|
43
|
+
this.outline_ = options.outline ?? 0.8;
|
|
44
|
+
this.jump_ = options.jump ?? 20;
|
|
45
|
+
this.paper_ = options.paper ?? Color.fromCssColorString('#f7f3ea');
|
|
46
|
+
this.ink_ = options.ink ?? Color.fromCssColorString('#1d1b19');
|
|
47
|
+
/** @type {Cartesian3 | undefined} */
|
|
48
|
+
this.origin_ = undefined;
|
|
49
|
+
// east, north and up at the origin
|
|
50
|
+
this.frame_ = new Matrix3();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @override
|
|
55
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
56
|
+
*/
|
|
57
|
+
createStage_(scene) {
|
|
58
|
+
const toEye = (/** @type {number} */ column, /** @type {Cartesian3} */ result) => {
|
|
59
|
+
this.follow_(scene);
|
|
60
|
+
Matrix3.getColumn(this.frame_, column, result);
|
|
61
|
+
return Matrix4.multiplyByPointAsVector(scene.camera.viewMatrix, result, result);
|
|
62
|
+
};
|
|
63
|
+
return new PostProcessStage({
|
|
64
|
+
fragmentShader: EyeFromDepth + DepthEdges + Height + Normal + Hash + DufourShader,
|
|
65
|
+
uniforms: {
|
|
66
|
+
...heightUniforms(scene),
|
|
67
|
+
spacing: () => this.spacing_,
|
|
68
|
+
strokeLength: () => this.strokeLength_,
|
|
69
|
+
weight: () => this.weight_,
|
|
70
|
+
oblique: () => this.oblique_,
|
|
71
|
+
flatSlope: () => CesiumMath.toRadians(this.flatSlope_),
|
|
72
|
+
outline: () => this.outline_,
|
|
73
|
+
jump: () => this.jump_,
|
|
74
|
+
paper: () => Cartesian3.fromElements(this.paper_.red, this.paper_.green, this.paper_.blue, paperScratch),
|
|
75
|
+
ink: () => Cartesian3.fromElements(this.ink_.red, this.ink_.green, this.ink_.blue, inkScratch),
|
|
76
|
+
originEC: () => {
|
|
77
|
+
this.follow_(scene);
|
|
78
|
+
return Matrix4.multiplyByPoint(scene.camera.viewMatrix, /** @type {Cartesian3} */ (this.origin_), originECScratch);
|
|
79
|
+
},
|
|
80
|
+
eastEC: () => toEye(0, eastScratch),
|
|
81
|
+
northEC: () => toEye(1, northScratch),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Moves the hachures' frame under the camera once it is REBASE meters away over the ground.
|
|
88
|
+
* @private
|
|
89
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
90
|
+
*/
|
|
91
|
+
follow_(scene) {
|
|
92
|
+
const surface = scene.ellipsoid.scaleToGeodeticSurface(scene.camera.positionWC, surfaceScratch);
|
|
93
|
+
if (!surface || (this.origin_ && Cartesian3.distance(surface, this.origin_) < REBASE)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.origin_ = Cartesian3.clone(surface, this.origin_);
|
|
97
|
+
Matrix4.getMatrix3(FixedFrameTransforms.eastNorthUpToFixedFrame(this.origin_, scene.ellipsoid, frameScratch), this.frame_);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @override
|
|
102
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
103
|
+
*/
|
|
104
|
+
activated_(scene) {
|
|
105
|
+
acquireTerrainDepth(scene);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @override
|
|
110
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
111
|
+
*/
|
|
112
|
+
deactivating_(scene) {
|
|
113
|
+
releaseTerrainDepth(scene);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The strokes' spacing on the screen, in pixels: the closest they get before the next power of two of meters
|
|
118
|
+
* takes over.
|
|
119
|
+
*/
|
|
120
|
+
get spacing() {
|
|
121
|
+
return this.spacing_;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
set spacing(value) {
|
|
125
|
+
this.spacing_ = value;
|
|
126
|
+
this.viewer.scene.requestRender();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A stroke's length between its breaks, in spacings, on average.
|
|
131
|
+
*/
|
|
132
|
+
get strokeLength() {
|
|
133
|
+
return this.strokeLength_;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set strokeLength(value) {
|
|
137
|
+
this.strokeLength_ = value;
|
|
138
|
+
this.viewer.scene.requestRender();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The ink's weight: 1 for Lehmann's scale stretched for the Alps, the share of the spacing a stroke covers growing
|
|
143
|
+
* with the slope, all of it at 60 degrees, before the light.
|
|
144
|
+
*/
|
|
145
|
+
get weight() {
|
|
146
|
+
return this.weight_;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set weight(value) {
|
|
150
|
+
this.weight_ = value;
|
|
151
|
+
this.viewer.scene.requestRender();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* How much thinner the strokes on the slopes facing the north-west light, and thicker away from it, 0 to 1.
|
|
156
|
+
*/
|
|
157
|
+
get oblique() {
|
|
158
|
+
return this.oblique_;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
set oblique(value) {
|
|
162
|
+
this.oblique_ = value;
|
|
163
|
+
this.viewer.scene.requestRender();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The slope under which the ground is left blank, in degrees.
|
|
168
|
+
*/
|
|
169
|
+
get flatSlope() {
|
|
170
|
+
return this.flatSlope_;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
set flatSlope(value) {
|
|
174
|
+
this.flatSlope_ = value;
|
|
175
|
+
this.viewer.scene.requestRender();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How dark the outline along the silhouettes, 0 for none.
|
|
180
|
+
*/
|
|
181
|
+
get outline() {
|
|
182
|
+
return this.outline_;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
set outline(value) {
|
|
186
|
+
this.outline_ = value;
|
|
187
|
+
this.viewer.scene.requestRender();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* How far the depth jumps, in pixels' sizes, to be a silhouette, outlined: 20 by default.
|
|
192
|
+
*/
|
|
193
|
+
get jump() {
|
|
194
|
+
return this.jump_;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
set jump(value) {
|
|
198
|
+
this.jump_ = value;
|
|
199
|
+
this.viewer.scene.requestRender();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The paper's color, and the sky's.
|
|
204
|
+
*/
|
|
205
|
+
get paper() {
|
|
206
|
+
return this.paper_;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
set paper(value) {
|
|
210
|
+
this.paper_ = value;
|
|
211
|
+
this.viewer.scene.requestRender();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The ink's color.
|
|
216
|
+
*/
|
|
217
|
+
get ink() {
|
|
218
|
+
return this.ink_;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
set ink(value) {
|
|
222
|
+
this.ink_ = value;
|
|
223
|
+
this.viewer.scene.requestRender();
|
|
224
|
+
}
|
|
225
|
+
}
|
package/local-frame.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {Matrix4} from '@cesium/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The local frame's coordinates from the eye's, for a shader that wants a pixel's place in a small frame (east-north-up about a point) to
|
|
5
|
+
* lay a pattern over the ground that stays where it is as the camera moves. Made in double precision on the CPU: the globe's offset
|
|
6
|
+
* (about 6.4 million meters) cancels in the product, so the matrix a shader gets is small and a place comes out to the millimeter. In world
|
|
7
|
+
* coordinates, a single precision float steps by half a meter.
|
|
8
|
+
* @param {Matrix4} frame The local frame in the world, such as `FixedFrameTransforms.eastNorthUpToFixedFrame(origin)`.
|
|
9
|
+
* @param {Matrix4} viewMatrix The camera's view matrix.
|
|
10
|
+
* @param {Matrix4} [result]
|
|
11
|
+
* @return {Matrix4}
|
|
12
|
+
*/
|
|
13
|
+
export function localFromEye(frame, viewMatrix, result = new Matrix4()) {
|
|
14
|
+
return Matrix4.inverseTransformation(Matrix4.multiply(viewMatrix, frame, result), result);
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geoblocks/cesium-post-process",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"color-isolation.js",
|
|
23
23
|
"depth-test.js",
|
|
24
24
|
"digital-video.js",
|
|
25
|
+
"dufour.js",
|
|
25
26
|
"drone-display.js",
|
|
26
27
|
"effect.js",
|
|
27
28
|
"focus.js",
|
|
@@ -31,8 +32,10 @@
|
|
|
31
32
|
"jello.js",
|
|
32
33
|
"lens-distortion.js",
|
|
33
34
|
"lightning.js",
|
|
35
|
+
"local-frame.js",
|
|
34
36
|
"motion-blur.js",
|
|
35
37
|
"precipitation.js",
|
|
38
|
+
"rain-rings.js",
|
|
36
39
|
"snow-line.js",
|
|
37
40
|
"speed-lines.js",
|
|
38
41
|
"spotlight.js",
|
|
@@ -53,5 +56,5 @@
|
|
|
53
56
|
"@cesium/core": ">=0.1.0",
|
|
54
57
|
"@cesium/engine": ">=10"
|
|
55
58
|
},
|
|
56
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "866feb087ad153c2303fdf12b670ec78f192f825"
|
|
57
60
|
}
|
package/rain-rings.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {Cartesian3, Cartesian4, Color, FixedFrameTransforms, Matrix4, Math as CesiumMath} 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 {heightUniforms} from './height.js';
|
|
7
|
+
import {localFromEye} from './local-frame.js';
|
|
8
|
+
import EyeFromDepth from './shaders/EyeFromDepth.js';
|
|
9
|
+
import Hash from './shaders/Hash.js';
|
|
10
|
+
import Height from './shaders/Height.js';
|
|
11
|
+
import Normal from './shaders/Normal.js';
|
|
12
|
+
import RainRingsShader from './shaders/RainRings.js';
|
|
13
|
+
|
|
14
|
+
// the rings grow and fade over a fraction of a second: a render for each frame of a video
|
|
15
|
+
const FRAME_RATE = 30;
|
|
16
|
+
// the shader's time wraps at an hour, a whole number of the rings' periods
|
|
17
|
+
const TIME_PERIOD = 3600;
|
|
18
|
+
// the water's level when there is no water: far above everything
|
|
19
|
+
const NO_WATER = 1e9;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rings where the rain lands: on the ground, the roofs and the water, in each cell of a local frame a drop landing, a thin ring growing from it
|
|
23
|
+
* and fading, wider and slower on water. For a view of a place from far enough that the drops themselves are too small to see; or under
|
|
24
|
+
* `Precipitation`, the drops falling and these where they land. Found from the depth, with the surface's slope from the normal rebuilt from the
|
|
25
|
+
* neighboring pixels: nothing lies in the air.
|
|
26
|
+
*/
|
|
27
|
+
export default class RainRings extends Effect {
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('@cesium/engine').CesiumWidget} viewer
|
|
30
|
+
* @param {{intensity?: number, cell?: number, reach?: number, waterLevel?: number, maxSlope?: number, rate?: number,
|
|
31
|
+
* color?: Color, origin?: Cartesian3}} [options] The share of the cells that have a drop, 0 to 1; the cell's size and the ring's growth
|
|
32
|
+
* on the ground, in meters (the ring on water is clipped at 0.3 cell); the water's height above the ellipsoid, in meters, or undefined
|
|
33
|
+
* for none; the steepest slope that takes a ring, in degrees; rings a second in a cell; the ring's color, its alpha how strongly it shows; the
|
|
34
|
+
* point of the world the cells are laid about.
|
|
35
|
+
*/
|
|
36
|
+
constructor(viewer, options = {}) {
|
|
37
|
+
super(viewer);
|
|
38
|
+
this.intensity_ = options.intensity ?? 0.5;
|
|
39
|
+
this.cell_ = options.cell ?? 2;
|
|
40
|
+
this.reach_ = options.reach ?? 0.35;
|
|
41
|
+
this.waterLevel_ = options.waterLevel;
|
|
42
|
+
this.maxSlope_ = options.maxSlope ?? 30;
|
|
43
|
+
this.rate_ = options.rate ?? 1.6;
|
|
44
|
+
this.color_ = options.color ?? new Color(0.31, 0.39, 0.5, 0.6);
|
|
45
|
+
this.origin_ = options.origin && Cartesian3.clone(options.origin);
|
|
46
|
+
/** @type {Matrix4 | undefined} */
|
|
47
|
+
this.localFrame_ = undefined;
|
|
48
|
+
this.colorScratch_ = new Cartesian4();
|
|
49
|
+
this.localScratch_ = new Matrix4();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The local frame, east-north-up about the origin: the camera's position when the effect is activated, if none was given.
|
|
54
|
+
* @private
|
|
55
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
56
|
+
*/
|
|
57
|
+
frameAt_(scene) {
|
|
58
|
+
this.origin_ ??= Cartesian3.clone(scene.camera.positionWC);
|
|
59
|
+
const frame = this.localFrame_ ?? FixedFrameTransforms.eastNorthUpToFixedFrame(this.origin_, scene.ellipsoid, new Matrix4());
|
|
60
|
+
this.localFrame_ = frame;
|
|
61
|
+
return frame;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @override
|
|
66
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
67
|
+
*/
|
|
68
|
+
createStage_(scene) {
|
|
69
|
+
const stage = new PostProcessStage({
|
|
70
|
+
fragmentShader: EyeFromDepth + Height + Hash + Normal + RainRingsShader,
|
|
71
|
+
uniforms: {
|
|
72
|
+
...heightUniforms(scene),
|
|
73
|
+
localFromEye: () => localFromEye(this.frameAt_(scene), scene.camera.viewMatrix, this.localScratch_),
|
|
74
|
+
intensity: () => this.intensity_,
|
|
75
|
+
cell: () => this.cell_,
|
|
76
|
+
reach: () => this.reach_,
|
|
77
|
+
waterLevel: () => this.waterLevel_ ?? NO_WATER,
|
|
78
|
+
maxSlope: () => CesiumMath.toRadians(this.maxSlope_),
|
|
79
|
+
rate: () => this.rate_,
|
|
80
|
+
time: () => (performance.now() / 1000) % TIME_PERIOD,
|
|
81
|
+
color: () => Cartesian4.fromColor(this.color_, this.colorScratch_),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
// no pass, and no frames, without an intensity
|
|
85
|
+
stage.enabled = this.intensity_ > 0;
|
|
86
|
+
return stage;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @override
|
|
91
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
92
|
+
*/
|
|
93
|
+
activated_(scene) {
|
|
94
|
+
this.frameAt_(scene);
|
|
95
|
+
acquireTerrainDepth(scene);
|
|
96
|
+
if (this.intensity_ > 0) {
|
|
97
|
+
acquireFrames(scene, FRAME_RATE);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @override
|
|
103
|
+
* @param {import('@cesium/engine').Scene} scene
|
|
104
|
+
*/
|
|
105
|
+
deactivating_(scene) {
|
|
106
|
+
if (this.intensity_ > 0) {
|
|
107
|
+
releaseFrames(scene, FRAME_RATE);
|
|
108
|
+
}
|
|
109
|
+
releaseTerrainDepth(scene);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The share of the cells that have a drop, 0 (no rings, no frames) to 1.
|
|
114
|
+
*/
|
|
115
|
+
get intensity() {
|
|
116
|
+
return this.intensity_;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
set intensity(value) {
|
|
120
|
+
const raining = this.intensity_ > 0;
|
|
121
|
+
this.intensity_ = value;
|
|
122
|
+
if (this.stage_ && raining !== value > 0) {
|
|
123
|
+
this.stage_.enabled = value > 0;
|
|
124
|
+
(value > 0 ? acquireFrames : releaseFrames)(this.viewer.scene, FRAME_RATE);
|
|
125
|
+
}
|
|
126
|
+
this.viewer.scene.requestRender();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The size of a cell, in meters: one drop in each, as often as the intensity says.
|
|
131
|
+
*/
|
|
132
|
+
get cell() {
|
|
133
|
+
return this.cell_;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set cell(value) {
|
|
137
|
+
this.cell_ = value;
|
|
138
|
+
this.viewer.scene.requestRender();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* How far a ring grows on the ground, in meters: over water, 1.7 times as far, but never past 0.3 cell, or the edge of its cell would clip it.
|
|
143
|
+
*/
|
|
144
|
+
get reach() {
|
|
145
|
+
return this.reach_;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
set reach(value) {
|
|
149
|
+
this.reach_ = value;
|
|
150
|
+
this.viewer.scene.requestRender();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The water's height above the ellipsoid, in meters, or undefined for none: what lies within a quarter meter of it is water, where the rings
|
|
155
|
+
* are wider and slower.
|
|
156
|
+
*/
|
|
157
|
+
get waterLevel() {
|
|
158
|
+
return this.waterLevel_;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
set waterLevel(value) {
|
|
162
|
+
this.waterLevel_ = value;
|
|
163
|
+
this.viewer.scene.requestRender();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The steepest slope that takes a ring, in degrees.
|
|
168
|
+
*/
|
|
169
|
+
get maxSlope() {
|
|
170
|
+
return this.maxSlope_;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
set maxSlope(value) {
|
|
174
|
+
this.maxSlope_ = value;
|
|
175
|
+
this.viewer.scene.requestRender();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Rings a second in a cell.
|
|
180
|
+
*/
|
|
181
|
+
get rate() {
|
|
182
|
+
return this.rate_;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
set rate(value) {
|
|
186
|
+
this.rate_ = value;
|
|
187
|
+
this.viewer.scene.requestRender();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The ring's color, its alpha how strongly it shows. Slate on a light ground, pale on a dark one.
|
|
192
|
+
*/
|
|
193
|
+
get color() {
|
|
194
|
+
return this.color_;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
set color(value) {
|
|
198
|
+
this.color_ = value;
|
|
199
|
+
this.viewer.scene.requestRender();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The point of the world the cells are laid about, which the effect keeps where it is as the camera moves; the camera's position when
|
|
204
|
+
* the effect is activated if none is given. Keep it within a few kilometers of what is looked at.
|
|
205
|
+
*/
|
|
206
|
+
get origin() {
|
|
207
|
+
return this.origin_;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
set origin(value) {
|
|
211
|
+
this.origin_ = value && Cartesian3.clone(value);
|
|
212
|
+
this.localFrame_ = undefined;
|
|
213
|
+
this.viewer.scene.requestRender();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// This file is automatically rebuilt from Dufour.glsl.
|
|
2
|
+
export default "// The Dufour map's look, after EyeFromDepth, DepthEdges, Height, Normal and Hash: the slopes drawn in hachures, ink\n// strokes down the steepest slope, broken along their length, thicker where it is steeper and on the slopes away from a\n// light from the north-west, the flat ground left blank; the silhouettes outlined; black ink on white paper.\nprecision highp sampler2D;\nuniform sampler2D colorTexture;\n// the strokes' spacing on the screen, in pixels, their length in spacings, the ink's weight\nuniform float spacing;\nuniform float strokeLength;\nuniform float weight;\n// how much thinner the strokes facing the light, and thicker away from it, 0 to 1\nuniform float oblique;\n// the slope under which the ground is left blank, radians\nuniform float flatSlope;\nuniform float outline;\nuniform vec3 paper;\nuniform vec3 ink;\n// the hachures' frame, in eye coordinates: its origin on the ground below the camera, east and north there (dufour.js)\nuniform vec3 originEC;\nuniform vec3 eastEC;\nuniform vec3 northEC;\nin vec2 v_textureCoordinates;\n\n// the strokes' finest spacing, in meters\nconst float BASE = 1.0;\n// the strokes' directions: the way down the slope snapped to one of these many, the two nearest blended, so that the\n// strokes are straight lines, parallel on a slope, rather than turning with each pixel's normal\nconst float DIRECTIONS = 12.0;\n// a stroke's breaks: its length in spacings, strokeLength long on average, and the share of it left blank\nconst float BREAK = 0.08;\n// the light, from the north-west, in east and north\nconst vec2 LIGHT = vec2(-0.7071, 0.7071);\n// the ink's share grows with the slope, none on the flat, all of it at 60 degrees (radians), Lehmann's scale\n// stretched for the Alps, whose slopes are mostly past his 45; the\n// widest share of the spacing a stroke covers, the paper showing between them, an engraving's\nconst float LEHMANN = 1.047;\nconst float MAX_COVER = 0.45;\n// the slopes over which the strokes fade in above the flat ground, radians (2 degrees): the terrain's triangles,\n// slightly tilted, would show as patches\nconst float FLAT_RAMP = 0.035;\n\n// the strokes' ink at a point of the ground, en its east and north, s their spacing in meters, angle the direction\n// down the slope they run along, cover the share of the spacing they cover. Each stroke is a straight line along it,\n// broken every strokeLength spacings or so, its breaks staggered from the next stroke's: no line across the strokes\nfloat hachuresAt(vec2 en, float angle, vec2 across, float cover, float s) {\n vec2 down = vec2(-across.y, across.x);\n // a phase of each direction, so that two directions' strokes do not meet in a pattern\n float u = dot(en, across) / s + hash(vec2(angle, s));\n float stroke = floor(u);\n float w = dot(en, down) / (strokeLength * s) + hash(vec2(stroke, s));\n float dash = step(BREAK, fract(w * mix(0.8, 1.25, hash(vec2(s, stroke)))));\n // antialiased, and its mean, the share it covers, where the strokes grow too close to tell apart (a slope seen\n // edge on): no ink where it covers none\n float halfWidth = 0.5 * cover;\n float aa = fwidth(u);\n float sharp = 1.0 - smoothstep(halfWidth - aa, halfWidth + aa, abs(fract(u) - 0.5));\n return mix(sharp * dash, 2.0 * halfWidth * (1.0 - BREAK), smoothstep(0.2, 0.5, aa)) * step(1e-3, halfWidth);\n}\n\n// the strokes in one direction, their spacing a power of two of meters, crossfaded with the next, so that they stay at\n// least spacing pixels apart: taken from the ground a pixel covers across them, far more than its width where a slope\n// is seen at a grazing angle\nfloat directionAt(vec2 en, float angle, float cover) {\n vec2 across = vec2(-sin(angle), cos(angle));\n float lod = log2(max(spacing * fwidth(dot(en, across)), BASE) / BASE);\n float s = BASE * exp2(floor(lod));\n return mix(hachuresAt(en, angle, across, cover, s), hachuresAt(en, angle, across, cover, 2.0 * s), fract(lod));\n}\n\n// the strokes in both of the directions nearest the way down, blended\nfloat strokesAt(vec2 en, vec2 down, float cover) {\n // the strokes are lines: a direction and its opposite are the same\n float t = mod(atan(down.y, down.x), czm_pi) / czm_pi * DIRECTIONS;\n float bin = floor(t);\n float width = czm_pi / DIRECTIONS;\n return mix(directionAt(en, bin * width, cover), directionAt(en, (bin + 1.0) * width, cover), smoothstep(0.4, 0.6, fract(t)));\n}\n\nvoid main() {\n vec2 uv = v_textureCoordinates;\n vec4 sceneColor = texture(colorTexture, uv);\n vec4 eye = eyeAt(depthTexture, uv);\n if (eye.w == 0.0) {\n out_FragColor = vec4(paper, 1.0);\n return;\n }\n float px = czm_pixelRatio;\n // averaged wider than smoothNormalAt: the slopes of the terrain's triangles would set the strokes' width\n vec3 normal = normalize(normalAt(depthTexture, uv, eye.xyz, 4.0 * px) + normalAt(depthTexture, uv, eye.xyz, 12.0 * px) + normalAt(depthTexture, uv, eye.xyz, 24.0 * px));\n float rise = dot(normal, up);\n float slope = acos(clamp(rise, -1.0, 1.0));\n float c = depthAt(uv);\n\n // the way down the slope, the horizontal part of a normal smoothed wider: the strokes' direction would wobble with\n // the terrain's triangles\n vec3 wide = normalize(normal + normalAt(depthTexture, uv, eye.xyz, 32.0 * px));\n vec3 horizontal = wide - dot(wide, up) * up;\n vec2 down = normalize(vec2(dot(horizontal, eastEC), dot(horizontal, northEC)) + 1e-6);\n vec3 rel = eye.xyz - originEC;\n vec2 en = vec2(dot(rel, eastEC), dot(rel, northEC));\n float steep = smoothstep(flatSlope, flatSlope + FLAT_RAMP, slope) * clamp(slope / LEHMANN, 0.0, 1.0);\n float cover = clamp(weight * steep * (1.0 - oblique * dot(down, LIGHT)), 0.0, MAX_COVER);\n // read everywhere, outside any branch, for its derivatives\n float amount = strokesAt(en, down, cover);\n\n if (outline > 0.0) {\n amount = max(amount, outline * outlineAt(uv, c, vec4(0.0), 1.0, pixelAt(c)));\n }\n out_FragColor = vec4(mix(paper, ink, amount), sceneColor.a);\n}\n";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// This file is automatically rebuilt from RainRings.glsl.
|
|
2
|
+
export default "// Rings where the rain lands: the ground cut into cells of a local frame, in each cell a drop landing, as often as the intensity says, where the\n// cell's hash puts it, a thin ring growing from it and fading as it grows, on what is flat enough. Over water slower and wider. The cells are in a\n// local frame, in double precision from the CPU (local-frame.js), so they stay where they are as the camera moves and do not step with the globe's\n// size. Needs EyeFromDepth, Height, Hash and Normal.\nprecision highp sampler2D;\nuniform sampler2D colorTexture;\nuniform sampler2D depthTexture;\nuniform mat4 localFromEye;\n// 0 to 1: the share of the cells that have a drop\nuniform float intensity;\n// meters: a cell's size, and how far a ring on the ground grows\nuniform float cell;\nuniform float reach;\n// meters above the ellipsoid, far above anything with no water\nuniform float waterLevel;\n// radians\nuniform float maxSlope;\n// rings a second\nuniform float rate;\n// seconds\nuniform float time;\n// the color of a ring, its alpha how strongly it shows\nuniform vec4 color;\nin vec2 v_textureCoordinates;\n\n// a ring over water reaches this much farther, and runs this much slower\nconst float WATER_REACH = 1.7;\nconst float WATER_RATE = 0.5;\n// meters from the water's level within which a surface is water, and beyond which it is not\nconst vec2 WET = vec2(0.1, 0.25);\n// the drop lands in the middle of its cell, this far from its edge, in cells: a ring is clipped at the edge\nconst float MARGIN = 0.3;\n// the narrowest line, in meters\nconst float LINE = 0.045;\n// a pixel wider than the first, in meters, fades the rings out to none at the second: they would be a speckle\nconst vec2 SMALL = vec2(0.08, 0.2);\n// half the width of the slope limit, in radians\nconst float SLOPE_EDGE = 0.1;\n\nvoid main() {\n vec4 sceneColor = texture(colorTexture, v_textureCoordinates);\n vec4 eye = eyeAt(depthTexture, v_textureCoordinates);\n if (intensity <= 0.0 || eye.w == 0.0) {\n out_FragColor = sceneColor;\n return;\n }\n vec3 normal = smoothNormalAt(depthTexture, v_textureCoordinates, eye.xyz);\n float slope = acos(clamp(dot(normal, up), -1.0, 1.0));\n vec2 local = (localFromEye * vec4(eye.xyz, 1.0)).xy;\n float wet = 1.0 - smoothstep(WET.x, WET.y, abs(heightAt(eye.xyz) - waterLevel));\n\n vec2 id = floor(local / cell);\n vec2 centre = (id + MARGIN + (1.0 - 2.0 * MARGIN) * vec2(hash(id + 3.1), hash(id + 7.7))) * cell;\n float phase = fract(time * rate * mix(1.0, WATER_RATE, wet) + hash(id + 11.3));\n // never past the cell's edge from the centre\n float grown = min(reach * mix(1.0, WATER_REACH, wet), MARGIN * cell) * phase;\n float distance = length(local - centre);\n float width = max(LINE, 1.5 * fwidth(distance));\n float ring = (1.0 - smoothstep(0.0, width, abs(distance - grown))) * (1.0 - phase);\n ring *= float(hash(id) < intensity);\n ring *= 1.0 - smoothstep(maxSlope - SLOPE_EDGE, maxSlope + SLOPE_EDGE, slope);\n ring *= 1.0 - smoothstep(SMALL.x, SMALL.y, length(fwidth(local)));\n out_FragColor = vec4(mix(sceneColor.rgb, color.rgb, ring * color.a), sceneColor.a);\n}\n";
|
package/shaders/SnowLine.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is automatically rebuilt from SnowLine.glsl.
|
|
2
|
-
export default "// Snow above an altitude, on slopes gentle enough to hold it, shaded by the sun and by the\n// brightness of the scene so that the relief stays readable. The slope comes from a normal\n// rebuilt from the neighboring pixels and smoothed over the facets of the terrain. Noise anchored\n// to the ground breaks up the edge of the snow, which lasts lower on the slopes away from the sun.\nprecision highp sampler2D;\nuniform sampler2D colorTexture;\nuniform sampler2D depthTexture;\n// meters above the ellipsoid: half covered at the altitude, over a band transition meters high\nuniform float altitude;\nuniform float transition;\n// radians\nuniform float maxSlope;\n// 0 to 1\nuniform float coverage;\nin vec2 v_textureCoordinates;\n\nconst vec3 SNOW_SHADE = vec3(0.80, 0.87, 1.0);\nconst vec3 SNOW_SUN = vec3(1.0, 0.98, 0.94);\n// meters the snow line moves down on a slope facing away from the sun, and up on one facing it\nconst float ASPECT_SHIFT = 0.6;\n// meters the noise moves the snow line, as a fraction of the transition\nconst float NOISE_SHIFT = 0.8;\n// part of the snow that still covers the steep slopes at the limit, as a fraction of the coverage\nconst float STEEP_SNOW = 0.35;\n// half width of the slope limit, in radians (8 degrees), so that the snow thins out across the\n// facets of the terrain rather than stopping at their edges\nconst float SLOPE_EDGE = 0.14;\n\nfloat hash(vec3 p) {\n p = fract(p * 0.3183099 + 0.1);\n p *= 17.0;\n return fract(p.x * p.y * p.z * (p.x + p.y + p.z));\n}\n\nfloat valueNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),\n mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y),\n f.z);\n}\n\n// 0 to 1, from a few octaves anchored to the ground\nfloat groundNoise(vec3 world) {\n return 0.55 * valueNoise(world / 90.0) + 0.3 * valueNoise(world / 30.0) + 0.15 * valueNoise(world / 10.0);\n}\n\nvoid main() {\n vec4 sceneColor = texture(colorTexture, v_textureCoordinates);\n vec4 eye = eyeAt(depthTexture, v_textureCoordinates);\n vec3 normal = smoothNormalAt(depthTexture, v_textureCoordinates, eye.xyz);\n float slope = acos(clamp(dot(normal, up), -1.0, 1.0));\n\n float sun = max(dot(normal, czm_sunDirectionEC), 0.0);\n float noise = groundNoise((czm_inverseView * vec4(eye.xyz, 1.0)).xyz) - 0.5;\n // lower on the slopes away from the sun, irregular along the edge\n float shift = transition * (NOISE_SHIFT * noise + ASPECT_SHIFT * (0.5 - sun));\n float snowLine = altitude + shift;\n float snow = smoothstep(snowLine - 0.5 * transition, snowLine + 0.5 * transition, heightAt(eye.xyz));\n // opaque before the top of the band, so that the imagery does not show through the snow\n snow = smoothstep(0.0, 0.7, snow);\n // the steep slopes keep a thin, patchy cover, and the rock shows through\n float steep = smoothstep(maxSlope - SLOPE_EDGE, maxSlope + SLOPE_EDGE, slope);\n snow *= 1.0 - steep * (1.0 - STEEP_SNOW * smoothstep(0.35, 0.65, noise + 0.5));\n // eye.w is 0 for the sky\n snow *= coverage * eye.w;\n float brightness = czm_luminance(sceneColor.rgb);\n // blue in the shade, warm in the sun, with the relief of the scene kept in the brightness\n vec3 lit = mix(SNOW_SHADE, SNOW_SUN, sun) * clamp(0.68 + 0.25 * sun + 0.1 * brightness, 0.0, 1.0);\n out_FragColor = vec4(mix(sceneColor.rgb, lit, snow), sceneColor.a);\n}\n";
|
|
2
|
+
export default "// Snow above an altitude, on slopes gentle enough to hold it, shaded by the sun and by the\n// brightness of the scene so that the relief stays readable. The slope comes from a normal\n// rebuilt from the neighboring pixels and smoothed over the facets of the terrain. Noise anchored\n// to the ground breaks up the edge of the snow, which lasts lower on the slopes away from the sun.\nprecision highp sampler2D;\nuniform sampler2D colorTexture;\nuniform sampler2D depthTexture;\n// meters above the ellipsoid: half covered at the altitude, over a band transition meters high\nuniform float altitude;\nuniform float transition;\n// radians\nuniform float maxSlope;\n// 0 to 1\nuniform float coverage;\n// 0 to 1: how far the coverage thins the snow into patches, where 0 fades it\nuniform float patchiness;\n// meters above the ellipsoid, far above anything with no water: none lies within a quarter meter of it\nuniform float waterLevel;\n// the sun toward which the snow is shaded, in eye coordinates, when ownSun is 1; else Cesium's\nuniform vec3 sunEC;\nuniform float ownSun;\nin vec2 v_textureCoordinates;\n\nconst vec3 SNOW_SHADE = vec3(0.80, 0.87, 1.0);\nconst vec3 SNOW_SUN = vec3(1.0, 0.98, 0.94);\n// meters the snow line moves down on a slope facing away from the sun, and up on one facing it\nconst float ASPECT_SHIFT = 0.6;\n// meters the noise moves the snow line, as a fraction of the transition\nconst float NOISE_SHIFT = 0.8;\n// part of the snow that still covers the steep slopes at the limit, as a fraction of the coverage\nconst float STEEP_SNOW = 0.35;\n// half width of the slope limit, in radians (8 degrees), so that the snow thins out across the\n// facets of the terrain rather than stopping at their edges\nconst float SLOPE_EDGE = 0.14;\n// the coverage, moved by the noise, from which the snow begins and is whole when it is patches; how far the noise moves it\nconst vec2 PATCH = vec2(0.25, 0.6);\nconst float PATCH_NOISE = 1.6;\n// meters from the water's level within which a surface is water, and beyond which it is not\nconst vec2 WET = vec2(0.1, 0.25);\n\nfloat hash(vec3 p) {\n p = fract(p * 0.3183099 + 0.1);\n p *= 17.0;\n return fract(p.x * p.y * p.z * (p.x + p.y + p.z));\n}\n\nfloat valueNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),\n mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y),\n f.z);\n}\n\n// 0 to 1, from a few octaves anchored to the ground\nfloat groundNoise(vec3 world) {\n return 0.55 * valueNoise(world / 90.0) + 0.3 * valueNoise(world / 30.0) + 0.15 * valueNoise(world / 10.0);\n}\n\nvoid main() {\n vec4 sceneColor = texture(colorTexture, v_textureCoordinates);\n vec4 eye = eyeAt(depthTexture, v_textureCoordinates);\n vec3 normal = smoothNormalAt(depthTexture, v_textureCoordinates, eye.xyz);\n float slope = acos(clamp(dot(normal, up), -1.0, 1.0));\n\n float sun = max(dot(normal, ownSun > 0.5 ? sunEC : czm_sunDirectionEC), 0.0);\n float noise = groundNoise((czm_inverseView * vec4(eye.xyz, 1.0)).xyz) - 0.5;\n // lower on the slopes away from the sun, irregular along the edge\n float shift = transition * (NOISE_SHIFT * noise + ASPECT_SHIFT * (0.5 - sun));\n float snowLine = altitude + shift;\n float snow = smoothstep(snowLine - 0.5 * transition, snowLine + 0.5 * transition, heightAt(eye.xyz));\n // opaque before the top of the band, so that the imagery does not show through the snow\n snow = smoothstep(0.0, 0.7, snow);\n // the steep slopes keep a thin, patchy cover, and the rock shows through\n float steep = smoothstep(maxSlope - SLOPE_EDGE, maxSlope + SLOPE_EDGE, slope);\n snow *= 1.0 - steep * (1.0 - STEEP_SNOW * smoothstep(0.35, 0.65, noise + 0.5));\n // the coverage an opacity, or, by the patchiness, the share of the ground the snow covers, the noise leaving it in patches\n float scattered = smoothstep(PATCH.x, PATCH.y, coverage + PATCH_NOISE * noise);\n // eye.w is 0 for the sky\n snow *= mix(coverage, scattered, patchiness) * eye.w;\n snow *= smoothstep(WET.x, WET.y, abs(heightAt(eye.xyz) - waterLevel));\n float brightness = czm_luminance(sceneColor.rgb);\n // blue in the shade, warm in the sun, with the relief of the scene kept in the brightness\n vec3 lit = mix(SNOW_SHADE, SNOW_SUN, sun) * clamp(0.68 + 0.25 * sun + 0.1 * brightness, 0.0, 1.0);\n out_FragColor = vec4(mix(sceneColor.rgb, lit, snow), sceneColor.a);\n}\n";
|
package/snow-line.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Math as CesiumMath} from '@cesium/core';
|
|
1
|
+
import {Cartesian3, Math as CesiumMath, Matrix4} from '@cesium/core';
|
|
2
2
|
import {PostProcessStage} from '@cesium/engine';
|
|
3
3
|
import {acquireTerrainDepth, releaseTerrainDepth} from './depth-test.js';
|
|
4
4
|
import Effect from './effect.js';
|
|
@@ -8,6 +8,11 @@ import Height from './shaders/Height.js';
|
|
|
8
8
|
import Normal from './shaders/Normal.js';
|
|
9
9
|
import SnowLineShader from './shaders/SnowLine.js';
|
|
10
10
|
|
|
11
|
+
// the water's level when there is no water: far above everything
|
|
12
|
+
const NO_WATER = 1e9;
|
|
13
|
+
|
|
14
|
+
const sunScratch = new Cartesian3();
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Snow above an altitude, on the slopes gentle enough to hold it, shaded by
|
|
13
18
|
* the sun.
|
|
@@ -15,7 +20,8 @@ import SnowLineShader from './shaders/SnowLine.js';
|
|
|
15
20
|
export default class SnowLine extends Effect {
|
|
16
21
|
/**
|
|
17
22
|
* @param {import('@cesium/engine').CesiumWidget} viewer
|
|
18
|
-
* @param {{altitude?: number, transition?: number, maxSlope?: number, coverage?: number
|
|
23
|
+
* @param {{altitude?: number, transition?: number, maxSlope?: number, coverage?: number, patchiness?: number, waterLevel?: number,
|
|
24
|
+
* sunDirection?: Cartesian3}} [options]
|
|
19
25
|
*/
|
|
20
26
|
constructor(viewer, options = {}) {
|
|
21
27
|
super(viewer);
|
|
@@ -23,6 +29,9 @@ export default class SnowLine extends Effect {
|
|
|
23
29
|
this.transition_ = options.transition ?? 200;
|
|
24
30
|
this.maxSlope_ = options.maxSlope ?? 50;
|
|
25
31
|
this.coverage_ = options.coverage ?? 1;
|
|
32
|
+
this.patchiness_ = options.patchiness ?? 0;
|
|
33
|
+
this.waterLevel_ = options.waterLevel;
|
|
34
|
+
this.sunDirection_ = options.sunDirection && Cartesian3.normalize(options.sunDirection, new Cartesian3());
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
@@ -38,6 +47,10 @@ export default class SnowLine extends Effect {
|
|
|
38
47
|
transition: () => this.transition_,
|
|
39
48
|
maxSlope: () => CesiumMath.toRadians(this.maxSlope_),
|
|
40
49
|
coverage: () => this.coverage_,
|
|
50
|
+
patchiness: () => this.patchiness_,
|
|
51
|
+
waterLevel: () => this.waterLevel_ ?? NO_WATER,
|
|
52
|
+
sunEC: () => this.sunDirection_ ? Matrix4.multiplyByPointAsVector(scene.camera.viewMatrix, this.sunDirection_, sunScratch) : sunScratch,
|
|
53
|
+
ownSun: () => this.sunDirection_ ? 1 : 0,
|
|
41
54
|
},
|
|
42
55
|
});
|
|
43
56
|
}
|
|
@@ -105,4 +118,43 @@ export default class SnowLine extends Effect {
|
|
|
105
118
|
this.coverage_ = value;
|
|
106
119
|
this.viewer.scene.requestRender();
|
|
107
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* How far the coverage thins the snow into patches, 0 to 1: at 0 the coverage is the snow's opacity, at 1 the share of the ground it
|
|
124
|
+
* covers, the noise leaving the rest bare.
|
|
125
|
+
*/
|
|
126
|
+
get patchiness() {
|
|
127
|
+
return this.patchiness_;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set patchiness(value) {
|
|
131
|
+
this.patchiness_ = value;
|
|
132
|
+
this.viewer.scene.requestRender();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The water's height above the ellipsoid, in meters, or undefined for none: what lies within a quarter meter of it holds no snow,
|
|
137
|
+
* so that a flat lake stays water.
|
|
138
|
+
*/
|
|
139
|
+
get waterLevel() {
|
|
140
|
+
return this.waterLevel_;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set waterLevel(value) {
|
|
144
|
+
this.waterLevel_ = value;
|
|
145
|
+
this.viewer.scene.requestRender();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The direction toward the sun that the snow is shaded by, in world coordinates, or undefined for Cesium's own sun. For a scene lit by a
|
|
150
|
+
* light of its own: `scene.light.direction` is the way the light travels, so give its negation.
|
|
151
|
+
*/
|
|
152
|
+
get sunDirection() {
|
|
153
|
+
return this.sunDirection_;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
set sunDirection(value) {
|
|
157
|
+
this.sunDirection_ = value && Cartesian3.normalize(value, new Cartesian3());
|
|
158
|
+
this.viewer.scene.requestRender();
|
|
159
|
+
}
|
|
108
160
|
}
|