@geoblocks/cesium-post-process 0.1.3 → 0.2.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 CHANGED
@@ -18,7 +18,7 @@ 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, 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
@@ -112,6 +112,17 @@ const whiteModel = new WhiteModel(viewer, {
112
112
  });
113
113
  whiteModel.active = true;
114
114
 
115
+ // or, in Toy's place, the Dufour map's hachures
116
+ const dufour = new Dufour(viewer, {
117
+ spacing: 3.5, // the strokes' spacing on the screen, in pixels
118
+ strokeLength: 14, // a stroke's length between its breaks, in spacings, on average
119
+ 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
120
+ oblique: 0.8, // how much thinner the strokes facing the north-west light, and thicker away from it, 0 to 1
121
+ flatSlope: 4, // the slope under which the ground is left blank, in degrees
122
+ outline: 0.8, // how dark the silhouettes' outline; 0 for none
123
+ });
124
+ dufour.active = true;
125
+
115
126
  const tiltShift = new TiltShift(viewer, {
116
127
  focus,
117
128
  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 +208,33 @@ super8.grain = 0.3;
197
208
  super8.destroy();
198
209
  ```
199
210
 
200
- ### Shader chunk
211
+ ### Shader chunks
201
212
 
202
213
  `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
214
 
215
+ `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:
216
+
217
+ ```js
218
+ import {EyeFromDepth, Height, heightUniforms} from '@geoblocks/cesium-post-process';
219
+
220
+ const stage = new PostProcessStage({
221
+ fragmentShader: EyeFromDepth + Height + myShader, // myShader calls eyeAt and heightAt
222
+ uniforms: {...heightUniforms(scene), myUniform: () => value},
223
+ });
224
+ ```
225
+
226
+ `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.
227
+
204
228
  ### Order of the effects
205
229
 
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 `WhiteModel`, `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 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.
230
+ 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`, `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 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
231
 
208
232
  ### Notes
209
233
 
210
234
  `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
235
 
236
+ `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.
237
+
212
238
  `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
239
 
214
240
  `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.
@@ -231,7 +257,7 @@ Its `snow`, from 0 to 1, turns the rain to snow in one population, so that nothi
231
257
 
232
258
  `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.
233
259
 
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.
260
+ `SnowLine`, `ValleyFog`, `Precipitation`, `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
261
 
236
262
  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
263
 
@@ -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';
@@ -19,5 +20,8 @@ export {default as Toy} from './toy.js';
19
20
  export {default as ValleyFog} from './valley-fog.js';
20
21
  export {default as WhiteModel} from './white-model.js';
21
22
 
22
- // the shader chunk the depth-based effects share, for effects of your own
23
+ // the shader chunks the depth-based effects share, and the uniforms of Height, for effects of your own
23
24
  export {default as EyeFromDepth} from './shaders/EyeFromDepth.js';
25
+ export {default as Height} from './shaders/Height.js';
26
+ export {heightUniforms} from './height.js';
27
+ 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geoblocks/cesium-post-process",
3
- "version": "0.1.3",
3
+ "version": "0.2.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",
@@ -53,5 +54,5 @@
53
54
  "@cesium/core": ">=0.1.0",
54
55
  "@cesium/engine": ">=10"
55
56
  },
56
- "gitHead": "5c6ee12b4e35f46ff733cf628da105f55121fbf3"
57
+ "gitHead": "71fc611a0eb4caf6ff6d99cbbab64f266fa73dd6"
57
58
  }
@@ -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";