diff --git a/build/generate-style-spec.ts b/build/generate-style-spec.ts index 72f3f502e8..099b456266 100644 --- a/build/generate-style-spec.ts +++ b/build/generate-style-spec.ts @@ -34,6 +34,8 @@ function propertyType(property) { } case 'light': return 'LightSpecification'; + case 'sky': + return 'SkySpecification'; case 'sources': return '{[_: string]: SourceSpecification}'; case '*': @@ -130,19 +132,19 @@ export type PromoteIdSpecification = {[_: string]: string} | string; export type ExpressionInputType = string | number | boolean; -export type CollatorExpressionSpecification = +export type CollatorExpressionSpecification = ['collator', { - 'case-sensitive'?: boolean | ExpressionSpecification, - 'diacritic-sensitive'?: boolean | ExpressionSpecification, + 'case-sensitive'?: boolean | ExpressionSpecification, + 'diacritic-sensitive'?: boolean | ExpressionSpecification, locale?: string | ExpressionSpecification} ]; // collator export type InterpolationSpecification = - | ['linear'] - | ['exponential', number | ExpressionSpecification] + | ['linear'] + | ['exponential', number | ExpressionSpecification] | ['cubic-bezier', number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification] -export type ExpressionSpecification = +export type ExpressionSpecification = // types | ['array', unknown | ExpressionSpecification] // array | ['array', ExpressionInputType | ExpressionSpecification, unknown | ExpressionSpecification] // array @@ -185,20 +187,20 @@ export type ExpressionSpecification = | ['>=', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean | ['all', ...(boolean | ExpressionSpecification)[]] // boolean | ['any', ...(boolean | ExpressionSpecification)[]] // boolean - | ['case', boolean | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + | ['case', boolean | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, ...(boolean | ExpressionInputType | ExpressionSpecification)[], ExpressionInputType | ExpressionSpecification] | ['coalesce', ...(ExpressionInputType | ExpressionSpecification)[]] // at least two inputs required - | ['match', ExpressionInputType | ExpressionSpecification, - ExpressionInputType | ExpressionInputType[], ExpressionInputType | ExpressionSpecification, + | ['match', ExpressionInputType | ExpressionSpecification, + ExpressionInputType | ExpressionInputType[], ExpressionInputType | ExpressionSpecification, ...(ExpressionInputType | ExpressionInputType[] | ExpressionSpecification)[], // repeated as above ExpressionInputType | ExpressionSpecification] | ['within', unknown | ExpressionSpecification] // Ramps, scales, curves - | ['interpolate', InterpolationSpecification, number | ExpressionSpecification, + | ['interpolate', InterpolationSpecification, number | ExpressionSpecification, ...(number | number[] | ColorSpecification)[]] // alternating number and number | number[] | ColorSpecification - | ['interpolate-hcl', InterpolationSpecification, number | ExpressionSpecification, + | ['interpolate-hcl', InterpolationSpecification, number | ExpressionSpecification, ...(number | ColorSpecification)[]] // alternating number and ColorSpecificaton - | ['interpolate-lab', InterpolationSpecification, number | ExpressionSpecification, + | ['interpolate-lab', InterpolationSpecification, number | ExpressionSpecification, ...(number | ColorSpecification)[]] // alternating number and ColorSpecification | ['step', number | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, ...(number | ExpressionInputType | ExpressionSpecification)[]] // alternating number and ExpressionInputType | ExpressionSpecification @@ -308,6 +310,8 @@ ${objectDeclaration('StyleSpecification', spec.$root)} ${objectDeclaration('LightSpecification', spec.light)} +${objectDeclaration('SkySpecification', spec.sky)} + ${objectDeclaration('TerrainSpecification', spec.terrain)} ${spec.source.map(key => objectDeclaration(sourceTypeName(key), spec[key])).join('\n\n')} diff --git a/src/geo/transform.ts b/src/geo/transform.ts index c1602bea9b..466fafaf70 100644 --- a/src/geo/transform.ts +++ b/src/geo/transform.ts @@ -35,6 +35,7 @@ class Transform { cameraToSeaLevelDistance: number; mercatorMatrix: mat4; projMatrix: mat4; + fogMatrix: mat4; invProjMatrix: mat4; alignedProjMatrix: mat4; pixelMatrix: mat4; @@ -59,6 +60,7 @@ class Transform { _constraining: boolean; _posMatrixCache: {[_: string]: mat4}; _alignedPosMatrixCache: {[_: string]: mat4}; + _fogMatrixCache: {[_: string]: mat4}; constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) { this.tileSize = 512; // constant @@ -86,6 +88,7 @@ class Transform { this._edgeInsets = new EdgeInsets(); this._posMatrixCache = {}; this._alignedPosMatrixCache = {}; + this._fogMatrixCache = {}; } clone(): Transform { @@ -712,6 +715,17 @@ class Transform { } } + calculateTileMatrix(unwrappedTileID: UnwrappedTileID): mat4 { + const canonical = unwrappedTileID.canonical; + const scale = this.worldSize / this.zoomScale(canonical.z); + const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; + + const worldMatrix = mat4.identity(new Float64Array(16) as any); + mat4.translate(worldMatrix, worldMatrix, [unwrappedX * scale, canonical.y * scale, 0]); + mat4.scale(worldMatrix, worldMatrix, [scale / EXTENT, scale / EXTENT, 1]); + return worldMatrix; + } + /** * Calculate the posMatrix that, given a tile coordinate, would be used to display the tile on a map. * @param {UnwrappedTileID} unwrappedTileID; @@ -724,19 +738,32 @@ class Transform { return cache[posMatrixKey]; } - const canonical = unwrappedTileID.canonical; - const scale = this.worldSize / this.zoomScale(canonical.z); - const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; - - const posMatrix = mat4.identity(new Float64Array(16) as any); - mat4.translate(posMatrix, posMatrix, [unwrappedX * scale, canonical.y * scale, 0]); - mat4.scale(posMatrix, posMatrix, [scale / EXTENT, scale / EXTENT, 1]); + const posMatrix = this.calculateTileMatrix(unwrappedTileID); mat4.multiply(posMatrix, aligned ? this.alignedProjMatrix : this.projMatrix, posMatrix); cache[posMatrixKey] = new Float32Array(posMatrix); return cache[posMatrixKey]; } + /** + * Calculate the fogMatrix that, given a tile coordinate, would be used to calculate fog on the map. + * @param {UnwrappedTileID} unwrappedTileID; + * @private + */ + calculateFogMatrix(unwrappedTileID: UnwrappedTileID): mat4 { + const posMatrixKey = unwrappedTileID.key; + const cache = this._fogMatrixCache; + if (cache[posMatrixKey]) { + return cache[posMatrixKey]; + } + + const fogMatrix = this.calculateTileMatrix(unwrappedTileID); + mat4.multiply(fogMatrix, this.fogMatrix, fogMatrix); + + cache[posMatrixKey] = new Float32Array(fogMatrix); + return cache[posMatrixKey]; + } + customLayerMatrix(): mat4 { return this.mercatorMatrix.slice() as any; } @@ -905,6 +932,20 @@ class Transform { this.projMatrix = m; this.invProjMatrix = mat4.invert([] as any, m); + // create a fog matrix, same es proj-matrix but with near clipping-plane in mapcenter + // needed to calculate a correct z-value for fog calculation, because projMatrix z value is not + this.fogMatrix = new Float64Array(16) as any; + mat4.perspective(this.fogMatrix, this._fov, this.width / this.height, this.cameraToSeaLevelDistance, farZ); + this.fogMatrix[8] = -offset.x * 2 / this.width; + this.fogMatrix[9] = offset.y * 2 / this.height; + mat4.scale(this.fogMatrix, this.fogMatrix, [1, -1, 1]); + mat4.translate(this.fogMatrix, this.fogMatrix, [0, 0, -this.cameraToCenterDistance]); + mat4.rotateX(this.fogMatrix, this.fogMatrix, this._pitch); + mat4.rotateZ(this.fogMatrix, this.fogMatrix, this.angle); + mat4.translate(this.fogMatrix, this.fogMatrix, [-x, -y, 0]); + mat4.scale(this.fogMatrix, this.fogMatrix, [1, 1, this._pixelPerMeter]); + mat4.translate(this.fogMatrix, this.fogMatrix, [0, 0, -this.elevation]); // elevate camera over terrain + // matrix for conversion from location to screen coordinates in 2D this.pixelMatrix3D = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m); @@ -929,6 +970,7 @@ class Transform { this._posMatrixCache = {}; this._alignedPosMatrixCache = {}; + this._fogMatrixCache = {}; } maxPitchScaleFactor() { diff --git a/src/render/draw_sky.ts b/src/render/draw_sky.ts new file mode 100644 index 0000000000..04f7f2289e --- /dev/null +++ b/src/render/draw_sky.ts @@ -0,0 +1,24 @@ +import StencilMode from '../gl/stencil_mode'; +import DepthMode from '../gl/depth_mode'; +import CullFaceMode from '../gl/cull_face_mode'; +import {skyUniformValues} from './program/sky_program'; +import type Painter from './painter'; +import Sky from '../style/sky'; + +export default drawSky; + +function drawSky(painter: Painter, sky: Sky) { + const context = painter.context; + const gl = context.gl; + + const skyUniforms = skyUniformValues(sky, painter.style.map.transform, painter.pixelRatio); + + const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, [0, 1]); + const stencilMode = StencilMode.disabled; + const colorMode = painter.colorModeForRenderPass(); + const program = painter.useProgram('sky'); + + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, + CullFaceMode.disabled, skyUniforms, undefined, 'sky', sky.vertexBuffer, + sky.indexBuffer, sky.segments); +} diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts index b8ed2d59f2..f4767e304f 100644 --- a/src/render/draw_terrain.ts +++ b/src/render/draw_terrain.ts @@ -83,11 +83,12 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array) { const terrainData = terrain.getTerrainData(tile.tileID); context.activeTexture.set(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture.texture); + const eleDelta = terrain.getMeshFrameDelta(painter.transform.zoom); + const fogMatrix = painter.transform.calculateFogMatrix(tile.tileID.toUnwrapped()); const posMatrix = painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped()); - const uniformValues = terrainUniformValues(posMatrix, terrain.getMeshFrameDelta(painter.transform.zoom)); + const uniformValues = terrainUniformValues(posMatrix, eleDelta, fogMatrix, painter.style.sky, painter.transform.pitch); program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); } - } export { diff --git a/src/render/painter.ts b/src/render/painter.ts index b715f2c252..67a35f3f7f 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -62,6 +62,7 @@ import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types'; import type ResolvedImage from '../style-spec/expression/types/resolved_image'; import type {RGBAImage} from '../util/image'; import RenderToTexture from './render_to_texture'; +import drawSky from './draw_sky'; export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; @@ -446,6 +447,9 @@ class Painter { this.context.clear({color: options.showOverdrawInspector ? Color.black : Color.transparent, depth: 1}); this.clearStencil(); + // draw sky first to not overwrite symbols + if (this.style.sky) drawSky(this, this.style.sky); + this._showOverdrawInspector = options.showOverdrawInspector; this.depthRangeFor3D = [0, 1 - ((style._order.length + 2) * this.numSublayers * this.depthEpsilon)]; diff --git a/src/render/program/program_uniforms.ts b/src/render/program/program_uniforms.ts index f1723ccac7..a080bb9578 100644 --- a/src/render/program/program_uniforms.ts +++ b/src/render/program/program_uniforms.ts @@ -11,6 +11,7 @@ import {rasterUniforms} from './raster_program'; import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program'; import {backgroundUniforms, backgroundPatternUniforms} from './background_program'; import {terrainUniforms, terrainDepthUniforms, terrainCoordsUniforms} from './terrain_program'; +import {skyUniforms} from './sky_program'; export const programUniforms = { fillExtrusion: fillExtrusionUniforms, @@ -40,5 +41,6 @@ export const programUniforms = { backgroundPattern: backgroundPatternUniforms, terrain: terrainUniforms, terrainDepth: terrainDepthUniforms, - terrainCoords: terrainCoordsUniforms + terrainCoords: terrainCoordsUniforms, + sky: skyUniforms }; diff --git a/src/render/program/sky_program.ts b/src/render/program/sky_program.ts new file mode 100644 index 0000000000..82122db10f --- /dev/null +++ b/src/render/program/sky_program.ts @@ -0,0 +1,28 @@ +import {UniformColor, Uniform1f} from '../uniform_binding'; +import type Context from '../../gl/context'; +import type {UniformValues, UniformLocations} from '../uniform_binding'; +import Transform from '../../geo/transform'; +import Sky from '../../style/sky'; + +export type SkyUniformsType = { + 'u_sky_color': UniformColor; + 'u_fog_color': UniformColor; + 'u_horizon': Uniform1f; + 'u_horizon_blend': Uniform1f; +}; + +const skyUniforms = (context: Context, locations: UniformLocations): SkyUniformsType => ({ + 'u_sky_color': new UniformColor(context, locations.u_sky_color), + 'u_fog_color': new UniformColor(context, locations.u_fog_color), + 'u_horizon': new Uniform1f(context, locations.u_horizon), + 'u_horizon_blend': new Uniform1f(context, locations.u_horizon_blend) +}); + +const skyUniformValues = (sky: Sky, transform: Transform, pixelRatio: number): UniformValues => ({ + 'u_sky_color': sky.properties.get('sky-color'), + 'u_fog_color': sky.properties.get('fog-color'), + 'u_horizon': (transform.height / 2 + transform.getHorizon()) * pixelRatio, + 'u_horizon_blend': (sky.properties.get('horizon-blend') * transform.height / 2) * pixelRatio +}); + +export {skyUniforms, skyUniformValues}; diff --git a/src/render/program/terrain_program.ts b/src/render/program/terrain_program.ts index 0f96737a12..2d39da5922 100644 --- a/src/render/program/terrain_program.ts +++ b/src/render/program/terrain_program.ts @@ -2,11 +2,14 @@ import { Uniform1i, Uniform1f, Uniform4f, - UniformMatrix4f + UniformMatrix4f, + UniformColor } from '../uniform_binding'; import type Context from '../../gl/context'; import type {UniformValues, UniformLocations} from '../../render/uniform_binding'; import {mat4} from 'gl-matrix'; +import Sky from '../../style/sky'; +import Color from '../../style-spec/util/color'; export type TerrainPreludeUniformsType = { 'u_depth': Uniform1i; @@ -21,6 +24,10 @@ export type TerrainUniformsType = { 'u_matrix': UniformMatrix4f; 'u_texture': Uniform1i; 'u_ele_delta': Uniform1f; + 'u_fog_matrix': UniformMatrix4f; + 'u_fog_color': UniformColor; + 'u_fog_blend': Uniform1f; + 'u_fog_blend_opacity': Uniform1f; }; export type TerrainDepthUniformsType = { @@ -47,7 +54,11 @@ const terrainPreludeUniforms = (context: Context, locations: UniformLocations): const terrainUniforms = (context: Context, locations: UniformLocations): TerrainUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), 'u_texture': new Uniform1i(context, locations.u_texture), - 'u_ele_delta': new Uniform1f(context, locations.u_ele_delta) + 'u_ele_delta': new Uniform1f(context, locations.u_ele_delta), + 'u_fog_matrix': new UniformMatrix4f(context, locations.u_fog_matrix), + 'u_fog_color': new UniformColor(context, locations.u_fog_color), + 'u_fog_blend': new Uniform1f(context, locations.u_fog_blend), + 'u_fog_blend_opacity': new Uniform1f(context, locations.u_fog_blend_opacity) }); const terrainDepthUniforms = (context: Context, locations: UniformLocations): TerrainDepthUniformsType => ({ @@ -64,11 +75,18 @@ const terrainCoordsUniforms = (context: Context, locations: UniformLocations): T const terrainUniformValues = ( matrix: mat4, - eleDelta: number + eleDelta: number, + fogMatrix: mat4, + sky: Sky, + pitch: number ): UniformValues => ({ 'u_matrix': matrix, 'u_texture': 0, - 'u_ele_delta': eleDelta + 'u_ele_delta': eleDelta, + 'u_fog_matrix': fogMatrix, + 'u_fog_color': sky ? sky.properties.get('fog-color') : Color.white, + 'u_fog_blend': sky ? sky.properties.get('fog-blend') : 1, + 'u_fog_blend_opacity': sky ? sky.calculateFogBlendOpacity(pitch) : 0 }); const terrainDepthUniformValues = ( diff --git a/src/shaders/_prelude.vertex.glsl b/src/shaders/_prelude.vertex.glsl index b190a31ff8..f6702744ac 100644 --- a/src/shaders/_prelude.vertex.glsl +++ b/src/shaders/_prelude.vertex.glsl @@ -85,11 +85,11 @@ uniform highp sampler2D u_depth; // methods for pack/unpack depth value to texture rgba // https://stackoverflow.com/questions/34963366/encode-floating-point-data-in-a-rgba-texture -const highp vec4 bitSh = vec4(256. * 256. * 256., 256. * 256., 256., 1.); -const highp vec4 bitShifts = vec4(1.) / bitSh; +const highp vec4 bitSh = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0); +const highp vec4 bitShifts = vec4(1.0) / bitSh; highp float unpack(highp vec4 color) { - return dot(color , bitShifts); + return dot(color, bitShifts); } // calculate the opacity behind terrain, returns a value between 0 and 1. diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index 1adfea6d19..3b6b77e57b 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -56,7 +56,11 @@ import symbolTextAndIconVert from './symbol_text_and_icon.vertex.glsl.g'; import terrainDepthFrag from './terrain_depth.fragment.glsl.g'; import terrainCoordsFrag from './terrain_coords.fragment.glsl.g'; import terrainFrag from './terrain.fragment.glsl.g'; +import terrainDepthVert from './terrain_depth.vertex.glsl.g'; +import terrainCoordsVert from './terrain_coords.vertex.glsl.g'; import terrainVert from './terrain.vertex.glsl.g'; +import skyFrag from './sky.fragment.glsl.g'; +import skyVert from './sky.vertex.glsl.g'; const shaders = { prelude: compile(preludeFrag, preludeVert), @@ -86,8 +90,9 @@ const shaders = { symbolSDF: compile(symbolSDFFrag, symbolSDFVert), symbolTextAndIcon: compile(symbolTextAndIconFrag, symbolTextAndIconVert), terrain: compile(terrainFrag, terrainVert), - terrainDepth: compile(terrainDepthFrag, terrainVert), - terrainCoords: compile(terrainCoordsFrag, terrainVert) + terrainDepth: compile(terrainDepthFrag, terrainDepthVert), + terrainCoords: compile(terrainCoordsFrag, terrainCoordsVert), + sky: compile(skyFrag, skyVert) }; export default shaders; diff --git a/src/shaders/sky.fragment.glsl b/src/shaders/sky.fragment.glsl new file mode 100644 index 0000000000..d0652361c0 --- /dev/null +++ b/src/shaders/sky.fragment.glsl @@ -0,0 +1,18 @@ +uniform vec4 u_sky_color; +uniform vec4 u_fog_color; +uniform float u_horizon; +uniform float u_horizon_blend; + +void main() { + float y = gl_FragCoord.y; + if (y > u_horizon) { + float blend = y - u_horizon; + if (blend < u_horizon_blend) { + gl_FragColor = mix(u_sky_color, u_fog_color, pow(1.0 - blend / u_horizon_blend, 2.0)); + } else { + gl_FragColor = u_sky_color; + } + } else { + gl_FragColor = u_fog_color; + } +} diff --git a/src/shaders/sky.vertex.glsl b/src/shaders/sky.vertex.glsl new file mode 100644 index 0000000000..594982ab06 --- /dev/null +++ b/src/shaders/sky.vertex.glsl @@ -0,0 +1,5 @@ +attribute vec2 a_pos; + +void main() { + gl_Position = vec4(a_pos, 1.0, 1.0); +} diff --git a/src/shaders/terrain.fragment.glsl b/src/shaders/terrain.fragment.glsl index 14e2517dab..c92080e42e 100644 --- a/src/shaders/terrain.fragment.glsl +++ b/src/shaders/terrain.fragment.glsl @@ -1,7 +1,17 @@ uniform sampler2D u_texture; +uniform vec4 u_fog_color; +uniform float u_fog_blend; +uniform float u_fog_blend_opacity; varying vec2 v_texture_pos; +varying float v_fog_depth; void main() { - gl_FragColor = texture2D(u_texture, v_texture_pos); + vec4 color = texture2D(u_texture, v_texture_pos); + if (v_fog_depth > u_fog_blend) { + float a = (v_fog_depth - u_fog_blend) / (1.0 - u_fog_blend); + gl_FragColor = mix(color, u_fog_color, pow(a * u_fog_blend_opacity, 2.0)); + } else { + gl_FragColor = color; + } } diff --git a/src/shaders/terrain.vertex.glsl b/src/shaders/terrain.vertex.glsl index 8cc3f0ba1b..dd4708030c 100644 --- a/src/shaders/terrain.vertex.glsl +++ b/src/shaders/terrain.vertex.glsl @@ -1,15 +1,17 @@ attribute vec3 a_pos3d; uniform mat4 u_matrix; +uniform mat4 u_fog_matrix; uniform float u_ele_delta; varying vec2 v_texture_pos; -varying float v_depth; +varying float v_fog_depth; void main() { - float extent = 8192.0; // 8192.0 is the hardcoded vector-tiles coordinates resolution + float ele = get_elevation(a_pos3d.xy); float ele_delta = a_pos3d.z == 1.0 ? u_ele_delta : 0.0; - v_texture_pos = a_pos3d.xy / extent; - gl_Position = u_matrix * vec4(a_pos3d.xy, get_elevation(a_pos3d.xy) - ele_delta, 1.0); - v_depth = gl_Position.z / gl_Position.w; + v_texture_pos = a_pos3d.xy / 8192.0; + gl_Position = u_matrix * vec4(a_pos3d.xy, ele - ele_delta, 1.0); + vec4 pos = u_fog_matrix * vec4(a_pos3d.xy, ele, 1.0); + v_fog_depth = pos.z / pos.w * 0.5 + 0.5; } diff --git a/src/shaders/terrain_coords.vertex.glsl b/src/shaders/terrain_coords.vertex.glsl new file mode 100644 index 0000000000..0c6cb41eba --- /dev/null +++ b/src/shaders/terrain_coords.vertex.glsl @@ -0,0 +1,13 @@ +attribute vec3 a_pos3d; + +uniform mat4 u_matrix; +uniform float u_ele_delta; + +varying vec2 v_texture_pos; + +void main() { + float ele = get_elevation(a_pos3d.xy); + float ele_delta = a_pos3d.z == 1.0 ? u_ele_delta : 0.0; + v_texture_pos = a_pos3d.xy / 8192.0; + gl_Position = u_matrix * vec4(a_pos3d.xy, ele - ele_delta, 1.0); +} diff --git a/src/shaders/terrain_depth.fragment.glsl b/src/shaders/terrain_depth.fragment.glsl index ff5cf76d8c..16445be647 100644 --- a/src/shaders/terrain_depth.fragment.glsl +++ b/src/shaders/terrain_depth.fragment.glsl @@ -2,8 +2,8 @@ varying float v_depth; // methods for pack/unpack depth value to texture rgba // https://stackoverflow.com/questions/34963366/encode-floating-point-data-in-a-rgba-texture -const highp vec4 bitSh = vec4(256. * 256. * 256., 256. * 256., 256., 1.); -const highp vec4 bitMsk = vec4(0.,vec3(1./256.0)); +const highp vec4 bitSh = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0); +const highp vec4 bitMsk = vec4(0.0, vec3(1.0 / 256.0)); highp vec4 pack(highp float value) { highp vec4 comp = fract(value * bitSh); comp -= comp.xxyz * bitMsk; diff --git a/src/shaders/terrain_depth.vertex.glsl b/src/shaders/terrain_depth.vertex.glsl new file mode 100644 index 0000000000..d4a2d8b0e9 --- /dev/null +++ b/src/shaders/terrain_depth.vertex.glsl @@ -0,0 +1,13 @@ +attribute vec3 a_pos3d; + +uniform mat4 u_matrix; +uniform float u_ele_delta; + +varying float v_depth; + +void main() { + float ele = get_elevation(a_pos3d.xy); + float ele_delta = a_pos3d.z == 1.0 ? u_ele_delta : 0.0; + gl_Position = u_matrix * vec4(a_pos3d.xy, ele - ele_delta, 1.0); + v_depth = gl_Position.z / gl_Position.w; +} diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 3dc26aa448..6bc0cba123 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -57,6 +57,13 @@ "intensity": 0.4 } }, + "sky": { + "type": "sky", + "doc": "The map sky.", + "example": { + "sky-color": "#199EF3" + } + }, "terrain": { "type": "terrain", "doc": "The terrain configuration.", @@ -3822,6 +3829,84 @@ } } }, + "sky": { + "sky-color": { + "type": "color", + "property-type": "data-constant", + "default": "#88C6FC", + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "Base color for sky.", + "sdk-support": { + "basic functionality": { + "js": "3.0.0" + } + } + }, + "fog-color": { + "type": "color", + "property-type": "data-constant", + "default": "#ffffff", + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "Base color for fog.", + "sdk-support": { + "basic functionality": { + "js": "3.0.0" + } + } + }, + "fog-blend": { + "type": "number", + "property-type": "data-constant", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "Blend fog over terrain3d. A value between 0 and 1. 0 is map center and 1 is the horizon", + "sdk-support": { + "basic functionality": { + "js": "3.0.0" + } + } + }, + "horizon-blend": { + "type": "number", + "property-type": "data-constant", + "default": 0.8, + "minimum": 0, + "maximum": 1, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "Blend fog and sky color at horizon. A value between 0 and 1. 0 is the horizon and 1 is map-height / 2", + "sdk-support": { + "basic functionality": { + "js": "3.0.0" + } + } + } + }, "terrain": { "source": { "type": "string", diff --git a/src/style-spec/validate/validate.ts b/src/style-spec/validate/validate.ts index 4dc2dc02ac..1b9e6f5862 100644 --- a/src/style-spec/validate/validate.ts +++ b/src/style-spec/validate/validate.ts @@ -17,6 +17,7 @@ import validateFilter from './validate_filter'; import validateLayer from './validate_layer'; import validateSource from './validate_source'; import validateLight from './validate_light'; +import validateSky from './validate_sky'; import validateTerrain from './validate_terrain'; import validateString from './validate_string'; import validateFormatted from './validate_formatted'; @@ -39,6 +40,7 @@ const VALIDATORS = { 'object': validateObject, 'source': validateSource, 'light': validateLight, + 'sky': validateSky, 'terrain': validateTerrain, 'string': validateString, 'formatted': validateFormatted, diff --git a/src/style-spec/validate/validate_sky.ts b/src/style-spec/validate/validate_sky.ts new file mode 100644 index 0000000000..58d2a34caf --- /dev/null +++ b/src/style-spec/validate/validate_sky.ts @@ -0,0 +1,34 @@ +import ValidationError from '../error/validation_error'; +import getType from '../util/get_type'; +import validate from './validate'; + +export default function validateSky(options) { + const sky = options.value; + const styleSpec = options.styleSpec; + const skySpec = styleSpec.sky; + const style = options.style; + + const rootType = getType(sky); + if (sky === undefined) { + return []; + } else if (rootType !== 'object') { + return [new ValidationError('sky', sky, `object expected, ${rootType} found`)]; + } + + let errors = []; + for (const key in sky) { + if (skySpec[key]) { + errors = errors.concat(validate({ + key, + value: sky[key], + valueSpec: skySpec[key], + style, + styleSpec + })); + } else { + errors = errors.concat([new ValidationError(key, sky[key], `unknown property "${key}"`)]); + } + } + + return errors; +} diff --git a/src/style-spec/validate_style.min.ts b/src/style-spec/validate_style.min.ts index 2ff408a60c..c1c3359563 100644 --- a/src/style-spec/validate_style.min.ts +++ b/src/style-spec/validate_style.min.ts @@ -6,6 +6,7 @@ import validateGlyphsURL from './validate/validate_glyphs_url'; import validateSource from './validate/validate_source'; import validateLight from './validate/validate_light'; +import validateSky from './validate/validate_sky'; import validateTerrain from './validate/validate_terrain'; import validateLayer from './validate/validate_layer'; import validateFilter from './validate/validate_filter'; @@ -60,6 +61,7 @@ function validateStyleMin(style: StyleSpecification, styleSpec = latestStyleSpec validateStyleMin.source = wrapCleanErrors(validateSource); validateStyleMin.light = wrapCleanErrors(validateLight); +validateStyleMin.sky = wrapCleanErrors(validateSky); validateStyleMin.terrain = wrapCleanErrors(validateTerrain); validateStyleMin.layer = wrapCleanErrors(validateLayer); validateStyleMin.filter = wrapCleanErrors(validateFilter); diff --git a/src/style/sky.ts b/src/style/sky.ts new file mode 100644 index 0000000000..0837122995 --- /dev/null +++ b/src/style/sky.ts @@ -0,0 +1,122 @@ +import {PosArray, TriangleIndexArray} from '../data/array_types.g'; +import posAttributes from '../data/pos_attributes'; +import VertexBuffer from '../gl/vertex_buffer'; +import IndexBuffer from '../gl/index_buffer'; +import SegmentVector from '../data/segment'; +import Context from '../gl/context'; +import {SkySpecification} from '../style-spec/types.g'; +import {Color, StylePropertySpecification} from '../style-spec/style-spec'; +import {DataConstantProperty, PossiblyEvaluated, Properties, Transitionable, Transitioning, TransitionParameters} from './properties'; +import styleSpec from '../style-spec/reference/latest'; +import {Evented} from '../util/evented'; +import validateSky from '../style-spec/validate/validate_sky'; +import EvaluationParameters from './evaluation_parameters'; +import {emitValidationErrors, validateStyle} from './validate_style'; +import {extend} from '../util/util'; + +type Props = { + 'sky-color': DataConstantProperty; + 'fog-color': DataConstantProperty; + 'fog-blend': DataConstantProperty; + 'horizon-blend': DataConstantProperty; +}; + +type PropsPossiblyEvaluated = { + 'sky-color': Color; + 'fog-color': Color; + 'fog-blend': number; + 'horizon-blend': number; +}; + +const properties: Properties = new Properties({ + 'sky-color': new DataConstantProperty(styleSpec.sky['sky-color'] as StylePropertySpecification), + 'fog-color': new DataConstantProperty(styleSpec.sky['fog-color'] as StylePropertySpecification), + 'fog-blend': new DataConstantProperty(styleSpec.sky['fog-blend'] as StylePropertySpecification), + 'horizon-blend': new DataConstantProperty(styleSpec.sky['horizon-blend'] as StylePropertySpecification), +}); + +const TRANSITION_SUFFIX = '-transition'; + +export default class Sky extends Evented { + properties: PossiblyEvaluated; + + _transitionable: Transitionable; + _transitioning: Transitioning; + + vertexBuffer: VertexBuffer; + indexBuffer: IndexBuffer; + segments: SegmentVector; + + constructor(context :Context, sky?: SkySpecification) { + super(); + this._transitionable = new Transitionable(properties); + this.setSky(sky); + this._transitioning = this._transitionable.untransitioned(); + + const vertexArray = new PosArray(); + vertexArray.emplaceBack(-1, -1); + vertexArray.emplaceBack(1, -1); + vertexArray.emplaceBack(1, 1); + vertexArray.emplaceBack(-1, 1); + + const indexArray = new TriangleIndexArray(); + indexArray.emplaceBack(0, 1, 2); + indexArray.emplaceBack(0, 2, 3); + + this.vertexBuffer = context.createVertexBuffer(vertexArray, posAttributes.members); + this.indexBuffer = context.createIndexBuffer(indexArray); + this.segments = SegmentVector.simpleSegment(0, 0, vertexArray.length, indexArray.length); + } + + setSky(sky?: SkySpecification) { + if (this._validate(validateSky, sky)) return; + + for (const name in sky) { + const value = sky[name]; + if (name.endsWith(TRANSITION_SUFFIX)) { + this._transitionable.setTransition(name.slice(0, -TRANSITION_SUFFIX.length) as keyof Props, value); + } else { + this._transitionable.setValue(name as keyof Props, value); + } + } + } + + getSky() { + return this._transitionable.serialize(); + } + + updateTransitions(parameters: TransitionParameters) { + this._transitioning = this._transitionable.transitioned(parameters, this._transitioning); + } + + hasTransition() { + return this._transitioning.hasTransition(); + } + + recalculate(parameters: EvaluationParameters) { + this.properties = this._transitioning.possiblyEvaluate(parameters); + } + + _validate(validate: Function, value: unknown) { + return emitValidationErrors(this, validate.call(validateStyle, extend({ + value, + // Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407 + style: {glyphs: true, sprite: true}, + styleSpec + }))); + } + + // Currently fog is a very simple implementation, and should only used + // to create an atmosphere near the horizon. + // But because the fog is drawn from the far-clipping-plane to + // map-center, and because the fog does nothing know about the horizon, + // this method does a fadeout in respect of pitch. So, when the horizon + // gets out of view, which is at about pitch 70, this methods calculates + // the corresponding opacity values. Below pitch 60 the fog is completely + // invisible. + calculateFogBlendOpacity(pitch) { + if (pitch < 60) return 0; // disable + if (pitch < 70) return (pitch - 60) / 10; // fade in + return 1; + } +} diff --git a/src/style/style.ts b/src/style/style.ts index b464a2cf7b..916a2c68f8 100644 --- a/src/style/style.ts +++ b/src/style/style.ts @@ -56,10 +56,12 @@ import type { StyleSpecification, LightSpecification, SourceSpecification, + SkySpecification, } from '../style-spec/types.g'; import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {Validator} from './validate_style'; import type {OverscaledTileID} from '../source/tile_id'; +import Sky from './sky'; const supportedDiffOperations = pick(diffOperations, [ 'addLayer', @@ -160,6 +162,7 @@ class Style extends Evented { glyphManager: GlyphManager; lineAtlas: LineAtlas; light: Light; + sky: Sky; _request: Cancelable; _spriteRequest: Cancelable; @@ -325,6 +328,8 @@ class Style extends Evented { this.light = new Light(this.stylesheet.light); + if (this.stylesheet.sky) this.setSky(this.stylesheet.sky); + this.map.setTerrain(this.stylesheet.terrain); this.fire(new Event('data', {dataType: 'style'})); @@ -403,6 +408,10 @@ class Style extends Evented { return true; } + if (this.sky && this.sky.hasTransition()) { + return true; + } + for (const id in this.sourceCaches) { if (this.sourceCaches[id].hasTransition()) { return true; @@ -460,6 +469,7 @@ class Style extends Evented { } this.light.updateTransitions(parameters); + if (this.sky) this.sky.updateTransitions(parameters); this._resetUpdates(); } @@ -489,6 +499,7 @@ class Style extends Evented { } this.light.recalculate(parameters); + if (this.sky) this.sky.recalculate(parameters); this.z = parameters.zoom; if (changed) { @@ -1262,6 +1273,28 @@ class Style extends Evented { this.light.updateTransitions(parameters); } + getSky() { + return this.sky && this.sky.getSky(); + } + + setSky(skyOptions?: SkySpecification) { + this._checkLoaded(); + + if (!skyOptions) { + this.sky = null; + } else { + if (this.sky) this.sky.setSky(skyOptions); + else this.sky = new Sky(this.map.painter.context, skyOptions); + this.sky.updateTransitions({ + now: browser.now(), + transition: extend({ + duration: 300, + delay: 0 + }, this.stylesheet.transition) + }); + } + } + _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean; } = {}) { diff --git a/src/style/validate_style.ts b/src/style/validate_style.ts index 81efa18fe1..288de79355 100644 --- a/src/style/validate_style.ts +++ b/src/style/validate_style.ts @@ -15,6 +15,7 @@ type ValidateStyle = { source: Validator; layer: Validator; light: Validator; + sky: Validator; terrain: Validator; filter: Validator; paintProperty: Validator; @@ -26,6 +27,7 @@ export const validateStyle = (validateStyleMin as ValidateStyle); export const validateSource = validateStyle.source; export const validateLight = validateStyle.light; +export const validateSky = validateStyle.sky; export const validateTerrain = validateStyle.terrain; export const validateFilter = validateStyle.filter; export const validatePaintProperty = validateStyle.paintProperty; diff --git a/src/ui/map.ts b/src/ui/map.ts index 67352aaf91..cba3fbb824 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -53,7 +53,8 @@ import type { StyleSpecification, LightSpecification, SourceSpecification, - TerrainSpecification + TerrainSpecification, + SkySpecification } from '../style-spec/types.g'; import {Callback} from '../types/callback'; import type {ControlPosition, IControl} from './control/control'; @@ -2301,6 +2302,30 @@ class Map extends Camera { return this.style.getLight(); } + /** + * Loads a Sky onto the map. + * @param sky Sky properties to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/#sky). + * @param {Object} [options] Options object. + * @param {boolean} [options.validate=true] Whether to check if the filter conforms to the MapLibre GL Style Specification. Disabling validation is a performance optimization that should only be used if you have previously validated the values you will be passing to this function. + * @returns {Map} `this` + * @example + * map.setSky({ 'sky-color': '#00f' }); + */ + setSky(sky: SkySpecification) { + this._lazyInitEmptyStyle(); + this.style.setSky(sky); + return this._update(true); + } + + /** + * Returns the value of the sky object. + * + * @returns {Object} sky Sky properties of the style. + */ + getSky() { + return this.style.getSky(); + } + // eslint-disable-next-line jsdoc/require-returns /** * Sets the `state` of a feature.