diff --git a/debug/fog.html b/debug/fog.html
index cb936577b35..2ca7b5420c4 100644
--- a/debug/fog.html
+++ b/debug/fog.html
@@ -36,6 +36,9 @@
this.fogRangeStart = 1.5;
this.fogRangeEnd = 2.0;
this.fogColor = [255, 255, 255];
+ this.fogHazeColor = [109, 123, 180];
+ this.fogHazeEnergy = 1.0;
+ this.fogStrength = 1.0;
this.fogSkyBlend = 0.1;
this.showTileBoundaries = false;
};
@@ -100,37 +103,61 @@
map.setFog(value ? {
"range": [guiParams.fogRangeStart, guiParams.fogRangeEnd],
"color": 'rgba(' + guiParams.fogColor[0] + ', ' + guiParams.fogColor[1] + ', ' + guiParams.fogColor[2] + ', 1.0)',
+ "haze-color": 'rgba(' + guiParams.fogHazeColor[0] + ', ' + guiParams.fogHazeColor[1] + ', ' + guiParams.fogHazeColor[2] + ', 1.0)',
+ "haze-energy": guiParams.fogHazeEnergy,
+ "strength": guiParams.fogStrength,
"sky-blend": guiParams.fogSkyBlend
} : null);
});
- var fogColor = fog.addColor(guiParams, 'fogColor');
- fogColor.onFinishChange((value) => {
- map.setFog({
- "color": 'rgba(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', 1.0)',
- });
- });
-
var fogRangeStart = fog.add(guiParams, 'fogRangeStart', 0.0, 15.0);
- fogRangeStart.onFinishChange((value) => {
+ fogRangeStart.onChange((value) => {
map.setFog({
"range": [value, guiParams.fogRangeEnd],
});
});
var fogRangeEnd = fog.add(guiParams, 'fogRangeEnd', 0.0, 15.0);
- fogRangeEnd.onFinishChange((value) => {
+ fogRangeEnd.onChange((value) => {
map.setFog({
"range": [guiParams.fogRangeStart, value],
});
});
+ var fogStrength = fog.add(guiParams, 'fogStrength', 0, 1);
+ fogStrength.onChange((value) => {
+ map.setFog({
+ "strength": value,
+ });
+ });
+
+ var fogHazeEnergy = fog.add(guiParams, 'fogHazeEnergy', 0, 2);
+ fogHazeEnergy.onChange((value) => {
+ map.setFog({
+ "haze-energy": value,
+ });
+ });
+
var fogSkyBlend = fog.add(guiParams, 'fogSkyBlend', 0.0, 1.0);
- fogSkyBlend.onFinishChange((value) => {
+ fogSkyBlend.onChange((value) => {
map.setFog({
"sky-blend": value,
});
});
+
+ var fogColor = fog.addColor(guiParams, 'fogColor');
+ fogColor.onChange((value) => {
+ map.setFog({
+ "color": 'rgba(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', 1.0)',
+ });
+ });
+
+ var fogHazeColor = fog.addColor(guiParams, 'fogHazeColor');
+ fogHazeColor.onChange((value) => {
+ map.setFog({
+ "haze-color": 'rgba(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', 1.0)',
+ });
+ });
};
var popup = new mapboxgl.Popup().setHTML('Pitch Alignment: auto
Rotation Alignment: auto');
@@ -190,6 +217,9 @@
map.setFog(guiParams.enableFog ? {
"range": [guiParams.fogRangeStart, guiParams.fogRangeEnd],
"color": 'rgba(' + guiParams.fogColor[0] + ', ' + guiParams.fogColor[1] + ', ' + guiParams.fogColor[2] + ', 1.0)',
+ "haze-color": 'rgba(' + guiParams.fogHazeColor[0] + ', ' + guiParams.fogHazeColor[1] + ', ' + guiParams.fogHazeColor[2] + ', 1.0)',
+ "strength": guiParams.fogStrength,
+ "haze-energy": guiParams.fogHazeEnergy,
"sky-blend": guiParams.fogSkyBlend
} : null);
diff --git a/src/css/mapbox-gl.css b/src/css/mapbox-gl.css
index 75642ceb015..2cd9cb359af 100644
--- a/src/css/mapbox-gl.css
+++ b/src/css/mapbox-gl.css
@@ -673,7 +673,7 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact {
}
.mapboxgl-marker-occluded {
- opacity: 0.0;
+ opacity: 0;
}
.mapboxgl-marker-occluded-low {
diff --git a/src/render/fog.js b/src/render/fog.js
index c6ca61d9d5e..52b403554bf 100644
--- a/src/render/fog.js
+++ b/src/render/fog.js
@@ -9,16 +9,22 @@ export type FogUniformsType = {|
'u_cam_matrix': UniformMatrix4f,
'u_fog_range': Uniform2f,
'u_fog_color': Uniform3f,
+ 'u_fog_exponent': Uniform1f,
'u_fog_opacity': Uniform1f,
'u_fog_sky_blend': Uniform1f,
'u_fog_temporal_offset': Uniform1f,
+ 'u_haze_color_linear': Uniform3f,
+ 'u_haze_energy': Uniform1f,
|};
export const fogUniforms = (context: Context, locations: UniformLocations): FogUniformsType => ({
'u_cam_matrix': new UniformMatrix4f(context, locations.u_cam_matrix),
'u_fog_range': new Uniform2f(context, locations.u_fog_range),
'u_fog_color': new Uniform3f(context, locations.u_fog_color),
+ 'u_fog_exponent': new Uniform1f(context, locations.u_fog_exponent),
'u_fog_opacity': new Uniform1f(context, locations.u_fog_opacity),
'u_fog_sky_blend': new Uniform1f(context, locations.u_fog_sky_blend),
'u_fog_temporal_offset': new Uniform1f(context, locations.u_fog_temporal_offset),
+ 'u_haze_color_linear': new Uniform3f(context, locations.u_haze_color_linear),
+ 'u_haze_energy': new Uniform1f(context, locations.u_haze_energy),
});
diff --git a/src/render/painter.js b/src/render/painter.js
index 4eb10fd58f2..4bab2a9fba2 100644
--- a/src/render/painter.js
+++ b/src/render/painter.js
@@ -184,11 +184,14 @@ class Painter {
if (fog) {
const fogStart = fog.properties.get('range')[0];
const fogEnd = fog.properties.get('range')[1];
- // We start culling at 80% between the fog start and end,
- // leaving a non-noticeable 2% fog opacity change threshold.
- const fogCullDist = fogStart + (fogEnd - fogStart) * 0.8;
-
- this.transform.fogCullDistSq = fogCullDist * fogCullDist;
+ const fogStrength = fog.properties.get('strength');
+ // We start culling where the fog opacity function hits 98%, leaving
+ // a non-noticeable opacity change threshold. We use an arbitrary function
+ // which bounds the true answer. See: https://www.desmos.com/calculator/lw03ldsuhy
+ const fogBoundFraction = 1 - 0.22 * Math.exp(4 * (fogStrength - 1));
+ const fogCullDist = fogStart + (fogEnd - fogStart) * fogBoundFraction;
+
+ this.transform.fogCullDistSq = fogCullDist * fogCullDist;
this.transform.fogCulling = this.transform.pitch > FOG_PITCH_END;
} else {
this.transform.fogCullDistSq = null;
@@ -749,12 +752,16 @@ class Painter {
const terrain = this.terrain && !this.terrain.renderingToTexture; // Enables elevation sampling in vertex shader.
const rtt = this.terrain && this.terrain.renderingToTexture;
const fog = this.style && this.style.fog;
+ const haze = fog && fog.properties && fog.properties.get('haze-energy') > 0;
const defines = [];
if (terrain) defines.push('TERRAIN');
// When terrain is active, fog is rendered as part of draping, not as part of tile
// rendering. Removing the fog flag during tile rendering avoids additional defines.
- if (fog && !rtt) defines.push('FOG');
+ if (fog && !rtt) {
+ defines.push('FOG');
+ if (haze) defines.push('FOG_HAZE');
+ }
if (rtt) defines.push('RENDER_TO_TEXTURE');
if (this._showOverdrawInspector) defines.push('OVERDRAW_INSPECTOR');
return defines;
@@ -836,14 +843,19 @@ class Painter {
if (fog) {
const temporalOffset = (this.frameCounter / 1000.0) % 1;
const fogColor = fog.properties.get('color');
+ const hazeColor = fog.properties.get('haze-color');
+ const hazeColorLinear = [Math.pow(hazeColor.r, 2.2), Math.pow(hazeColor.g, 2.2), Math.pow(hazeColor.b, 2.2)];
const uniforms = {};
uniforms['u_cam_matrix'] = tileID ? this.transform.calculateCameraMatrix(tileID) : this.identityMat;
uniforms['u_fog_range'] = fog.properties.get('range');
uniforms['u_fog_color'] = [fogColor.r, fogColor.g, fogColor.b];
+ uniforms['u_fog_exponent'] = Math.max(1e-3, 12 * Math.pow(1 - fog.properties.get('strength'), 2));
uniforms['u_fog_opacity'] = fog.getFogPitchFactor(this.transform.pitch);
uniforms['u_fog_sky_blend'] = fog.properties.get('sky-blend');
uniforms['u_fog_temporal_offset'] = temporalOffset;
+ uniforms['u_haze_color_linear'] = hazeColorLinear;
+ uniforms['u_haze_energy'] = fog.properties.get('haze-energy');
program.setFogUniformValues(context, uniforms);
}
diff --git a/src/render/program.js b/src/render/program.js
index 4316d7d2f86..39898b41bc5 100644
--- a/src/render/program.js
+++ b/src/render/program.js
@@ -158,7 +158,9 @@ class Program {
context.program.set(this.program);
for (const name in fogUniformsValues) {
- uniforms[name].set(fogUniformsValues[name]);
+ if (uniforms[name].location) {
+ uniforms[name].set(fogUniformsValues[name]);
+ }
}
}
diff --git a/src/shaders/_prelude.fragment.glsl b/src/shaders/_prelude.fragment.glsl
index e9f24514ea2..7fd1ffa2447 100644
--- a/src/shaders/_prelude.fragment.glsl
+++ b/src/shaders/_prelude.fragment.glsl
@@ -28,3 +28,12 @@ vec3 dither(vec3 color, highp vec2 seed) {
vec3 rnd = hash(seed) + hash(seed + 0.59374) - 0.5;
return color + rnd / 255.0;
}
+
+vec3 linear_to_srgb(vec3 color) {
+ return pow(color, vec3(1.0 / 2.2));
+}
+
+vec3 srgb_to_linear(vec3 color) {
+ return pow(color, vec3(2.2));
+}
+
diff --git a/src/shaders/_prelude_fog.fragment.glsl b/src/shaders/_prelude_fog.fragment.glsl
index 316c9d9f41a..1454c82d950 100644
--- a/src/shaders/_prelude_fog.fragment.glsl
+++ b/src/shaders/_prelude_fog.fragment.glsl
@@ -2,9 +2,19 @@
uniform vec2 u_fog_range;
uniform vec3 u_fog_color;
+uniform vec3 u_haze_color_linear;
+uniform float u_haze_energy;
uniform float u_fog_opacity;
uniform float u_fog_sky_blend;
uniform float u_fog_temporal_offset;
+uniform float u_fog_exponent;
+
+vec3 tonemap(vec3 color) {
+ // Use an exponential smoothmin between y=x and y=1 for tone-mapping
+ // See: https://www.desmos.com/calculator/h8odggcnd0
+ const float k = 8.0;
+ return max(vec3(0), log2(exp2(-k * color) + exp2(-k)) * (-1.0 / k));
+}
// Assumes z up and camera_dir *normalized* (to avoid computing its length multiple
// times for different functions).
@@ -15,30 +25,48 @@ float fog_sky_blending(vec3 camera_dir) {
return u_fog_opacity * exp(-3.0 * t * t);
}
-float fog_opacity(vec3 pos) {
- float depth = length(pos);
- float start = u_fog_range.x;
- float end = u_fog_range.y;
-
- // The fog is not physically accurate, so we seek an expression which satisfies a
- // couple basic constraints:
- // - opacity should be 0 at the near limit
- // - opacity should be 1 at the far limit
- // - the onset should have smooth derivatives to avoid a sharp band
- // To this end, we use an (1 - e^x)^n, where n is set to 3 to ensure the
- // function is C2 continuous at the onset. The fog is about 99% opaque at
- // the far limit, so we simply scale it and clip to achieve 100% opacity.
- // https://www.desmos.com/calculator/3taufutxid
- const float decay = 5.5;
- float falloff = max(0.0, 1.0 - exp(-decay * (depth - start) / (end - start)));
+// Computes the fog opacity when fog strength = 1. Otherwise it's multiplied
+// by a smoothstep to a power to decrease the amount of fog relative to haze.
+// - t: depth, rescaled to 0 at fogStart and 1 at fogEnd
+// See: https://www.desmos.com/calculator/3taufutxid
+// This function much match src/style/fog.js
+float fog_opacity(float t) {
+ const float decay = 6.0;
+ float falloff = 1.0 - min(1.0, exp(-decay * t));
// Cube without pow()
falloff *= falloff * falloff;
// Scale and clip to 1 at the far limit
- falloff = min(1.0, 1.00747 * falloff);
+ return u_fog_opacity * min(1.0, 1.00747 * falloff);
+}
+
+// This function is only used in rare places like heatmap where opacity is used
+// directly, outside the normal fog_apply method.
+float fog_opacity (vec3 pos) {
+ return fog_opacity((length(pos) - u_fog_range.x) / (u_fog_range.y - u_fog_range.x));
+}
+
+vec3 fog_apply(vec3 color, vec3 pos) {
+ // Map [near, far] to [0, 1]
+ float t = (length(pos) - u_fog_range.x) / (u_fog_range.y - u_fog_range.x);
- return falloff * u_fog_opacity;
+ float haze_opac = fog_opacity(pos);
+ float fog_opac = haze_opac * pow(smoothstep(0.0, 1.0, t), u_fog_exponent);
+
+#ifdef FOG_HAZE
+ vec3 haze = (haze_opac * u_haze_energy) * u_haze_color_linear;
+
+ // The smoothstep fades in tonemapping slightly before the fog layer. This causes
+ // the principle that fog should not have an effect outside the fog layer, but the
+ // effect is hardly noticeable except on pure white glaciers..
+ float tonemap_strength = u_fog_opacity * min(1.0, u_haze_energy) * smoothstep(-0.5, 0.25, t);
+ color = srgb_to_linear(color);
+ color = mix(color, tonemap(color + haze), tonemap_strength);
+ color = linear_to_srgb(color);
+#endif
+
+ return mix(color, u_fog_color, fog_opac);
}
// Assumes z up
@@ -46,14 +74,6 @@ vec3 fog_apply_sky_gradient(vec3 camera_ray, vec3 sky_color) {
return mix(sky_color, u_fog_color, fog_sky_blending(normalize(camera_ray)));
}
-vec3 fog_apply(vec3 color, vec3 pos) {
- // We mix in sRGB color space. sRGB roughly corrects for perceived brightness
- // so that dark fog and light fog obscure similarly for otherwise identical
- // parameters. If we blend in linear RGB, then the parameters to control dark
- // and light fog are fundamentally different.
- return mix(color, u_fog_color, fog_opacity(pos));
-}
-
// Un-premultiply the alpha, then blend fog, then re-premultiply alpha. For
// use with colors using premultiplied alpha
vec4 fog_apply_premultiplied(vec4 color, vec3 pos) {
diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json
index faf6fb54f87..487fb3d2fac 100644
--- a/src/style-spec/reference/v8.json
+++ b/src/style-spec/reference/v8.json
@@ -3731,6 +3731,71 @@
}
}
},
+ "haze-color": {
+ "type": "color",
+ "property-type": "data-constant",
+ "default": "#7287d5",
+ "expression": {
+ "interpolated": true,
+ "parameters": [
+ "zoom"
+ ]
+ },
+ "transition": true,
+ "doc": "",
+ "sdk-support": {
+ "basic functionality": {
+ "js": "",
+ "android": "",
+ "ios": "",
+ "macos": ""
+ }
+ }
+ },
+ "strength": {
+ "type": "number",
+ "property-type": "data-constant",
+ "default": 1.0,
+ "minimum": 0.0,
+ "expression": {
+ "interpolated": true,
+ "parameters": [
+ "zoom"
+ ]
+ },
+ "transition": true,
+ "doc": "",
+ "sdk-support": {
+ "basic functionality": {
+ "js": "",
+ "android": "",
+ "ios": "",
+ "macos": ""
+ }
+ }
+ },
+ "haze-energy": {
+ "type": "number",
+ "property-type": "data-constant",
+ "default": 1.0,
+ "minimum": 0.0,
+ "expression": {
+ "interpolated": true,
+ "parameters": [
+ "zoom"
+ ]
+ },
+ "transition": true,
+ "doc": "",
+ "sdk-support": {
+ "basic functionality": {
+ "js": "",
+ "android": "",
+ "ios": "",
+ "macos": ""
+ }
+ }
+ },
"sky-blend": {
"type": "number",
"property-type": "data-constant",
diff --git a/src/style-spec/types.js b/src/style-spec/types.js
index 3ccb21d98fe..7fd6ab5c88c 100644
--- a/src/style-spec/types.js
+++ b/src/style-spec/types.js
@@ -90,6 +90,9 @@ export type TerrainSpecification = {|
export type FogSpecification = {|
"range"?: PropertyValueSpecification<[number, number]>,
"color"?: PropertyValueSpecification,
+ "haze-color"?: PropertyValueSpecification,
+ "strength"?: PropertyValueSpecification,
+ "haze-energy"?: PropertyValueSpecification,
"sky-blend"?: PropertyValueSpecification
|}
diff --git a/src/style/fog.js b/src/style/fog.js
index 104f98b43a9..20693005d90 100644
--- a/src/style/fog.js
+++ b/src/style/fog.js
@@ -18,12 +18,18 @@ import Color from '../style-spec/util/color.js';
type Props = {|
"range": DataConstantProperty<[number, number]>,
"color": DataConstantProperty,
+ "haze-color": DataConstantProperty,
+ "haze-energy": DataConstantProperty,
+ "strength": DataConstantProperty,
"sky-blend": DataConstantProperty,
|};
const properties: Properties = new Properties({
"range": new DataConstantProperty(styleSpec.fog.range),
"color": new DataConstantProperty(styleSpec.fog.color),
+ "haze-color": new DataConstantProperty(styleSpec.fog["haze-color"]),
+ "haze-energy": new DataConstantProperty(styleSpec.fog["strength"]),
+ "strength": new DataConstantProperty(styleSpec.fog["haze-energy"]),
"sky-blend": new DataConstantProperty(styleSpec.fog["sky-blend"]),
});
@@ -42,6 +48,7 @@ export class FogSampler {
const props = this.properties;
const range = props.get('range');
const fogOpacity = smoothstep(FOG_PITCH_START, FOG_PITCH_END, pitch);
+ const fogStrength = props.get('strength');
const [start, end] = range;
// The fog is not physically accurate, so we seek an expression which satisfies a
@@ -53,9 +60,10 @@ export class FogSampler {
// function is C2 continuous at the onset. The fog is about 99% opaque at
// the far limit, so we simply scale it and clip to achieve 100% opacity.
// https://www.desmos.com/calculator/3taufutxid
- // The output of this function must match src/shaders/_prelude_fog.fragment.glsl
- const decay = 5.5;
- let falloff = Math.max(0.0, 1.0 - Math.exp(-decay * (depth - start) / (end - start)));
+ // The output of this function should match src/shaders/_prelude_fog.fragment.glsl
+ const decay = 6;
+ const t = (depth - start) / (end - start);
+ let falloff = 1.0 - Math.min(1, Math.exp(-decay * t));
// Cube without pow()
falloff *= falloff * falloff;
@@ -63,6 +71,12 @@ export class FogSampler {
// Scale and clip to 1 at the far limit
falloff = Math.min(1.0, 1.00747 * falloff);
+ // From src/render/painter.js via fog uniforms:
+ const fogExponent = 12 * Math.pow(1 - fogStrength, 2);
+
+ // Account for fog strength
+ falloff *= Math.pow(smoothstep(0, 1, t), fogExponent);
+
return falloff * fogOpacity;
}