From 6c807d7293f28fa477e69318a970c9a0a5632fb4 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 6 Apr 2021 12:31:40 -0700 Subject: [PATCH] Add haziness property to fog (#10537) * Add haziness to fog * Fix flow types * Apply PR feedback --- debug/fog.html | 50 ++++++++++++++---- src/css/mapbox-gl.css | 2 +- src/render/fog.js | 6 +++ src/render/painter.js | 24 ++++++--- src/render/program.js | 4 +- src/shaders/_prelude.fragment.glsl | 9 ++++ src/shaders/_prelude_fog.fragment.glsl | 72 ++++++++++++++++---------- src/style-spec/reference/v8.json | 65 +++++++++++++++++++++++ src/style-spec/types.js | 3 ++ src/style/fog.js | 20 +++++-- 10 files changed, 208 insertions(+), 47 deletions(-) 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 dbeec1f9187..5af5183ea3d 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; }