@geoblocks/cesium-binoculars 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cesium-binoculars.js +170 -22
- package/package.json +9 -2
- package/shaders/EyeFromDepth.js +2 -0
- package/shaders/Lens.js +2 -0
package/cesium-binoculars.js
CHANGED
|
@@ -1,16 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Cartesian2,
|
|
3
|
+
Cartesian3,
|
|
4
|
+
Math as CesiumMath,
|
|
5
|
+
} from "@cesium/core";
|
|
6
|
+
import {
|
|
7
|
+
CameraEventType,
|
|
8
|
+
PostProcessStage,
|
|
9
|
+
PostProcessStageComposite,
|
|
10
|
+
PostProcessStageLibrary,
|
|
11
|
+
ScreenSpaceEventType,
|
|
12
|
+
} from "@cesium/engine";
|
|
13
|
+
import EyeFromDepth from "./shaders/EyeFromDepth.js";
|
|
14
|
+
import Lens from "./shaders/Lens.js";
|
|
15
|
+
|
|
16
|
+
// blur of the depth of field and of the rim, the sigma of Cesium's blur stage
|
|
17
|
+
const LENS_BLUR = 3;
|
|
18
|
+
|
|
19
|
+
const middleScratch = new Cartesian2();
|
|
2
20
|
|
|
3
21
|
export default class CesiumBinoculars {
|
|
4
22
|
/**
|
|
5
|
-
* @param {import('cesium').
|
|
6
|
-
* @param {number} [zoomFactor]
|
|
23
|
+
* @param {import('@cesium/engine').CesiumWidget} viewer
|
|
24
|
+
* @param {number} [zoomFactor] magnification per mouse wheel notch
|
|
25
|
+
* @param {number} [maxMagnification]
|
|
7
26
|
*/
|
|
8
|
-
constructor(viewer, zoomFactor =
|
|
27
|
+
constructor(viewer, zoomFactor = 1.25, maxMagnification = 20) {
|
|
9
28
|
this.viewer = viewer;
|
|
10
29
|
this.zoomFactor_ = zoomFactor;
|
|
30
|
+
this.maxMagnification = maxMagnification;
|
|
11
31
|
this.originalFov_ = undefined;
|
|
12
32
|
this.active_ = false;
|
|
13
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @type {Pick<import('@cesium/engine').ScreenSpaceCameraController, 'enableZoom' | 'enableRotate' | 'enableTilt' | 'lookEventTypes'> | undefined}
|
|
36
|
+
*/
|
|
37
|
+
this.previousController_ = undefined;
|
|
38
|
+
/**
|
|
39
|
+
* @type {ReturnType<import('@cesium/engine').ScreenSpaceEventHandler['getInputAction']> | undefined}
|
|
40
|
+
*/
|
|
41
|
+
this.previousAction_ = undefined;
|
|
42
|
+
/** @type {PostProcessStageComposite | undefined} */
|
|
43
|
+
this.lens_ = undefined;
|
|
44
|
+
this.previousDepthTestAgainstTerrain_ = false;
|
|
45
|
+
this.reticle_ = false;
|
|
46
|
+
this.onPreRender_ = this.onPreRender.bind(this);
|
|
14
47
|
this.onMouseWheel_ = this.onMouseWheel.bind(this);
|
|
15
48
|
}
|
|
16
49
|
|
|
@@ -23,30 +56,145 @@ export default class CesiumBinoculars {
|
|
|
23
56
|
return;
|
|
24
57
|
}
|
|
25
58
|
this.active_ = active;
|
|
26
|
-
this.viewer.scene.screenSpaceCameraController
|
|
27
|
-
const
|
|
59
|
+
const controller = this.viewer.scene.screenSpaceCameraController;
|
|
60
|
+
const handler = this.viewer.screenSpaceEventHandler;
|
|
28
61
|
if (active) {
|
|
29
|
-
this.originalFov_ =
|
|
30
|
-
|
|
62
|
+
this.originalFov_ = this.frustum_.fov;
|
|
63
|
+
const {enableZoom, enableRotate, enableTilt, lookEventTypes} = controller;
|
|
64
|
+
this.previousController_ = {enableZoom, enableRotate, enableTilt, lookEventTypes};
|
|
65
|
+
// stand still: a drag turns the camera in place instead of moving it around the globe
|
|
66
|
+
controller.enableZoom = false;
|
|
67
|
+
controller.enableRotate = false;
|
|
68
|
+
controller.enableTilt = false;
|
|
69
|
+
controller.lookEventTypes = CameraEventType.LEFT_DRAG;
|
|
70
|
+
this.previousAction_ = handler.getInputAction(ScreenSpaceEventType.WHEEL);
|
|
71
|
+
handler.setInputAction(this.onMouseWheel_, ScreenSpaceEventType.WHEEL);
|
|
72
|
+
this.addLens_();
|
|
73
|
+
this.viewer.scene.preRender.addEventListener(this.onPreRender_);
|
|
31
74
|
} else {
|
|
32
|
-
|
|
33
|
-
this.
|
|
75
|
+
this.viewer.scene.preRender.removeEventListener(this.onPreRender_);
|
|
76
|
+
this.removeLens_();
|
|
77
|
+
this.frustum_.fov = this.originalFov_;
|
|
78
|
+
Object.assign(controller, this.previousController_);
|
|
79
|
+
if (this.previousAction_) {
|
|
80
|
+
handler.setInputAction(this.previousAction_, ScreenSpaceEventType.WHEEL);
|
|
81
|
+
} else {
|
|
82
|
+
handler.removeInputAction(ScreenSpaceEventType.WHEEL);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// requestRenderMode: adding or removing the lens stage does not request a render
|
|
86
|
+
this.viewer.scene.requestRender();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
addLens_() {
|
|
90
|
+
const scene = this.viewer.scene;
|
|
91
|
+
// the post-process stages only get the depth of the terrain when it is depth tested
|
|
92
|
+
if (scene.globe) {
|
|
93
|
+
this.previousDepthTestAgainstTerrain_ = scene.globe.depthTestAgainstTerrain;
|
|
94
|
+
scene.globe.depthTestAgainstTerrain = true;
|
|
95
|
+
}
|
|
96
|
+
const blur = PostProcessStageLibrary.createBlurStage();
|
|
97
|
+
this.lens_ = new PostProcessStageComposite({
|
|
98
|
+
stages: [
|
|
99
|
+
blur,
|
|
100
|
+
new PostProcessStage({
|
|
101
|
+
fragmentShader: EyeFromDepth + Lens,
|
|
102
|
+
uniforms: {
|
|
103
|
+
blurTexture: blur.name,
|
|
104
|
+
magnification: () => this.magnification,
|
|
105
|
+
reticle: () => (this.reticle_ ? 1 : 0),
|
|
106
|
+
pixelsPerMil: () => (scene.drawingBufferHeight / 2 / Math.tan(/** @type {number} */ (this.frustum_.fovy) / 2)) * 0.001,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
// both read the scene
|
|
111
|
+
inputPreviousStageTexture: false,
|
|
112
|
+
uniforms: blur.uniforms,
|
|
113
|
+
});
|
|
114
|
+
this.lens_.uniforms.sigma = LENS_BLUR;
|
|
115
|
+
scene.postProcessStages.add(this.lens_);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
removeLens_() {
|
|
119
|
+
const scene = this.viewer.scene;
|
|
120
|
+
// removing a stage also destroys it
|
|
121
|
+
scene.postProcessStages.remove(/** @type {PostProcessStageComposite} */ (this.lens_));
|
|
122
|
+
this.lens_ = undefined;
|
|
123
|
+
if (scene.globe) {
|
|
124
|
+
scene.globe.depthTestAgainstTerrain = this.previousDepthTestAgainstTerrain_;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether to draw a rangefinder reticle: a mil scale (milliradians) that
|
|
130
|
+
* grows with the magnification, so an object's size is its distance times
|
|
131
|
+
* the mils it covers, divided by 1000.
|
|
132
|
+
*/
|
|
133
|
+
get reticle() {
|
|
134
|
+
return this.reticle_;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
set reticle(value) {
|
|
138
|
+
this.reticle_ = value;
|
|
139
|
+
this.viewer.scene.requestRender();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Distance in meters from the camera to what is in the middle of the view,
|
|
144
|
+
* terrain or 3D Tiles; undefined for the sky, when inactive or when picking
|
|
145
|
+
* positions is not supported. Picked when read.
|
|
146
|
+
*/
|
|
147
|
+
get distance() {
|
|
148
|
+
const scene = this.viewer.scene;
|
|
149
|
+
if (!this.active_ || !scene.pickPositionSupported) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const canvas = scene.canvas;
|
|
153
|
+
const middle = Cartesian2.fromElements(canvas.clientWidth / 2, canvas.clientHeight / 2, middleScratch);
|
|
154
|
+
const position = scene.pickPosition(middle);
|
|
155
|
+
return position && Cartesian3.distance(scene.camera.positionWC, position);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get frustum_() {
|
|
159
|
+
return /** @type {import('@cesium/core').PerspectiveFrustum} */ (this.viewer.scene.camera.frustum);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Magnification relative to the field of view when the binoculars were activated, 1 when inactive.
|
|
164
|
+
* The magnification is proportional to 1 / tan(fov / 2).
|
|
165
|
+
*/
|
|
166
|
+
get magnification() {
|
|
167
|
+
if (!this.active_) {
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
return Math.tan(/** @type {number} */ (this.originalFov_) / 2) / Math.tan(/** @type {number} */ (this.frustum_.fov) / 2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Only applies while active; clamped between 1 and maxMagnification.
|
|
175
|
+
*/
|
|
176
|
+
set magnification(magnification) {
|
|
177
|
+
if (!this.active_) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const clamped = CesiumMath.clamp(magnification, 1, this.maxMagnification);
|
|
181
|
+
this.frustum_.fov = 2 * Math.atan(Math.tan(/** @type {number} */ (this.originalFov_) / 2) / clamped);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* In 3D, Cesium's look turns the camera around its own axes, which rolls the view: keep the horizon level.
|
|
186
|
+
*/
|
|
187
|
+
onPreRender() {
|
|
188
|
+
const camera = this.viewer.scene.camera;
|
|
189
|
+
if (Math.abs(CesiumMath.negativePiToPi(camera.roll)) > CesiumMath.EPSILON6) {
|
|
190
|
+
camera.setView({orientation: {heading: camera.heading, pitch: camera.pitch, roll: 0}});
|
|
34
191
|
}
|
|
35
192
|
}
|
|
36
193
|
|
|
37
194
|
/**
|
|
38
|
-
* @param {number}
|
|
195
|
+
* @param {number} delta 120 per mouse wheel notch, positive to zoom in; trackpads send smaller values
|
|
39
196
|
*/
|
|
40
|
-
onMouseWheel(
|
|
41
|
-
|
|
42
|
-
this.viewer.scene.camera.frustum
|
|
43
|
-
);
|
|
44
|
-
const fov =
|
|
45
|
-
frustum.fov + (movement > 0 ? -this.zoomFactor_ : this.zoomFactor_);
|
|
46
|
-
frustum.fov = CesiumMath.clamp(
|
|
47
|
-
fov,
|
|
48
|
-
CesiumMath.toRadians(1),
|
|
49
|
-
CesiumMath.toRadians(60)
|
|
50
|
-
);
|
|
197
|
+
onMouseWheel(delta) {
|
|
198
|
+
this.magnification *= Math.pow(this.zoomFactor_, delta / 120);
|
|
51
199
|
}
|
|
52
200
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geoblocks/cesium-binoculars",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "https://github.com/geoblocks/cesium-helpers/",
|
|
@@ -9,11 +10,17 @@
|
|
|
9
10
|
},
|
|
10
11
|
"main": "cesium-binoculars.js",
|
|
11
12
|
"module": "cesium-binoculars.js",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"files": [
|
|
15
|
+
"cesium-binoculars.js",
|
|
16
|
+
"shaders/*.js"
|
|
17
|
+
],
|
|
12
18
|
"publishConfig": {
|
|
13
19
|
"access": "public"
|
|
14
20
|
},
|
|
15
21
|
"peerDependencies": {
|
|
22
|
+
"@cesium/core": ">=0.1.0",
|
|
16
23
|
"@cesium/engine": ">=10"
|
|
17
24
|
},
|
|
18
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "430bbbb63bf3d60221d670f65d5e51fdb6d5b944"
|
|
19
26
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// This file is automatically rebuilt from EyeFromDepth.glsl.
|
|
2
|
+
export default "// Eye coordinates of the pixel at uv, from the depth texture; w is 0 for the sky. With a\n// logarithmic depth buffer, czm_readDepth's perspective depth is about 1 beyond a few meters,\n// so the distance is decoded from the logarithmic depth directly.\nprecision highp sampler2D;\nvec4 eyeAt(sampler2D depthTexture, vec2 uv) {\n float raw = texture(depthTexture, uv).r;\n // the pixel's view ray, through the near plane\n vec4 ray = czm_inverseProjection * vec4(2.0 * uv - 1.0, -1.0, 1.0);\n ray.xyz /= ray.w;\n if (raw >= 1.0) {\n return vec4(normalize(ray.xyz), 0.0);\n }\n#ifdef LOG_DEPTH\n float viewDepth = exp2(raw * czm_log2FarDepthFromNearPlusOne) - 1.0 + czm_currentFrustum.x;\n#else\n vec4 eye = czm_inverseProjection * vec4(2.0 * uv - 1.0, 2.0 * raw - 1.0, 1.0);\n float viewDepth = -eye.z / eye.w;\n#endif\n return vec4(ray.xyz * (viewDepth / -ray.z), 1.0);\n}\n";
|
package/shaders/Lens.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// This file is automatically rebuilt from Lens.glsl.
|
|
2
|
+
export default "// The eyepieces, two overlapping circles merged into one wide opening in a 160x90 box fitted to\n// the canvas, near black around them, with an uneven edge. Inside, lens effects: a slight fisheye\n// and color fringes toward the rim, both stronger at high magnification, the light falling off and\n// the image softening toward the rim, a violet and orange edge, specks of dirt on the glass, and\n// a depth of field focused on the middle of the view that gets shallower as the magnification\n// grows. Optionally, a rangefinder reticle, a mil scale that grows with the magnification. Picking\n// ignores the fisheye, so near the rim the picked point is a little off what is shown.\nprecision highp sampler2D;\nuniform sampler2D colorTexture;\nuniform sampler2D blurTexture;\nuniform sampler2D depthTexture;\nuniform float magnification;\n// 1 to draw the reticle, 0 not to\nuniform float reticle;\n// device pixels per milliradian, from the field of view\nuniform float pixelsPerMil;\nin vec2 v_textureCoordinates;\n\nconst vec2 BOX = vec2(160.0, 90.0);\nconst vec2 LEFT = vec2(55.2, 45.0);\nconst vec2 RIGHT = vec2(104.8, 45.0);\nconst float RADIUS = 43.3;\n// how much the notches between the eyepieces fill in, in box units\nconst float MERGE = 6.0;\n// half width of the soft edge of the eyepieces, in box units\nconst float EDGE = 3.5;\n// around the eyepieces\nconst vec3 SURROUND = vec3(0.02);\n// the edge of the opening for red is this much further right, and for blue further left, in box\n// units, so it fades through violet on the left and orange on the right\nconst float EDGE_SHIFT = 2.0;\n// the edge wanders in and out by up to this much, in box units, with bumps about BUMP apart\nconst float UNEVEN = 1.2;\nconst float BUMP = 9.0;\n// specks of dirt on the glass: cells per canvas height, share of cells with a speck, how dark\nconst float DIRT_CELLS = 45.0;\nconst float DIRT_DENSITY = 0.015;\nconst float DIRT = 0.55;\n// the effects only apply within this distance of the rim, in box units, so the middle of the\n// view, where the eyepieces overlap, stays clean\nconst float BAND = 20.0;\n// fisheye: on the rim, sample up to this fraction further from the center, at 5x\nconst float FISHEYE = 0.07;\n// color fringe on the rim, in box units, at 5x\nconst float FRINGE = 1.0;\n// light lost on the rim of an eyepiece\nconst float VIGNETTE = 0.25;\n// a ring this wide inside the edge, in box units, where the glass bends the light more: it starts\n// abruptly and shows the image from RING_PULL further in, blurred, with a wider color fringe\nconst float RING = 4.5;\nconst float RING_PULL = 1.5;\nconst float RING_FRINGE = 0.15;\n// blur on the rim, off the optical axis\nconst float RIM_BLUR = 1.0;\n// sharp within this many factors of two of the focal distance at 1x, narrower as 1 / magnification\nconst float FOCUS_RANGE = 2.0;\n\n// ticks of the reticle closer than this, in CSS pixels, are left out for every 5th or 10th\nconst float MIN_TICK_SPACING = 6.0;\n// ticks on each side of the middle, and the empty gap in the middle, in ticks\nconst float TICKS = 10.0;\nconst float GAP = 1.0;\n// dark lines in a light halo, readable on dark forest as on snow\nconst vec4 RETICLE = vec4(0.05, 0.05, 0.05, 0.9);\nconst vec4 HALO = vec4(0.9, 0.9, 0.85, 0.6);\n\n// coverage of a line this far from the pixel, width pixels wide\nfloat line(float distance, float width) {\n return 1.0 - smoothstep(0.5 * width - 0.5, 0.5 * width + 0.5, distance);\n}\n\n// coverage of the reticle at a pixel, in device pixels from the middle of the view, with lines\n// width device pixels wide: a cross of mil scales, ticks every 1, 5 or 10 mils, longer every 5th\nfloat reticleAt(vec2 p, float width) {\n float minSpacing = MIN_TICK_SPACING * czm_pixelRatio;\n float spacing = pixelsPerMil;\n if (spacing < minSpacing) {\n spacing *= 5.0;\n }\n if (spacing < minSpacing) {\n spacing *= 2.0;\n }\n float coverage = 0.0;\n // one axis at a time: along is the distance along the scale, across the distance from it\n for (int axis = 0; axis < 2; axis++) {\n float along = axis == 0 ? p.x : p.y;\n float across = axis == 0 ? p.y : p.x;\n float fromMiddle = abs(along);\n if (fromMiddle < GAP * spacing || fromMiddle > TICKS * spacing + width) {\n continue;\n }\n float tick = floor(fromMiddle / spacing + 0.5);\n float tickLength = (mod(tick, 5.0) == 0.0 ? 6.0 : 3.0) * czm_pixelRatio + 0.5 * width;\n coverage = max(coverage, line(abs(across), width));\n if (abs(across) < tickLength) {\n coverage = max(coverage, line(abs(fromMiddle - tick * spacing), width));\n }\n }\n return coverage;\n}\n\nfloat hash(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\n// coverage of the dirt at a pixel, in canvas heights: at most one small speck per cell, some\n// with a smaller one next to it, so they are not all round\nfloat dirtAt(vec2 p) {\n // before the branch: derivatives are undefined where neighboring pixels take different ones,\n // and not from inCell, which jumps at the cell borders\n float soft = 0.5 * fwidth(p.x * DIRT_CELLS);\n vec2 cell = floor(p * DIRT_CELLS);\n if (hash(cell) > DIRT_DENSITY) {\n return 0.0;\n }\n vec2 inCell = fract(p * DIRT_CELLS);\n vec2 center = 0.35 + 0.3 * vec2(hash(cell + 17.0), hash(cell + 31.0));\n float radius = 0.04 + 0.12 * hash(cell + 47.0) * hash(cell + 53.0);\n vec2 satellite = center + (vec2(hash(cell + 61.0), hash(cell + 67.0)) - 0.5) * 3.0 * radius;\n float speck = 1.0 - smoothstep(radius - soft, radius + soft, length(inCell - center));\n float small = 1.0 - smoothstep(0.6 * radius - soft, 0.6 * radius + soft, length(inCell - satellite));\n // some specks darker than others\n return (0.4 + 0.6 * hash(cell + 79.0)) * max(speck, small * step(0.5, hash(cell + 71.0)));\n}\n\n// smooth noise between -1 and 1, one bump per unit\nfloat noise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n float a = hash(i);\n float b = hash(i + vec2(1.0, 0.0));\n float c = hash(i + vec2(0.0, 1.0));\n float d = hash(i + vec2(1.0, 1.0));\n return 2.0 * mix(mix(a, b, f.x), mix(c, d, f.x), f.y) - 1.0;\n}\n\n// distance in box units to the edge of the opening, negative inside: a smooth union of the two\n// circles, which fills in the notches between them, made uneven\nfloat toRimAt(vec2 position) {\n float toLeftRim = length(position - LEFT) - RADIUS;\n float toRightRim = length(position - RIGHT) - RADIUS;\n float h = clamp(0.5 + 0.5 * (toRightRim - toLeftRim) / MERGE, 0.0, 1.0);\n float toRim = mix(toRightRim, toLeftRim, h) - MERGE * h * (1.0 - h);\n vec2 p = position / BUMP;\n return toRim + UNEVEN * (0.7 * noise(p) + 0.3 * noise(2.7 * p + 5.0));\n}\n\n// offsets in box units for one eyepiece: xy the fisheye, zw the color fringe\nvec4 lensOffsets(vec2 toCenter, float strength) {\n float fromCenter = length(toCenter);\n // 0 up to the band, 1 on the rim\n float rim = 1.0 - clamp((RADIUS - fromCenter) / BAND, 0.0, 1.0);\n vec2 direction = toCenter / max(fromCenter, 1e-3);\n return strength * vec4(toCenter * FISHEYE * rim * rim, direction * FRINGE * rim * rim * rim);\n}\n\n// the scene at uv, blurred by the depth of field around the focal distance and toward the rim\nvec4 sampleAt(vec2 uv, float focalDistance, float rimBlur) {\n vec4 eye = eyeAt(depthTexture, uv);\n float fromCamera = eye.w == 0.0 ? 1e30 : length(eye.xyz);\n float range = FOCUS_RANGE / magnification;\n float defocus = focalDistance > 0.0\n ? smoothstep(range, 2.0 * range, abs(log2(fromCamera / focalDistance)))\n : 0.0;\n // no depth of field at 1x, fully from 3x\n defocus *= smoothstep(1.0, 3.0, magnification);\n return mix(texture(colorTexture, uv), texture(blurTexture, uv), max(defocus, rimBlur));\n}\n\nvoid main() {\n vec2 size = czm_viewport.zw;\n float scale = min(size.x / BOX.x, size.y / BOX.y);\n vec2 position = (v_textureCoordinates * size - 0.5 * (size - scale * BOX)) / scale;\n vec2 toLeft = position - LEFT;\n vec2 toRight = position - RIGHT;\n\n // cheap optics show their flaws more when zoomed in: 0.6 at 1x, 1 at 5x, 2 at 20x and beyond\n float strength = clamp(0.5 + magnification / 10.0, 0.6, 2.0);\n vec4 left = lensOffsets(toLeft, strength);\n vec4 right = lensOffsets(toRight, strength);\n // blend the two eyepieces where they overlap, so there is no seam between them\n float weight = smoothstep(-6.0, 6.0, length(toRight) - length(toLeft));\n float toRim = toRimAt(position);\n float ring = smoothstep(-RING, -RING + 1.5, toRim);\n vec2 outward = normalize(mix(toRight, toLeft, weight));\n vec4 offsets = (mix(right, left, weight) + ring * vec4(-RING_PULL * outward, RING_FRINGE * outward)) * scale / vec4(size, size);\n\n // focused on what is in the middle of the view; the sky there is at infinity\n vec4 target = eyeAt(depthTexture, vec2(0.5));\n float focalDistance = target.w == 0.0 ? 0.0 : length(target.xyz);\n // 0 in the middle of an eyepiece, 1 on the rim; from the edge of the opening, so the middle of\n // the view, where the eyepieces meet, is not dimmed\n float fromAxis = 1.0 + toRim / RADIUS;\n float rimBlur = max(RIM_BLUR * smoothstep(0.5, 0.95, fromAxis), ring);\n\n vec2 uv = v_textureCoordinates + offsets.xy;\n vec4 center = sampleAt(uv, focalDistance, rimBlur);\n vec3 color = vec3(\n sampleAt(uv + offsets.zw, focalDistance, rimBlur).r,\n center.g,\n sampleAt(uv - offsets.zw, focalDistance, rimBlur).b\n );\n // the light falls off toward the field stop\n color *= 1.0 - VIGNETTE * smoothstep(0.5, 1.0, fromAxis);\n color *= 1.0 - DIRT * dirtAt(gl_FragCoord.xy / size.y);\n if (reticle > 0.0) {\n vec2 fromMiddle = gl_FragCoord.xy - 0.5 * size;\n color = mix(color, HALO.rgb, HALO.a * reticleAt(fromMiddle, 3.0 * czm_pixelRatio));\n color = mix(color, RETICLE.rgb, RETICLE.a * reticleAt(fromMiddle, czm_pixelRatio));\n }\n vec2 shift = vec2(EDGE_SHIFT, 0.0);\n vec3 open = 1.0 - smoothstep(-EDGE, EDGE, vec3(toRimAt(position - shift), toRim, toRimAt(position + shift)));\n out_FragColor = vec4(mix(SURROUND, color, open), center.a);\n}\n";
|