diff --git a/debug/fog.html b/debug/fog.html index 570b27b37b5..cef9e32e1ba 100644 --- a/debug/fog.html +++ b/debug/fog.html @@ -36,6 +36,7 @@ this.start = 2; this.end = 12.0; this.color = [255, 255, 255]; + this.opacity = 1.0; this.horizonBlend = 0.1; this.showTileBoundaries = false; }; @@ -99,7 +100,7 @@ enableFog.onFinishChange((value) => { map.setFog(value ? { "range": [guiParams.start, guiParams.end], - "color": 'rgba(' + guiParams.color[0] + ', ' + guiParams.color[1] + ', ' + guiParams.color[2] + ', 1.0)', + "color": 'rgba(' + guiParams.color[0] + ', ' + guiParams.color[1] + ', ' + guiParams.color[2] + ', ' + guiParams.opacity + ')', "horizon-blend": guiParams.horizonBlend } : null); }); @@ -125,10 +126,17 @@ }); }); + var opacity = fog.add(guiParams, 'opacity', 0.0, 1.0); + opacity.onChange((value) => { + map.setFog({ + "color": 'rgba(' + guiParams.color[0] + ', ' + guiParams.color[1] + ', ' + guiParams.color[2] + ', ' + value + ')' + }); + }); + var color = fog.addColor(guiParams, 'color'); color.onChange((value) => { map.setFog({ - "color": 'rgba(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', 1.0)', + "color": 'rgba(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', ' + guiParams.opacity + ')' }); }); }; @@ -189,7 +197,7 @@ map.setFog(guiParams.enableFog ? { "range": [guiParams.start, guiParams.end], - "color": 'rgba(' + guiParams.color[0] + ', ' + guiParams.color[1] + ', ' + guiParams.color[2] + ', 1.0)', + "color": 'rgba(' + guiParams.color[0] + ', ' + guiParams.color[1] + ', ' + guiParams.color[2] + ', ' + guiParams.opacity + ')', "horizon-blend": guiParams.horizonBlend } : null); diff --git a/src/render/fog.js b/src/render/fog.js index a5767f82b35..a51cd5edf24 100644 --- a/src/render/fog.js +++ b/src/render/fog.js @@ -1,25 +1,48 @@ // @flow import Context from '../gl/context.js'; -import type {UniformLocations} from './uniform_binding.js'; - -import {Uniform1f, Uniform2f, Uniform3f, UniformMatrix4f} from './uniform_binding.js'; +import type {UniformLocations, UniformValues} from './uniform_binding.js'; +import type {UnwrappedTileID} from '../source/tile_id.js'; +import Painter from './painter.js'; +import Fog from '../style/fog.js'; +import {Uniform1f, Uniform2f, Uniform4f, UniformMatrix4f} from './uniform_binding.js'; export type FogUniformsType = {| 'u_fog_matrix': UniformMatrix4f, 'u_fog_range': Uniform2f, - 'u_fog_color': Uniform3f, + 'u_fog_color': Uniform4f, 'u_fog_horizon_blend': Uniform1f, 'u_fog_temporal_offset': Uniform1f, - 'u_fog_opacity': Uniform1f, |}; export const fogUniforms = (context: Context, locations: UniformLocations): FogUniformsType => ({ 'u_fog_matrix': new UniformMatrix4f(context, locations.u_fog_matrix), 'u_fog_range': new Uniform2f(context, locations.u_fog_range), - 'u_fog_color': new Uniform3f(context, locations.u_fog_color), + 'u_fog_color': new Uniform4f(context, locations.u_fog_color), 'u_fog_horizon_blend': new Uniform1f(context, locations.u_fog_horizon_blend), 'u_fog_temporal_offset': new Uniform1f(context, locations.u_fog_temporal_offset), - 'u_fog_opacity': new Uniform1f(context, locations.u_fog_opacity), }); + +export const fogUniformValues = ( + painter: Painter, + fog: Fog, + tileID: ?UnwrappedTileID, + fogOpacity: number +): UniformValues => { + const fogColor = fog.properties.get('color'); + const temporalOffset = (painter.frameCounter / 1000.0) % 1; + const fogColorUnpremultiplied = [ + fogColor.r / fogColor.a, + fogColor.g / fogColor.a, + fogColor.b / fogColor.a, + fogOpacity + ]; + return { + 'u_fog_matrix': tileID ? painter.transform.calculateFogTileMatrix(tileID) : painter.identityMat, + 'u_fog_range': fog.properties.get('range'), + 'u_fog_color': fogColorUnpremultiplied, + 'u_fog_horizon_blend': fog.properties.get('horizon-blend'), + 'u_fog_temporal_offset': temporalOffset + }; +}; diff --git a/src/render/painter.js b/src/render/painter.js index 3b5d1e14034..fda78278d9e 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -19,7 +19,7 @@ import shaders from '../shaders/shaders.js'; import Program from './program.js'; import {programUniforms} from './program/program_uniforms.js'; import Context from '../gl/context.js'; -import {FOG_PITCH_END} from '../style/fog_helpers.js'; +import {fogUniformValues} from '../render/fog.js'; import DepthMode from '../gl/depth_mode.js'; import StencilMode from '../gl/stencil_mode.js'; import ColorMode from '../gl/color_mode.js'; @@ -179,13 +179,13 @@ class Painter { terrain.update(style, this.transform, cameraChanging); } - _updateFog() { - if (this.transform.pitch <= FOG_PITCH_END || !(this.style && this.style.fog)) { + _updateFog(style: Style) { + const fog = style.fog; + if (!fog || (fog && fog.getFogOpacity(this.transform.pitch) !== 1.0)) { this.transform.fogCullDistSq = null; return; } - const fog = this.style.fog; const fogStart = fog.properties.get('range')[0]; const fogEnd = fog.properties.get('range')[1]; @@ -753,13 +753,14 @@ 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 fogOpacity = fog && fog.getFogPitchFactor(this.transform.pitch); - 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 && fogOpacity !== 0.0 && !rtt) defines.push('FOG'); + if (fog && !rtt && fog.getFogOpacity(this.transform.pitch) !== 0.0) { + defines.push('FOG'); + } if (rtt) defines.push('RENDER_TO_TEXTURE'); if (this._showOverdrawInspector) defines.push('OVERDRAW_INSPECTOR'); return defines; @@ -837,26 +838,12 @@ class Painter { } prepareDrawProgram(context: Context, program: Program<*>, tileID: ?UnwrappedTileID) { - const fog = this.style && this.style.fog; - const fogOpacity = (fog && fog.getFogPitchFactor(this.transform.pitch)) || 0.0; - if (fog && fogOpacity !== 0.0) { - const temporalOffset = (this.frameCounter / 1000.0) % 1; - const fogColor = fog.properties.get('color'); - const fogColorUnpremultiplied = fogColor.a === 0 ? [0, 0, 0] : [ - fogColor.r / fogColor.a, - fogColor.g / fogColor.a, - fogColor.b / fogColor.a - ]; - const uniforms = {}; - - uniforms['u_fog_matrix'] = tileID ? this.transform.calculateFogTileMatrix(tileID) : this.identityMat; - uniforms['u_fog_range'] = fog.properties.get('range'); - uniforms['u_fog_color'] = fogColorUnpremultiplied; - uniforms['u_fog_horizon_blend'] = fog.properties.get('horizon-blend'); - uniforms['u_fog_temporal_offset'] = temporalOffset; - uniforms['u_fog_opacity'] = fogOpacity; - - program.setFogUniformValues(context, uniforms); + const fog = this.style.fog; + if (fog) { + const fogOpacity = fog.getFogOpacity(this.transform.pitch); + if (fogOpacity !== 0.0) { + program.setFogUniformValues(context, fogUniformValues(this, fog, tileID, fogOpacity)); + } } } diff --git a/src/shaders/_prelude.glsl b/src/shaders/_prelude.glsl index bb1721febbc..7d2b9c105c0 100644 --- a/src/shaders/_prelude.glsl +++ b/src/shaders/_prelude.glsl @@ -7,7 +7,7 @@ #ifdef FOG -uniform mediump float u_fog_opacity; +uniform mediump vec4 u_fog_color; uniform mediump vec2 u_fog_range; uniform mediump float u_fog_horizon_blend; @@ -24,7 +24,7 @@ float fog_horizon_blending(vec3 camera_dir) { float t = max(0.0, camera_dir.z / u_fog_horizon_blend); // Factor of 3 chosen to roughly match smoothstep. // See: https://www.desmos.com/calculator/pub31lvshf - return u_fog_opacity * exp(-3.0 * t * t); + return u_fog_color.a * exp(-3.0 * t * t); } // Compute a ramp for fog opacity @@ -38,7 +38,7 @@ float fog_opacity(float t) { falloff *= falloff * falloff; // Scale and clip to 1 at the far limit - return u_fog_opacity * min(1.0, 1.00747 * falloff); + return u_fog_color.a * min(1.0, 1.00747 * falloff); } #endif diff --git a/src/shaders/_prelude_fog.fragment.glsl b/src/shaders/_prelude_fog.fragment.glsl index 6eddf131844..25e4325c96d 100644 --- a/src/shaders/_prelude_fog.fragment.glsl +++ b/src/shaders/_prelude_fog.fragment.glsl @@ -1,6 +1,5 @@ #ifdef FOG -uniform vec3 u_fog_color; uniform float u_fog_temporal_offset; // This function is only used in rare places like heatmap where opacity is used @@ -14,18 +13,18 @@ vec3 fog_apply(vec3 color, vec3 pos) { float depth = length(pos); float opacity = fog_opacity(fog_range(depth)); opacity *= fog_horizon_blending(pos / depth); - return mix(color, u_fog_color, opacity); + return mix(color, u_fog_color.rgb, opacity); } // Apply fog computed in the vertex shader vec3 fog_apply_from_vert(vec3 color, float fog_opac) { - return mix(color, u_fog_color, fog_opac); + return mix(color, u_fog_color.rgb, fog_opac); } // Assumes z up vec3 fog_apply_sky_gradient(vec3 camera_ray, vec3 sky_color) { float horizon_blend = fog_horizon_blending(normalize(camera_ray)); - return mix(sky_color, u_fog_color, horizon_blend); + return mix(sky_color, u_fog_color.rgb, horizon_blend); } // Un-premultiply the alpha, then blend fog, then re-premultiply alpha. diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 978da0cb8b8..5cafea53732 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -3718,7 +3718,7 @@ ] }, "transition": true, - "doc": "The color of the fog. If provided as an `rgba` color, the alpha component must be nonzero, but is otherwise unused.", + "doc": "The color of the fog. Using opacity is recommended only for smoothly transitioning fog on/off as anything less than 100% opacity results in more tiles loaded and drawn.", "sdk-support": { "basic functionality": { "js": "2.3.0" diff --git a/src/style-spec/validate/validate_fog.js b/src/style-spec/validate/validate_fog.js index db375748560..63c8228c5f8 100644 --- a/src/style-spec/validate/validate_fog.js +++ b/src/style-spec/validate/validate_fog.js @@ -4,7 +4,6 @@ import validate from './validate.js'; import getType from '../util/get_type.js'; import {isExpression} from '../expression/index.js'; import {deepUnbundle} from '../util/unbundle_jsonlint.js'; -import {parseCSSColor} from 'csscolorparser'; export default function validateFog(options) { const fog = options.value; @@ -21,13 +20,6 @@ export default function validateFog(options) { return errors; } - if (fog.color && !isExpression(deepUnbundle(fog.color))) { - const fogColor = parseCSSColor(fog.color); - if (fogColor && fogColor[3] === 0) { - errors = errors.concat([new ValidationError('fog', fog, 'fog.color alpha must be nonzero.')]); - } - } - if (fog.range && !isExpression(deepUnbundle(fog.range)) && fog.range[0] >= fog.range[1]) { errors = errors.concat([new ValidationError('fog', fog, 'fog.range[0] can\'t be greater than or equal to fog.range[1]')]); } diff --git a/src/style/fog.js b/src/style/fog.js index fe740c01135..74614f398c5 100644 --- a/src/style/fog.js +++ b/src/style/fog.js @@ -43,7 +43,8 @@ class Fog extends Evented { get state(): FogState { return { range: this.properties.get('range'), - horizonBlend: this.properties.get('horizon-blend') + horizonBlend: this.properties.get('horizon-blend'), + alpha: this.properties.get('color').a }; } @@ -70,6 +71,11 @@ class Fog extends Evented { return smoothstep(FOG_PITCH_START, FOG_PITCH_END, pitch); } + getFogOpacity(pitch: number): number { + const fogColor = (this.properties && this.properties.get('color')) || 1.0; + return this.getFogPitchFactor(pitch) * fogColor.a; + } + getOpacityAtLatLng(lngLat: LngLat, transform: Transform): number { return getFogOpacityAtLngLat(this.state, lngLat, transform); } diff --git a/src/style/fog_helpers.js b/src/style/fog_helpers.js index 40ffb408d22..6c624120ed6 100644 --- a/src/style/fog_helpers.js +++ b/src/style/fog_helpers.js @@ -12,7 +12,8 @@ export const FOG_SYMBOL_CLIPPING_THRESHOLD = 0.9; export type FogState = { range: [number, number], - horizonBlend: number + horizonBlend: number, + alpha: number }; // As defined in _prelude_fog.fragment.glsl#fog_opacity @@ -30,7 +31,7 @@ export function getFogOpacity(state: FogState, pos: Array, pitch: number falloff *= falloff * falloff; falloff = Math.min(1.0, 1.00747 * falloff); - return falloff * fogPitchOpacity; + return falloff * fogPitchOpacity * state.alpha; } export function getFogOpacityAtTileCoord(state: FogState, x: number, y: number, z: number, tileId: UnwrappedTileID, transform: Transform): number { diff --git a/src/ui/map.js b/src/ui/map.js index 82ff451113f..5be892d6f0e 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -2658,7 +2658,7 @@ class Map extends Camera { // need for the current transform if (this.style && this._sourcesDirty) { this._sourcesDirty = false; - this.painter._updateFog(); + this.painter._updateFog(this.style); this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. this.style._updateSources(this.transform); } diff --git a/test/integration/render-tests/fog/color-opacity/expected.png b/test/integration/render-tests/fog/color-opacity/expected.png new file mode 100644 index 00000000000..7530a2233b3 Binary files /dev/null and b/test/integration/render-tests/fog/color-opacity/expected.png differ diff --git a/test/integration/render-tests/fog/color-opacity/style.json b/test/integration/render-tests/fog/color-opacity/style.json new file mode 100644 index 00000000000..9659dc6011e --- /dev/null +++ b/test/integration/render-tests/fog/color-opacity/style.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "allowed": 0.0005 + } + }, + "center": [ + 52.499167, + 13.418056 + ], + "zoom": 16, + "pitch": 70, + "fog": { + "range": [1, 4], + "color": "rgba(255, 30, 35, 0.8)" + }, + "sources": {}, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "beige" + } + } + ] +} diff --git a/test/integration/render-tests/fog/culling/opacity/expected.png b/test/integration/render-tests/fog/culling/opacity/expected.png new file mode 100644 index 00000000000..ff333b3a8eb Binary files /dev/null and b/test/integration/render-tests/fog/culling/opacity/expected.png differ diff --git a/test/integration/render-tests/fog/culling/opacity/style.json b/test/integration/render-tests/fog/culling/opacity/style.json new file mode 100644 index 00000000000..70637edc4f3 --- /dev/null +++ b/test/integration/render-tests/fog/culling/opacity/style.json @@ -0,0 +1,60 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 512, + "width": 64 + } + }, + "center": [ + 0.1, + 0.1 + ], + "pitch": 70, + "bearing": 90, + "zoom": 15, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -180, + 0 + ], + [ + 180, + 0.0 + ] + ] + } + } + } + }, + "fog": { + "range": [1, 1.2], + "color": "rgba(0, 0, 255, 0.8)" + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "beige" + } + }, + { + "id": "dash", + "type": "line", + "source": "geojson", + "paint": { + "line-color": "green", + "line-width": 30 + } + } + ] +} diff --git a/test/integration/render-tests/fog/disable/expected.png b/test/integration/render-tests/fog/disable/expected.png new file mode 100644 index 00000000000..64ef56df5c6 Binary files /dev/null and b/test/integration/render-tests/fog/disable/expected.png differ diff --git a/test/integration/render-tests/fog/disable/style.json b/test/integration/render-tests/fog/disable/style.json new file mode 100644 index 00000000000..aac21e2e0ac --- /dev/null +++ b/test/integration/render-tests/fog/disable/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "operations": [ + ["wait", 500], + ["setFog", null], + ["wait", 500] + ] + } + }, + "center": [-113.32296, 35.94662], + "zoom": 12.1, + "pitch": 80, + "bearing": 64.5, + "terrain": { + "source": "rgbterrain" + }, + "fog": { + "range": [1, 2], + "color": "white", + "horizon-blend": 0.15 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 15 + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fog/reenable/expected.png b/test/integration/render-tests/fog/reenable/expected.png new file mode 100644 index 00000000000..09066b8c9df Binary files /dev/null and b/test/integration/render-tests/fog/reenable/expected.png differ diff --git a/test/integration/render-tests/fog/reenable/style.json b/test/integration/render-tests/fog/reenable/style.json new file mode 100644 index 00000000000..58efad08242 --- /dev/null +++ b/test/integration/render-tests/fog/reenable/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "operations": [ + ["wait", 500], + ["setFog", null], + ["setFog", { + "range": [1, 2], + "color": "blue", + "horizon-blend": 0.15 + }], + ["wait", 500] + ] + } + }, + "center": [-113.32296, 35.94662], + "zoom": 12.1, + "pitch": 80, + "bearing": 64.5, + "terrain": { + "source": "rgbterrain" + }, + "fog": { + "range": [1, 2], + "color": "white", + "horizon-blend": 0.15 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/unit/style-spec/fixture/fog-invalid-color.input.json b/test/unit/style-spec/fixture/fog-invalid-color.input.json deleted file mode 100644 index d000388c5a1..00000000000 --- a/test/unit/style-spec/fixture/fog-invalid-color.input.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 8, - "sources": { - "vector": { - "type": "vector", - "url": "mapbox://mapbox.mapbox-streets-v5" - } - }, - "layers": [], - "fog": { - "range": [1, 2], - "color": "rgba(50,100,150,0)" - } -} diff --git a/test/unit/style-spec/fixture/fog-invalid-color.output-api-supported.json b/test/unit/style-spec/fixture/fog-invalid-color.output-api-supported.json deleted file mode 100644 index b8bb172b823..00000000000 --- a/test/unit/style-spec/fixture/fog-invalid-color.output-api-supported.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "line": 10, - "message": "fog: fog.color alpha must be nonzero." - } -] diff --git a/test/unit/style-spec/fixture/fog-invalid-color.output.json b/test/unit/style-spec/fixture/fog-invalid-color.output.json deleted file mode 100644 index b8bb172b823..00000000000 --- a/test/unit/style-spec/fixture/fog-invalid-color.output.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "line": 10, - "message": "fog: fog.color alpha must be nonzero." - } -]